Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
Commit
•
4c34e70
1
Parent(s):
3a944ef
playing with stable video diffusion
Browse files- src/app/interface/recommended-videos/index.tsx +38 -0
- src/app/interface/video-card/index.tsx +31 -12
- src/app/interface/video-list/index.tsx +5 -2
- src/app/server/actions/ai-tube-hf/getChannelVideos.ts +5 -0
- src/app/server/actions/ai-tube-hf/getVideoRequestsFromChannel.ts +9 -3
- src/app/server/actions/ai-tube-hf/getVideos.ts +63 -15
- src/app/server/actions/ai-tube-hf/parseChannel.ts +6 -3
- src/app/server/actions/ai-tube-hf/uploadVideoRequestToDataset.ts +43 -1
- src/app/server/actions/submitVideoRequest.ts +16 -2
- src/app/server/actions/utils/parseDatasetPrompt.ts +30 -7
- src/app/server/actions/utils/parseDatasetReadme.ts +9 -4
- src/app/server/actions/utils/parseVideoModelName.ts +24 -0
- src/app/state/useStore.ts +10 -0
- src/app/views/home-view/index.tsx +1 -2
- src/app/views/public-channel-view/index.tsx +0 -1
- src/app/views/public-video-view/index.tsx +9 -4
- src/app/views/user-channel-view/index.tsx +46 -2
- src/types.ts +66 -4
- tailwind.config.js +3 -0
src/app/interface/recommended-videos/index.tsx
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
import { useEffect, useTransition } from "react"
|
3 |
+
|
4 |
+
import { useStore } from "@/app/state/useStore"
|
5 |
+
import { cn } from "@/lib/utils"
|
6 |
+
import { VideoInfo } from "@/types"
|
7 |
+
|
8 |
+
import { VideoList } from "../video-list"
|
9 |
+
import { getVideos } from "@/app/server/actions/ai-tube-hf/getVideos"
|
10 |
+
|
11 |
+
export function RecommendedVideos({
|
12 |
+
video,
|
13 |
+
}: {
|
14 |
+
// the video to use for the recommendations
|
15 |
+
video: VideoInfo
|
16 |
+
}) {
|
17 |
+
const [_isPending, startTransition] = useTransition()
|
18 |
+
const setRecommendedVideos = useStore(s => s.setRecommendedVideos)
|
19 |
+
const recommendedVideos = useStore(s => s.recommendedVideos)
|
20 |
+
|
21 |
+
useEffect(() => {
|
22 |
+
startTransition(async () => {
|
23 |
+
setRecommendedVideos(await getVideos({
|
24 |
+
sortBy: "random",
|
25 |
+
niceToHaveTags: video.tags,
|
26 |
+
ignoreVideoIds: [video.id],
|
27 |
+
maxVideos: 16
|
28 |
+
}))
|
29 |
+
})
|
30 |
+
}, video.tags)
|
31 |
+
|
32 |
+
return (
|
33 |
+
<VideoList
|
34 |
+
videos={recommendedVideos}
|
35 |
+
layout="vertical"
|
36 |
+
/>
|
37 |
+
)
|
38 |
+
}
|
src/app/interface/video-card/index.tsx
CHANGED
@@ -12,10 +12,12 @@ const defaultChannelThumbnail = "/huggingface-avatar.jpeg"
|
|
12 |
export function VideoCard({
|
13 |
video,
|
14 |
className = "",
|
|
|
15 |
onSelect,
|
16 |
}: {
|
17 |
video: VideoInfo
|
18 |
className?: string
|
|
|
19 |
onSelect?: (video: VideoInfo) => void
|
20 |
}) {
|
21 |
const ref = useRef<HTMLVideoElement>(null)
|
@@ -23,6 +25,8 @@ export function VideoCard({
|
|
23 |
|
24 |
const [channelThumbnail, setChannelThumbnail] = useState(video.channel.thumbnail)
|
25 |
|
|
|
|
|
26 |
const handlePointerEnter = () => {
|
27 |
// ref.current?.load()
|
28 |
ref.current?.play()
|
@@ -55,10 +59,9 @@ export function VideoCard({
|
|
55 |
<Link href={`/watch?v=${video.id}`}>
|
56 |
<div
|
57 |
className={cn(
|
58 |
-
`w-full`,
|
59 |
-
`flex flex-col`,
|
60 |
`bg-line-900`,
|
61 |
-
`space-y-3`,
|
62 |
`cursor-pointer`,
|
63 |
className,
|
64 |
)}
|
@@ -66,16 +69,20 @@ export function VideoCard({
|
|
66 |
onPointerLeave={handlePointerLeave}
|
67 |
// onClick={handleClick}
|
68 |
>
|
|
|
69 |
<div
|
70 |
className={cn(
|
71 |
-
`flex flex-col
|
72 |
`rounded-xl overflow-hidden`,
|
|
|
73 |
)}
|
74 |
>
|
75 |
<video
|
76 |
ref={ref}
|
77 |
src={video.assetUrl}
|
78 |
-
className=
|
|
|
|
|
79 |
onLoadedMetadata={handleLoad}
|
80 |
muted
|
81 |
/>
|
@@ -100,29 +107,41 @@ export function VideoCard({
|
|
100 |
</div>
|
101 |
</div>
|
102 |
</div>
|
|
|
|
|
103 |
<div className={cn(
|
104 |
-
`flex flex-row
|
|
|
105 |
)}>
|
106 |
-
<div className="flex flex-col">
|
107 |
<div className="flex w-9 rounded-full overflow-hidden">
|
108 |
<img
|
109 |
src={channelThumbnail}
|
110 |
onError={handleBadChannelThumbnail}
|
111 |
/>
|
112 |
</div>
|
113 |
-
</div>
|
114 |
-
<div className=
|
115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
116 |
<div className={cn(
|
117 |
`flex flex-row items-center`,
|
118 |
-
`text-neutral-400
|
|
|
119 |
)}>
|
120 |
<div>{video.channel.label}</div>
|
121 |
<div><RiCheckboxCircleFill className="" /></div>
|
122 |
</div>
|
|
|
123 |
<div className={cn(
|
124 |
`flex flex-row`,
|
125 |
-
`text-neutral-400
|
|
|
126 |
`space-x-1`
|
127 |
)}>
|
128 |
<div>0 views</div>
|
|
|
12 |
export function VideoCard({
|
13 |
video,
|
14 |
className = "",
|
15 |
+
layout = "normal",
|
16 |
onSelect,
|
17 |
}: {
|
18 |
video: VideoInfo
|
19 |
className?: string
|
20 |
+
layout?: "normal" | "compact"
|
21 |
onSelect?: (video: VideoInfo) => void
|
22 |
}) {
|
23 |
const ref = useRef<HTMLVideoElement>(null)
|
|
|
25 |
|
26 |
const [channelThumbnail, setChannelThumbnail] = useState(video.channel.thumbnail)
|
27 |
|
28 |
+
const isCompact = layout === "compact"
|
29 |
+
|
30 |
const handlePointerEnter = () => {
|
31 |
// ref.current?.load()
|
32 |
ref.current?.play()
|
|
|
59 |
<Link href={`/watch?v=${video.id}`}>
|
60 |
<div
|
61 |
className={cn(
|
62 |
+
`w-full flex`,
|
63 |
+
isCompact ? `flex-row h-24 py-1 space-x-2` : `flex-col space-y-3`,
|
64 |
`bg-line-900`,
|
|
|
65 |
`cursor-pointer`,
|
66 |
className,
|
67 |
)}
|
|
|
69 |
onPointerLeave={handlePointerLeave}
|
70 |
// onClick={handleClick}
|
71 |
>
|
72 |
+
{/* VIDEO BLOCK */}
|
73 |
<div
|
74 |
className={cn(
|
75 |
+
`flex flex-col items-center justify-center`,
|
76 |
`rounded-xl overflow-hidden`,
|
77 |
+
isCompact ? `w-42 h-[94px]` : `aspect-video`
|
78 |
)}
|
79 |
>
|
80 |
<video
|
81 |
ref={ref}
|
82 |
src={video.assetUrl}
|
83 |
+
className={cn(
|
84 |
+
`w-full`
|
85 |
+
)}
|
86 |
onLoadedMetadata={handleLoad}
|
87 |
muted
|
88 |
/>
|
|
|
107 |
</div>
|
108 |
</div>
|
109 |
</div>
|
110 |
+
|
111 |
+
{/* TEXT BLOCK */}
|
112 |
<div className={cn(
|
113 |
+
`flex flex-row`,
|
114 |
+
isCompact ? `w-51` : `space-x-4`,
|
115 |
)}>
|
116 |
+
{isCompact ? null : <div className="flex flex-col">
|
117 |
<div className="flex w-9 rounded-full overflow-hidden">
|
118 |
<img
|
119 |
src={channelThumbnail}
|
120 |
onError={handleBadChannelThumbnail}
|
121 |
/>
|
122 |
</div>
|
123 |
+
</div>}
|
124 |
+
<div className={cn(
|
125 |
+
`flex flex-col`,
|
126 |
+
isCompact ? `` : `flex-grow`
|
127 |
+
)}>
|
128 |
+
<h3 className={cn(
|
129 |
+
`text-zinc-100 font-medium mb-0 line-clamp-2`,
|
130 |
+
isCompact ? `text-sm mb-1.5` : `text-base`
|
131 |
+
)}>{video.label}</h3>
|
132 |
<div className={cn(
|
133 |
`flex flex-row items-center`,
|
134 |
+
`text-neutral-400 font-normal space-x-1`,
|
135 |
+
isCompact ? `text-xs` : `text-sm`
|
136 |
)}>
|
137 |
<div>{video.channel.label}</div>
|
138 |
<div><RiCheckboxCircleFill className="" /></div>
|
139 |
</div>
|
140 |
+
|
141 |
<div className={cn(
|
142 |
`flex flex-row`,
|
143 |
+
`text-neutral-400 font-normal`,
|
144 |
+
isCompact ? `text-xs` : `text-sm`,
|
145 |
`space-x-1`
|
146 |
)}>
|
147 |
<div>0 views</div>
|
src/app/interface/video-list/index.tsx
CHANGED
@@ -5,7 +5,7 @@ import { VideoCard } from "../video-card"
|
|
5 |
|
6 |
export function VideoList({
|
7 |
videos,
|
8 |
-
layout = "
|
9 |
className = "",
|
10 |
onSelect,
|
11 |
}: {
|
@@ -18,7 +18,7 @@ export function VideoList({
|
|
18 |
* - based on the device type (eg. a smart TV)
|
19 |
* - a design choice for a particular page
|
20 |
*/
|
21 |
-
layout?: "grid" | "
|
22 |
|
23 |
className?: string
|
24 |
|
@@ -30,6 +30,8 @@ export function VideoList({
|
|
30 |
className={cn(
|
31 |
layout === "grid"
|
32 |
? `grid grid-cols-2 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4`
|
|
|
|
|
33 |
: `flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4`,
|
34 |
className,
|
35 |
)}
|
@@ -39,6 +41,7 @@ export function VideoList({
|
|
39 |
key={video.id}
|
40 |
video={video}
|
41 |
className="w-full"
|
|
|
42 |
onSelect={onSelect}
|
43 |
/>
|
44 |
))}
|
|
|
5 |
|
6 |
export function VideoList({
|
7 |
videos,
|
8 |
+
layout = "grid",
|
9 |
className = "",
|
10 |
onSelect,
|
11 |
}: {
|
|
|
18 |
* - based on the device type (eg. a smart TV)
|
19 |
* - a design choice for a particular page
|
20 |
*/
|
21 |
+
layout?: "grid" | "horizontal" | "vertical"
|
22 |
|
23 |
className?: string
|
24 |
|
|
|
30 |
className={cn(
|
31 |
layout === "grid"
|
32 |
? `grid grid-cols-2 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4`
|
33 |
+
: layout === "vertical"
|
34 |
+
? `grid grid-cols-1 gap-2`
|
35 |
: `flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4`,
|
36 |
className,
|
37 |
)}
|
|
|
41 |
key={video.id}
|
42 |
video={video}
|
43 |
className="w-full"
|
44 |
+
layout={layout === "vertical" ? "compact" : "normal"}
|
45 |
onSelect={onSelect}
|
46 |
/>
|
47 |
))}
|
src/app/server/actions/ai-tube-hf/getChannelVideos.ts
CHANGED
@@ -36,6 +36,11 @@ export async function getChannelVideos({
|
|
36 |
description: v.description,
|
37 |
prompt: v.prompt,
|
38 |
thumbnailUrl: v.thumbnailUrl,
|
|
|
|
|
|
|
|
|
|
|
39 |
assetUrl: "",
|
40 |
numberOfViews: 0,
|
41 |
numberOfLikes: 0,
|
|
|
36 |
description: v.description,
|
37 |
prompt: v.prompt,
|
38 |
thumbnailUrl: v.thumbnailUrl,
|
39 |
+
model: v.model,
|
40 |
+
lora: v.lora,
|
41 |
+
style: v.style,
|
42 |
+
voice: v.voice,
|
43 |
+
music: v.music,
|
44 |
assetUrl: "",
|
45 |
numberOfViews: 0,
|
46 |
numberOfLikes: 0,
|
src/app/server/actions/ai-tube-hf/getVideoRequestsFromChannel.ts
CHANGED
@@ -6,6 +6,7 @@ import { listFiles } from "@/huggingface/hub/src"
|
|
6 |
import { parsePromptFileName } from "../utils/parsePromptFileName"
|
7 |
import { downloadFileAsText } from "./downloadFileAsText"
|
8 |
import { parseDatasetPrompt } from "../utils/parseDatasetPrompt"
|
|
|
9 |
|
10 |
/**
|
11 |
* Return all the videos requests created by a user on their channel
|
@@ -71,7 +72,7 @@ export async function getVideoRequestsFromChannel({
|
|
71 |
continue
|
72 |
}
|
73 |
|
74 |
-
const { title, description, tags, prompt, thumbnail } = parseDatasetPrompt(rawMarkdown)
|
75 |
|
76 |
if (!title || !description || !prompt) {
|
77 |
// console.log("dataset prompt is incomplete or unparseable")
|
@@ -85,15 +86,20 @@ export async function getVideoRequestsFromChannel({
|
|
85 |
? `https://huggingface.co/${repo}/resolve/main/${thumbnail}`
|
86 |
: ""
|
87 |
|
|
|
88 |
const video: VideoRequest = {
|
89 |
id,
|
90 |
label: title,
|
91 |
description,
|
92 |
prompt,
|
93 |
thumbnailUrl,
|
94 |
-
|
|
|
|
|
|
|
|
|
95 |
updatedAt: file.lastCommit?.date || new Date().toISOString(),
|
96 |
-
tags
|
97 |
channel,
|
98 |
}
|
99 |
|
|
|
6 |
import { parsePromptFileName } from "../utils/parsePromptFileName"
|
7 |
import { downloadFileAsText } from "./downloadFileAsText"
|
8 |
import { parseDatasetPrompt } from "../utils/parseDatasetPrompt"
|
9 |
+
import { parseVideoModelName } from "../utils/parseVideoModelName"
|
10 |
|
11 |
/**
|
12 |
* Return all the videos requests created by a user on their channel
|
|
|
72 |
continue
|
73 |
}
|
74 |
|
75 |
+
const { title, description, tags, prompt, thumbnail, model, lora, style, music, voice } = parseDatasetPrompt(rawMarkdown, channel)
|
76 |
|
77 |
if (!title || !description || !prompt) {
|
78 |
// console.log("dataset prompt is incomplete or unparseable")
|
|
|
86 |
? `https://huggingface.co/${repo}/resolve/main/${thumbnail}`
|
87 |
: ""
|
88 |
|
89 |
+
|
90 |
const video: VideoRequest = {
|
91 |
id,
|
92 |
label: title,
|
93 |
description,
|
94 |
prompt,
|
95 |
thumbnailUrl,
|
96 |
+
model,
|
97 |
+
lora,
|
98 |
+
style,
|
99 |
+
voice,
|
100 |
+
music,
|
101 |
updatedAt: file.lastCommit?.date || new Date().toISOString(),
|
102 |
+
tags: Array.isArray(tags) && tags.length ? tags : channel.tags,
|
103 |
channel,
|
104 |
}
|
105 |
|
src/app/server/actions/ai-tube-hf/getVideos.ts
CHANGED
@@ -8,12 +8,25 @@ const HARD_LIMIT = 100
|
|
8 |
|
9 |
// this just return ALL videos on the platform
|
10 |
export async function getVideos({
|
11 |
-
|
|
|
12 |
sortBy = "date",
|
|
|
13 |
maxVideos = HARD_LIMIT,
|
14 |
}: {
|
15 |
-
|
16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
maxVideos?: number
|
18 |
}): Promise<VideoInfo[]> {
|
19 |
// the index is gonna grow more and more,
|
@@ -24,23 +37,58 @@ export async function getVideos({
|
|
24 |
})
|
25 |
|
26 |
|
27 |
-
let
|
28 |
|
29 |
-
|
30 |
-
|
31 |
-
if (requestedTag) {
|
32 |
-
videos = videos
|
33 |
-
.filter(v =>
|
34 |
-
v.tags.map(t => t.toLowerCase().trim()).includes(requestedTag)
|
35 |
-
)
|
36 |
}
|
37 |
|
38 |
if (sortBy === "date") {
|
39 |
-
|
40 |
} else {
|
41 |
-
|
42 |
}
|
43 |
|
44 |
-
|
45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
46 |
}
|
|
|
8 |
|
9 |
// this just return ALL videos on the platform
|
10 |
export async function getVideos({
|
11 |
+
mandatoryTags = [],
|
12 |
+
niceToHaveTags = [],
|
13 |
sortBy = "date",
|
14 |
+
ignoreVideoIds = [],
|
15 |
maxVideos = HARD_LIMIT,
|
16 |
}: {
|
17 |
+
// the videos MUST include those tags
|
18 |
+
mandatoryTags?: string[]
|
19 |
+
|
20 |
+
// tags that we should try to use to filter the videos,
|
21 |
+
// but it isn't a hard limit - TODO: use some semantic search here?
|
22 |
+
niceToHaveTags?: string[]
|
23 |
+
|
24 |
+
sortBy?: "random" | "date"
|
25 |
+
|
26 |
+
// ignore some ids - this is used to not show the same videos again
|
27 |
+
// eg. videos already watched, or disliked etc
|
28 |
+
ignoreVideoIds?: string[]
|
29 |
+
|
30 |
maxVideos?: number
|
31 |
}): Promise<VideoInfo[]> {
|
32 |
// the index is gonna grow more and more,
|
|
|
37 |
})
|
38 |
|
39 |
|
40 |
+
let allPotentiallyValidVideos = Object.values(published)
|
41 |
|
42 |
+
if (ignoreVideoIds.length) {
|
43 |
+
allPotentiallyValidVideos = allPotentiallyValidVideos.filter(video => !ignoreVideoIds.includes(video.id))
|
|
|
|
|
|
|
|
|
|
|
44 |
}
|
45 |
|
46 |
if (sortBy === "date") {
|
47 |
+
allPotentiallyValidVideos.sort(((a, b) => b.updatedAt.localeCompare(a.updatedAt)))
|
48 |
} else {
|
49 |
+
allPotentiallyValidVideos.sort(() => Math.random() - 0.5)
|
50 |
}
|
51 |
|
52 |
+
let videosMatchingFilters: VideoInfo[] = allPotentiallyValidVideos
|
53 |
+
|
54 |
+
// filter videos by mandatory tags, or else we return everything
|
55 |
+
const mandatoryTagsList = mandatoryTags.map(tag => tag.toLowerCase().trim()).filter(tag => tag)
|
56 |
+
if (mandatoryTagsList.length) {
|
57 |
+
videosMatchingFilters = allPotentiallyValidVideos.filter(video =>
|
58 |
+
video.tags.some(tag =>
|
59 |
+
mandatoryTagsList.includes(tag.toLowerCase().trim())
|
60 |
+
)
|
61 |
+
)
|
62 |
+
}
|
63 |
+
|
64 |
+
// filter videos by mandatory tags, or else we return everything
|
65 |
+
const niceToHaveTagsList = niceToHaveTags.map(tag => tag.toLowerCase().trim()).filter(tag => tag)
|
66 |
+
if (niceToHaveTagsList.length) {
|
67 |
+
videosMatchingFilters = videosMatchingFilters.filter(video =>
|
68 |
+
video.tags.some(tag =>
|
69 |
+
mandatoryTagsList.includes(tag.toLowerCase().trim())
|
70 |
+
)
|
71 |
+
)
|
72 |
+
|
73 |
+
// if we don't have enough videos
|
74 |
+
if (videosMatchingFilters.length < maxVideos) {
|
75 |
+
// count how many we need
|
76 |
+
const nbMissingVideos = maxVideos - videosMatchingFilters.length
|
77 |
+
|
78 |
+
// then we try to fill the gap with valid videos from other topics
|
79 |
+
const videosToUseAsFiller = allPotentiallyValidVideos
|
80 |
+
.filter(video => !videosMatchingFilters.some(v => v.id === video.id)) // of course we don't reuse the same
|
81 |
+
// .sort(() => Math.random() - 0.5) // randomize them
|
82 |
+
.slice(0, nbMissingVideos) // and only pick those we need
|
83 |
+
|
84 |
+
videosMatchingFilters = [
|
85 |
+
...videosMatchingFilters,
|
86 |
+
...videosToUseAsFiller,
|
87 |
+
]
|
88 |
+
}
|
89 |
+
}
|
90 |
+
|
91 |
+
|
92 |
+
// we enforce the max limit of HARD_LIMIT (eg. 100)
|
93 |
+
return videosMatchingFilters.slice(0, Math.min(HARD_LIMIT, maxVideos))
|
94 |
}
|
src/app/server/actions/ai-tube-hf/parseChannel.ts
CHANGED
@@ -2,7 +2,7 @@
|
|
2 |
|
3 |
import { Credentials, downloadFile, whoAmI } from "@/huggingface/hub/src"
|
4 |
import { parseDatasetReadme } from "@/app/server/actions/utils/parseDatasetReadme"
|
5 |
-
import { ChannelInfo } from "@/types"
|
6 |
|
7 |
import { adminCredentials } from "../config"
|
8 |
|
@@ -62,13 +62,14 @@ export async function parseChannel(options: {
|
|
62 |
// TODO parse the README to get the proper label
|
63 |
let label = slug.replaceAll("-", " ")
|
64 |
|
65 |
-
let model = ""
|
66 |
let lora = ""
|
67 |
let style = ""
|
68 |
let thumbnail = ""
|
69 |
let prompt = ""
|
70 |
let description = ""
|
71 |
let voice = ""
|
|
|
72 |
let tags: string[] = []
|
73 |
|
74 |
// console.log(`going to read datasets/${name}`)
|
@@ -88,10 +89,11 @@ export async function parseChannel(options: {
|
|
88 |
label = parsedDatasetReadme.pretty_name
|
89 |
description = parsedDatasetReadme.description
|
90 |
thumbnail = parsedDatasetReadme.thumbnail || "thumbnail.jpg"
|
91 |
-
model = parsedDatasetReadme.model
|
92 |
lora = parsedDatasetReadme.lora || ""
|
93 |
style = parsedDatasetReadme.style || ""
|
94 |
voice = parsedDatasetReadme.voice || ""
|
|
|
95 |
|
96 |
thumbnail =
|
97 |
thumbnail.startsWith("http")
|
@@ -119,6 +121,7 @@ export async function parseChannel(options: {
|
|
119 |
lora,
|
120 |
style,
|
121 |
voice,
|
|
|
122 |
thumbnail,
|
123 |
prompt,
|
124 |
likes: options.likes,
|
|
|
2 |
|
3 |
import { Credentials, downloadFile, whoAmI } from "@/huggingface/hub/src"
|
4 |
import { parseDatasetReadme } from "@/app/server/actions/utils/parseDatasetReadme"
|
5 |
+
import { ChannelInfo, VideoGenerationModel } from "@/types"
|
6 |
|
7 |
import { adminCredentials } from "../config"
|
8 |
|
|
|
62 |
// TODO parse the README to get the proper label
|
63 |
let label = slug.replaceAll("-", " ")
|
64 |
|
65 |
+
let model: VideoGenerationModel = "HotshotXL"
|
66 |
let lora = ""
|
67 |
let style = ""
|
68 |
let thumbnail = ""
|
69 |
let prompt = ""
|
70 |
let description = ""
|
71 |
let voice = ""
|
72 |
+
let music = ""
|
73 |
let tags: string[] = []
|
74 |
|
75 |
// console.log(`going to read datasets/${name}`)
|
|
|
89 |
label = parsedDatasetReadme.pretty_name
|
90 |
description = parsedDatasetReadme.description
|
91 |
thumbnail = parsedDatasetReadme.thumbnail || "thumbnail.jpg"
|
92 |
+
model = parsedDatasetReadme.model
|
93 |
lora = parsedDatasetReadme.lora || ""
|
94 |
style = parsedDatasetReadme.style || ""
|
95 |
voice = parsedDatasetReadme.voice || ""
|
96 |
+
music = parsedDatasetReadme.music || ""
|
97 |
|
98 |
thumbnail =
|
99 |
thumbnail.startsWith("http")
|
|
|
121 |
lora,
|
122 |
style,
|
123 |
voice,
|
124 |
+
music,
|
125 |
thumbnail,
|
126 |
prompt,
|
127 |
likes: options.likes,
|
src/app/server/actions/ai-tube-hf/uploadVideoRequestToDataset.ts
CHANGED
@@ -3,7 +3,7 @@
|
|
3 |
import { Blob } from "buffer"
|
4 |
|
5 |
import { Credentials, uploadFile, whoAmI } from "@/huggingface/hub/src"
|
6 |
-
import { ChannelInfo, VideoInfo, VideoRequest } from "@/types"
|
7 |
import { formatPromptFileName } from "../utils/formatPromptFileName"
|
8 |
|
9 |
/**
|
@@ -16,6 +16,11 @@ export async function uploadVideoRequestToDataset({
|
|
16 |
title,
|
17 |
description,
|
18 |
prompt,
|
|
|
|
|
|
|
|
|
|
|
19 |
tags,
|
20 |
}: {
|
21 |
channel: ChannelInfo
|
@@ -23,6 +28,11 @@ export async function uploadVideoRequestToDataset({
|
|
23 |
title: string
|
24 |
description: string
|
25 |
prompt: string
|
|
|
|
|
|
|
|
|
|
|
26 |
tags: string[]
|
27 |
}): Promise<{
|
28 |
videoRequest: VideoRequest
|
@@ -44,11 +54,33 @@ export async function uploadVideoRequestToDataset({
|
|
44 |
// Convert string to a Buffer
|
45 |
const blob = new Blob([`
|
46 |
# Title
|
|
|
47 |
${title}
|
48 |
|
49 |
# Description
|
|
|
50 |
${description}
|
51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
52 |
# Tags
|
53 |
|
54 |
${tags.map(tag => `- ${tag}`).join("\n")}
|
@@ -75,6 +107,11 @@ ${prompt}
|
|
75 |
label: title,
|
76 |
description,
|
77 |
prompt,
|
|
|
|
|
|
|
|
|
|
|
78 |
thumbnailUrl: channel.thumbnail,
|
79 |
updatedAt: new Date().toISOString(),
|
80 |
tags,
|
@@ -87,6 +124,11 @@ ${prompt}
|
|
87 |
label: title,
|
88 |
description,
|
89 |
prompt,
|
|
|
|
|
|
|
|
|
|
|
90 |
thumbnailUrl: channel.thumbnail, // will be generated in async
|
91 |
assetUrl: "", // will be generated in async
|
92 |
numberOfViews: 0,
|
|
|
3 |
import { Blob } from "buffer"
|
4 |
|
5 |
import { Credentials, uploadFile, whoAmI } from "@/huggingface/hub/src"
|
6 |
+
import { ChannelInfo, VideoGenerationModel, VideoInfo, VideoRequest } from "@/types"
|
7 |
import { formatPromptFileName } from "../utils/formatPromptFileName"
|
8 |
|
9 |
/**
|
|
|
16 |
title,
|
17 |
description,
|
18 |
prompt,
|
19 |
+
model,
|
20 |
+
lora,
|
21 |
+
style,
|
22 |
+
voice,
|
23 |
+
music,
|
24 |
tags,
|
25 |
}: {
|
26 |
channel: ChannelInfo
|
|
|
28 |
title: string
|
29 |
description: string
|
30 |
prompt: string
|
31 |
+
model: VideoGenerationModel
|
32 |
+
lora: string
|
33 |
+
style: string
|
34 |
+
voice: string
|
35 |
+
music: string
|
36 |
tags: string[]
|
37 |
}): Promise<{
|
38 |
videoRequest: VideoRequest
|
|
|
54 |
// Convert string to a Buffer
|
55 |
const blob = new Blob([`
|
56 |
# Title
|
57 |
+
|
58 |
${title}
|
59 |
|
60 |
# Description
|
61 |
+
|
62 |
${description}
|
63 |
|
64 |
+
# Model
|
65 |
+
|
66 |
+
${model}
|
67 |
+
|
68 |
+
# LoRA
|
69 |
+
|
70 |
+
${lora}
|
71 |
+
|
72 |
+
# Style
|
73 |
+
|
74 |
+
${style}
|
75 |
+
|
76 |
+
# Voice
|
77 |
+
|
78 |
+
${voice}
|
79 |
+
|
80 |
+
# Music
|
81 |
+
|
82 |
+
${music}
|
83 |
+
|
84 |
# Tags
|
85 |
|
86 |
${tags.map(tag => `- ${tag}`).join("\n")}
|
|
|
107 |
label: title,
|
108 |
description,
|
109 |
prompt,
|
110 |
+
model,
|
111 |
+
style,
|
112 |
+
lora,
|
113 |
+
voice,
|
114 |
+
music,
|
115 |
thumbnailUrl: channel.thumbnail,
|
116 |
updatedAt: new Date().toISOString(),
|
117 |
tags,
|
|
|
124 |
label: title,
|
125 |
description,
|
126 |
prompt,
|
127 |
+
model,
|
128 |
+
style,
|
129 |
+
lora,
|
130 |
+
voice,
|
131 |
+
music,
|
132 |
thumbnailUrl: channel.thumbnail, // will be generated in async
|
133 |
assetUrl: "", // will be generated in async
|
134 |
numberOfViews: 0,
|
src/app/server/actions/submitVideoRequest.ts
CHANGED
@@ -1,9 +1,8 @@
|
|
1 |
"use server"
|
2 |
|
3 |
-
import { ChannelInfo, VideoInfo } from "@/types"
|
4 |
|
5 |
import { uploadVideoRequestToDataset } from "./ai-tube-hf/uploadVideoRequestToDataset"
|
6 |
-
import { updateQueue } from "./ai-tube-robot/updateQueue"
|
7 |
|
8 |
export async function submitVideoRequest({
|
9 |
channel,
|
@@ -11,6 +10,11 @@ export async function submitVideoRequest({
|
|
11 |
title,
|
12 |
description,
|
13 |
prompt,
|
|
|
|
|
|
|
|
|
|
|
14 |
tags,
|
15 |
}: {
|
16 |
channel: ChannelInfo
|
@@ -18,6 +22,11 @@ export async function submitVideoRequest({
|
|
18 |
title: string
|
19 |
description: string
|
20 |
prompt: string
|
|
|
|
|
|
|
|
|
|
|
21 |
tags: string[]
|
22 |
}): Promise<VideoInfo> {
|
23 |
if (!apiKey) {
|
@@ -30,6 +39,11 @@ export async function submitVideoRequest({
|
|
30 |
title,
|
31 |
description,
|
32 |
prompt,
|
|
|
|
|
|
|
|
|
|
|
33 |
tags
|
34 |
})
|
35 |
|
|
|
1 |
"use server"
|
2 |
|
3 |
+
import { ChannelInfo, VideoGenerationModel, VideoInfo } from "@/types"
|
4 |
|
5 |
import { uploadVideoRequestToDataset } from "./ai-tube-hf/uploadVideoRequestToDataset"
|
|
|
6 |
|
7 |
export async function submitVideoRequest({
|
8 |
channel,
|
|
|
10 |
title,
|
11 |
description,
|
12 |
prompt,
|
13 |
+
model,
|
14 |
+
lora,
|
15 |
+
style,
|
16 |
+
voice,
|
17 |
+
music,
|
18 |
tags,
|
19 |
}: {
|
20 |
channel: ChannelInfo
|
|
|
22 |
title: string
|
23 |
description: string
|
24 |
prompt: string
|
25 |
+
model: VideoGenerationModel
|
26 |
+
lora: string
|
27 |
+
style: string
|
28 |
+
voice: string
|
29 |
+
music: string
|
30 |
tags: string[]
|
31 |
}): Promise<VideoInfo> {
|
32 |
if (!apiKey) {
|
|
|
39 |
title,
|
40 |
description,
|
41 |
prompt,
|
42 |
+
model,
|
43 |
+
lora,
|
44 |
+
style,
|
45 |
+
voice,
|
46 |
+
music,
|
47 |
tags
|
48 |
})
|
49 |
|
src/app/server/actions/utils/parseDatasetPrompt.ts
CHANGED
@@ -1,24 +1,37 @@
|
|
1 |
|
2 |
-
import { ParsedDatasetPrompt } from "@/types"
|
|
|
3 |
|
4 |
-
export function parseDatasetPrompt(markdown: string
|
5 |
try {
|
6 |
-
const { title, description, tags, prompt, thumbnail } = parseMarkdown(markdown)
|
7 |
|
8 |
return {
|
9 |
title: typeof title === "string" && title ? title : "",
|
10 |
description: typeof description === "string" && description ? description : "",
|
11 |
-
tags:
|
|
|
|
|
12 |
prompt: typeof prompt === "string" && prompt ? prompt : "",
|
|
|
|
|
|
|
13 |
thumbnail: typeof thumbnail === "string" && thumbnail ? thumbnail : "",
|
|
|
|
|
14 |
}
|
15 |
} catch (err) {
|
16 |
return {
|
17 |
title: "",
|
18 |
description: "",
|
19 |
-
tags: [],
|
20 |
-
prompt:
|
|
|
|
|
|
|
21 |
thumbnail: "",
|
|
|
|
|
22 |
}
|
23 |
}
|
24 |
}
|
@@ -33,9 +46,14 @@ function parseMarkdown(markdown: string): {
|
|
33 |
description: string
|
34 |
tags: string
|
35 |
prompt: string
|
|
|
|
|
|
|
36 |
thumbnail: string
|
|
|
|
|
37 |
} {
|
38 |
-
markdown = markdown
|
39 |
|
40 |
// Improved regular expression to find markdown sections and accommodate multi-line content.
|
41 |
const sectionRegex = /^#+\s+(?<key>.+?)\n\n?(?<content>[^#]+)/gm;
|
@@ -53,6 +71,11 @@ function parseMarkdown(markdown: string): {
|
|
53 |
description: sections["description"] || "",
|
54 |
tags: sections["tags"] || "",
|
55 |
prompt: sections["prompt"] || "",
|
|
|
|
|
|
|
56 |
thumbnail: sections["thumbnail"] || "",
|
|
|
|
|
57 |
};
|
58 |
}
|
|
|
1 |
|
2 |
+
import { ChannelInfo, ParsedDatasetPrompt } from "@/types"
|
3 |
+
import { parseVideoModelName } from "./parseVideoModelName"
|
4 |
|
5 |
+
export function parseDatasetPrompt(markdown: string, channel: ChannelInfo): ParsedDatasetPrompt {
|
6 |
try {
|
7 |
+
const { title, description, tags, prompt, model, lora, style, thumbnail, voice, music } = parseMarkdown(markdown)
|
8 |
|
9 |
return {
|
10 |
title: typeof title === "string" && title ? title : "",
|
11 |
description: typeof description === "string" && description ? description : "",
|
12 |
+
tags:
|
13 |
+
tags && typeof tags === "string" ? tags.split("-").map(x => x.trim()).filter(x => x)
|
14 |
+
: (channel.tags || []),
|
15 |
prompt: typeof prompt === "string" && prompt ? prompt : "",
|
16 |
+
model: parseVideoModelName(model, channel.model),
|
17 |
+
lora: typeof lora === "string" && lora ? lora : (channel.lora || ""),
|
18 |
+
style: typeof style === "string" && style ? style : (channel.style || ""),
|
19 |
thumbnail: typeof thumbnail === "string" && thumbnail ? thumbnail : "",
|
20 |
+
voice: typeof voice === "string" && voice ? voice : (channel.voice || ""),
|
21 |
+
music: typeof music === "string" && music ? music : (channel.music || ""),
|
22 |
}
|
23 |
} catch (err) {
|
24 |
return {
|
25 |
title: "",
|
26 |
description: "",
|
27 |
+
tags: channel.tags || [],
|
28 |
+
prompt: "",
|
29 |
+
model: channel.model || "HotshotXL",
|
30 |
+
lora: channel.lora || "",
|
31 |
+
style: channel.style || "",
|
32 |
thumbnail: "",
|
33 |
+
voice: channel.voice || "",
|
34 |
+
music: channel.music || "",
|
35 |
}
|
36 |
}
|
37 |
}
|
|
|
46 |
description: string
|
47 |
tags: string
|
48 |
prompt: string
|
49 |
+
model: string
|
50 |
+
lora: string
|
51 |
+
style: string
|
52 |
thumbnail: string
|
53 |
+
voice: string
|
54 |
+
music: string
|
55 |
} {
|
56 |
+
markdown = `${markdown || ""}`.trim()
|
57 |
|
58 |
// Improved regular expression to find markdown sections and accommodate multi-line content.
|
59 |
const sectionRegex = /^#+\s+(?<key>.+?)\n\n?(?<content>[^#]+)/gm;
|
|
|
71 |
description: sections["description"] || "",
|
72 |
tags: sections["tags"] || "",
|
73 |
prompt: sections["prompt"] || "",
|
74 |
+
model: sections["model"] || "",
|
75 |
+
lora: sections["lora"] || "",
|
76 |
+
style: sections["style"] || "",
|
77 |
thumbnail: sections["thumbnail"] || "",
|
78 |
+
voice: sections["voice"] || "",
|
79 |
+
music: sections["music"] || "",
|
80 |
};
|
81 |
}
|
src/app/server/actions/utils/parseDatasetReadme.ts
CHANGED
@@ -2,6 +2,7 @@
|
|
2 |
import metadataParser from "markdown-yaml-metadata-parser"
|
3 |
|
4 |
import { ParsedDatasetReadme, ParsedMetadataAndContent } from "@/types"
|
|
|
5 |
|
6 |
export function parseDatasetReadme(markdown: string = ""): ParsedDatasetReadme {
|
7 |
try {
|
@@ -11,18 +12,19 @@ export function parseDatasetReadme(markdown: string = ""): ParsedDatasetReadme {
|
|
11 |
|
12 |
// console.log("DEBUG README:", { metadata, content })
|
13 |
|
14 |
-
const { model, lora, style, thumbnail, voice, description, prompt, tags } = parseMarkdown(content)
|
15 |
|
16 |
return {
|
17 |
license: typeof metadata?.license === "string" ? metadata.license : "",
|
18 |
pretty_name: typeof metadata?.pretty_name === "string" ? metadata.pretty_name : "",
|
19 |
hf_tags: Array.isArray(metadata?.tags) ? metadata.tags : [],
|
20 |
tags: tags && typeof tags === "string" ? tags.split("-").map(x => x.trim()).filter(x => x) : [],
|
21 |
-
model,
|
22 |
lora,
|
23 |
-
style,
|
24 |
thumbnail,
|
25 |
voice,
|
|
|
26 |
description,
|
27 |
prompt,
|
28 |
}
|
@@ -32,11 +34,12 @@ export function parseDatasetReadme(markdown: string = ""): ParsedDatasetReadme {
|
|
32 |
pretty_name: "",
|
33 |
hf_tags: [], // Hugging Face tags
|
34 |
tags: [],
|
35 |
-
model: "",
|
36 |
lora: "",
|
37 |
style: "",
|
38 |
thumbnail: "",
|
39 |
voice: "",
|
|
|
40 |
description: "",
|
41 |
prompt: "",
|
42 |
}
|
@@ -54,6 +57,7 @@ function parseMarkdown(markdown: string): {
|
|
54 |
style: string
|
55 |
thumbnail: string
|
56 |
voice: string
|
|
|
57 |
description: string
|
58 |
prompt: string
|
59 |
tags: string
|
@@ -77,6 +81,7 @@ function parseMarkdown(markdown: string): {
|
|
77 |
style: sections["style"] || "",
|
78 |
thumbnail: sections["thumbnail"] || "",
|
79 |
voice: sections["voice"] || "",
|
|
|
80 |
prompt: sections["prompt"] || "",
|
81 |
tags: sections["tags"] || "",
|
82 |
};
|
|
|
2 |
import metadataParser from "markdown-yaml-metadata-parser"
|
3 |
|
4 |
import { ParsedDatasetReadme, ParsedMetadataAndContent } from "@/types"
|
5 |
+
import { parseVideoModelName } from "./parseVideoModelName"
|
6 |
|
7 |
export function parseDatasetReadme(markdown: string = ""): ParsedDatasetReadme {
|
8 |
try {
|
|
|
12 |
|
13 |
// console.log("DEBUG README:", { metadata, content })
|
14 |
|
15 |
+
const { model, lora, style, thumbnail, voice, music, description, prompt, tags } = parseMarkdown(content)
|
16 |
|
17 |
return {
|
18 |
license: typeof metadata?.license === "string" ? metadata.license : "",
|
19 |
pretty_name: typeof metadata?.pretty_name === "string" ? metadata.pretty_name : "",
|
20 |
hf_tags: Array.isArray(metadata?.tags) ? metadata.tags : [],
|
21 |
tags: tags && typeof tags === "string" ? tags.split("-").map(x => x.trim()).filter(x => x) : [],
|
22 |
+
model: parseVideoModelName(model, "HotshotXL"),
|
23 |
lora,
|
24 |
+
style: style && typeof style === "string" ? style.split("- ").map(x => x.trim()).filter(x => x).join(", ") : [].join(", "),
|
25 |
thumbnail,
|
26 |
voice,
|
27 |
+
music,
|
28 |
description,
|
29 |
prompt,
|
30 |
}
|
|
|
34 |
pretty_name: "",
|
35 |
hf_tags: [], // Hugging Face tags
|
36 |
tags: [],
|
37 |
+
model: "HotshotXL",
|
38 |
lora: "",
|
39 |
style: "",
|
40 |
thumbnail: "",
|
41 |
voice: "",
|
42 |
+
music: "",
|
43 |
description: "",
|
44 |
prompt: "",
|
45 |
}
|
|
|
57 |
style: string
|
58 |
thumbnail: string
|
59 |
voice: string
|
60 |
+
music: string
|
61 |
description: string
|
62 |
prompt: string
|
63 |
tags: string
|
|
|
81 |
style: sections["style"] || "",
|
82 |
thumbnail: sections["thumbnail"] || "",
|
83 |
voice: sections["voice"] || "",
|
84 |
+
music: sections["music"] || "",
|
85 |
prompt: sections["prompt"] || "",
|
86 |
tags: sections["tags"] || "",
|
87 |
};
|
src/app/server/actions/utils/parseVideoModelName.ts
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { VideoGenerationModel } from "@/types"
|
2 |
+
|
3 |
+
export function parseVideoModelName(text: any, defaultToUse: VideoGenerationModel): VideoGenerationModel {
|
4 |
+
const rawModelString = `${text || ""}`.trim().toLowerCase()
|
5 |
+
|
6 |
+
let model: VideoGenerationModel = "HotshotXL"
|
7 |
+
|
8 |
+
if (
|
9 |
+
rawModelString === "stable video diffusion" ||
|
10 |
+
rawModelString === "stablevideodiffusion" ||
|
11 |
+
rawModelString === "svd"
|
12 |
+
) {
|
13 |
+
model = "SVD"
|
14 |
+
}
|
15 |
+
|
16 |
+
if (
|
17 |
+
rawModelString === "la vie" ||
|
18 |
+
rawModelString === "lavie"
|
19 |
+
) {
|
20 |
+
model = "LaVie"
|
21 |
+
}
|
22 |
+
|
23 |
+
return defaultToUse
|
24 |
+
}
|
src/app/state/useStore.ts
CHANGED
@@ -55,6 +55,9 @@ export const useStore = create<{
|
|
55 |
userVideos: VideoInfo[]
|
56 |
setUserVideos: (userVideos: VideoInfo[]) => void
|
57 |
|
|
|
|
|
|
|
58 |
// currentPrompts: VideoInfo[]
|
59 |
// setCurrentPrompts: (currentPrompts: VideoInfo[]) => void
|
60 |
}>((set, get) => ({
|
@@ -157,4 +160,11 @@ export const useStore = create<{
|
|
157 |
userVideos: Array.isArray(userVideos) ? userVideos : []
|
158 |
})
|
159 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
160 |
}))
|
|
|
55 |
userVideos: VideoInfo[]
|
56 |
setUserVideos: (userVideos: VideoInfo[]) => void
|
57 |
|
58 |
+
recommendedVideos: VideoInfo[]
|
59 |
+
setRecommendedVideos: (recommendedVideos: VideoInfo[]) => void
|
60 |
+
|
61 |
// currentPrompts: VideoInfo[]
|
62 |
// setCurrentPrompts: (currentPrompts: VideoInfo[]) => void
|
63 |
}>((set, get) => ({
|
|
|
160 |
userVideos: Array.isArray(userVideos) ? userVideos : []
|
161 |
})
|
162 |
},
|
163 |
+
|
164 |
+
recommendedVideos: [],
|
165 |
+
setRecommendedVideos: (recommendedVideos: VideoInfo[]) => {
|
166 |
+
set({
|
167 |
+
recommendedVideos: Array.isArray(recommendedVideos) ? recommendedVideos : []
|
168 |
+
})
|
169 |
+
},
|
170 |
}))
|
src/app/views/home-view/index.tsx
CHANGED
@@ -21,7 +21,7 @@ export function HomeView() {
|
|
21 |
startTransition(async () => {
|
22 |
const videos = await getVideos({
|
23 |
sortBy: "date",
|
24 |
-
|
25 |
maxVideos: 25
|
26 |
})
|
27 |
|
@@ -40,7 +40,6 @@ export function HomeView() {
|
|
40 |
)}>
|
41 |
<VideoList
|
42 |
videos={publicVideos}
|
43 |
-
layout="grid"
|
44 |
onSelect={handleSelect}
|
45 |
/>
|
46 |
</div>
|
|
|
21 |
startTransition(async () => {
|
22 |
const videos = await getVideos({
|
23 |
sortBy: "date",
|
24 |
+
mandatoryTags: currentTag ? [currentTag] : [],
|
25 |
maxVideos: 25
|
26 |
})
|
27 |
|
|
|
40 |
)}>
|
41 |
<VideoList
|
42 |
videos={publicVideos}
|
|
|
43 |
onSelect={handleSelect}
|
44 |
/>
|
45 |
</div>
|
src/app/views/public-channel-view/index.tsx
CHANGED
@@ -35,7 +35,6 @@ export function PublicChannelView() {
|
|
35 |
`flex flex-col`
|
36 |
)}>
|
37 |
<VideoList
|
38 |
-
layout="grid"
|
39 |
videos={publicVideos}
|
40 |
/>
|
41 |
</div>
|
|
|
35 |
`flex flex-col`
|
36 |
)}>
|
37 |
<VideoList
|
|
|
38 |
videos={publicVideos}
|
39 |
/>
|
40 |
</div>
|
src/app/views/public-video-view/index.tsx
CHANGED
@@ -12,6 +12,7 @@ import { cn } from "@/lib/utils"
|
|
12 |
import { VideoPlayer } from "@/app/interface/video-player"
|
13 |
import { VideoInfo } from "@/types"
|
14 |
import { ActionButton } from "@/app/interface/action-button"
|
|
|
15 |
|
16 |
|
17 |
export function PublicVideoView() {
|
@@ -65,10 +66,14 @@ export function PublicVideoView() {
|
|
65 |
|
66 |
{/** VIDEO TITLE - HORIZONTAL */}
|
67 |
<div className={cn(
|
|
|
68 |
`text-xl text-zinc-100 font-medium mb-0 line-clamp-2`,
|
69 |
`mb-2`
|
70 |
)}>
|
71 |
-
{video.label}
|
|
|
|
|
|
|
72 |
</div>
|
73 |
|
74 |
{/** VIDEO TOOLBAR - HORIZONTAL */}
|
@@ -181,11 +186,11 @@ export function PublicVideoView() {
|
|
181 |
</div>
|
182 |
</div>
|
183 |
<div className={cn(
|
184 |
-
`sm:w-56 md:w-
|
185 |
`hidden sm:flex flex-col`,
|
186 |
-
`
|
187 |
)}>
|
188 |
-
|
189 |
</div>
|
190 |
</div>
|
191 |
)
|
|
|
12 |
import { VideoPlayer } from "@/app/interface/video-player"
|
13 |
import { VideoInfo } from "@/types"
|
14 |
import { ActionButton } from "@/app/interface/action-button"
|
15 |
+
import { RecommendedVideos } from "@/app/interface/recommended-videos"
|
16 |
|
17 |
|
18 |
export function PublicVideoView() {
|
|
|
66 |
|
67 |
{/** VIDEO TITLE - HORIZONTAL */}
|
68 |
<div className={cn(
|
69 |
+
`flex flew-row space-x-1`,
|
70 |
`text-xl text-zinc-100 font-medium mb-0 line-clamp-2`,
|
71 |
`mb-2`
|
72 |
)}>
|
73 |
+
<div>{video.label}</div>
|
74 |
+
<div className={cn(``)}>
|
75 |
+
{video.model || "HotshotXL"}
|
76 |
+
</div>
|
77 |
</div>
|
78 |
|
79 |
{/** VIDEO TOOLBAR - HORIZONTAL */}
|
|
|
186 |
</div>
|
187 |
</div>
|
188 |
<div className={cn(
|
189 |
+
`sm:w-56 md:w-[450px]`,
|
190 |
`hidden sm:flex flex-col`,
|
191 |
+
`pl-5 pr-8`,
|
192 |
)}>
|
193 |
+
<RecommendedVideos video={video} />
|
194 |
</div>
|
195 |
</div>
|
196 |
)
|
src/app/views/user-channel-view/index.tsx
CHANGED
@@ -4,7 +4,7 @@ import { useEffect, useState, useTransition } from "react"
|
|
4 |
|
5 |
import { useStore } from "@/app/state/useStore"
|
6 |
import { cn } from "@/lib/utils"
|
7 |
-
import { VideoInfo } from "@/types"
|
8 |
|
9 |
import { useLocalStorage } from "usehooks-ts"
|
10 |
import { localStorageKeys } from "@/app/state/localStorageKeys"
|
@@ -15,6 +15,8 @@ import { Button } from "@/components/ui/button"
|
|
15 |
import { submitVideoRequest } from "@/app/server/actions/submitVideoRequest"
|
16 |
import { PendingVideoList } from "@/app/interface/pending-video-list"
|
17 |
import { getChannelVideos } from "@/app/server/actions/ai-tube-hf/getChannelVideos"
|
|
|
|
|
18 |
|
19 |
export function UserChannelView() {
|
20 |
const [_isPending, startTransition] = useTransition()
|
@@ -22,16 +24,27 @@ export function UserChannelView() {
|
|
22 |
localStorageKeys.huggingfaceApiKey,
|
23 |
defaultSettings.huggingfaceApiKey
|
24 |
)
|
|
|
|
|
|
|
|
|
|
|
25 |
const [titleDraft, setTitleDraft] = useState("")
|
26 |
const [descriptionDraft, setDescriptionDraft] = useState("")
|
27 |
const [tagsDraft, setTagsDraft] = useState("")
|
28 |
const [promptDraft, setPromptDraft] = useState("")
|
29 |
-
|
|
|
|
|
|
|
|
|
|
|
30 |
// we do not include the tags in the list of required fields
|
31 |
const missingFields = !titleDraft || !descriptionDraft || !promptDraft
|
32 |
|
33 |
const [isSubmitting, setIsSubmitting] = useState(false)
|
34 |
|
|
|
35 |
const userChannel = useStore(s => s.userChannel)
|
36 |
const userChannels = useStore(s => s.userChannels)
|
37 |
const userVideos = useStore(s => s.userVideos)
|
@@ -78,6 +91,11 @@ export function UserChannelView() {
|
|
78 |
title: titleDraft,
|
79 |
description: descriptionDraft,
|
80 |
prompt: promptDraft,
|
|
|
|
|
|
|
|
|
|
|
81 |
tags: tagsDraft.trim().split(",").map(x => x.trim()).filter(x => x),
|
82 |
})
|
83 |
|
@@ -88,6 +106,12 @@ export function UserChannelView() {
|
|
88 |
setDescriptionDraft("")
|
89 |
setTagsDraft("")
|
90 |
setTitleDraft("")
|
|
|
|
|
|
|
|
|
|
|
|
|
91 |
// also renew the cache on Next's side
|
92 |
/*
|
93 |
await getChannelVideos({
|
@@ -174,6 +198,26 @@ export function UserChannelView() {
|
|
174 |
</div>
|
175 |
</div>
|
176 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
177 |
<div className="flex flex-row space-x-2 items-start">
|
178 |
<label className="flex w-24 pt-1">Tags (optional):</label>
|
179 |
<div className="flex flex-col space-y-2 flex-grow">
|
|
|
4 |
|
5 |
import { useStore } from "@/app/state/useStore"
|
6 |
import { cn } from "@/lib/utils"
|
7 |
+
import { VideoGenerationModel, VideoInfo } from "@/types"
|
8 |
|
9 |
import { useLocalStorage } from "usehooks-ts"
|
10 |
import { localStorageKeys } from "@/app/state/localStorageKeys"
|
|
|
15 |
import { submitVideoRequest } from "@/app/server/actions/submitVideoRequest"
|
16 |
import { PendingVideoList } from "@/app/interface/pending-video-list"
|
17 |
import { getChannelVideos } from "@/app/server/actions/ai-tube-hf/getChannelVideos"
|
18 |
+
import { parseVideoModelName } from "@/app/server/actions/utils/parseVideoModelName"
|
19 |
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
20 |
|
21 |
export function UserChannelView() {
|
22 |
const [_isPending, startTransition] = useTransition()
|
|
|
24 |
localStorageKeys.huggingfaceApiKey,
|
25 |
defaultSettings.huggingfaceApiKey
|
26 |
)
|
27 |
+
|
28 |
+
const defaultVideoModel = "SVD"
|
29 |
+
const defaultVoice = "Julian"
|
30 |
+
|
31 |
+
|
32 |
const [titleDraft, setTitleDraft] = useState("")
|
33 |
const [descriptionDraft, setDescriptionDraft] = useState("")
|
34 |
const [tagsDraft, setTagsDraft] = useState("")
|
35 |
const [promptDraft, setPromptDraft] = useState("")
|
36 |
+
const [modelDraft, setModelDraft] = useState<VideoGenerationModel>(defaultVideoModel)
|
37 |
+
const [loraDraft, setLoraDraft] = useState("")
|
38 |
+
const [styleDraft, setStyleDraft] = useState("")
|
39 |
+
const [voiceDraft, setVoiceDraft] = useState(defaultVoice)
|
40 |
+
const [musicDraft, setMusicDraft] = useState("")
|
41 |
+
|
42 |
// we do not include the tags in the list of required fields
|
43 |
const missingFields = !titleDraft || !descriptionDraft || !promptDraft
|
44 |
|
45 |
const [isSubmitting, setIsSubmitting] = useState(false)
|
46 |
|
47 |
+
|
48 |
const userChannel = useStore(s => s.userChannel)
|
49 |
const userChannels = useStore(s => s.userChannels)
|
50 |
const userVideos = useStore(s => s.userVideos)
|
|
|
91 |
title: titleDraft,
|
92 |
description: descriptionDraft,
|
93 |
prompt: promptDraft,
|
94 |
+
model: modelDraft,
|
95 |
+
lora: loraDraft,
|
96 |
+
style: styleDraft,
|
97 |
+
voice: voiceDraft,
|
98 |
+
music: musicDraft,
|
99 |
tags: tagsDraft.trim().split(",").map(x => x.trim()).filter(x => x),
|
100 |
})
|
101 |
|
|
|
106 |
setDescriptionDraft("")
|
107 |
setTagsDraft("")
|
108 |
setTitleDraft("")
|
109 |
+
setModelDraft(defaultVideoModel)
|
110 |
+
setVoiceDraft(defaultVoice)
|
111 |
+
setMusicDraft("")
|
112 |
+
setLoraDraft("")
|
113 |
+
setStyleDraft("")
|
114 |
+
|
115 |
// also renew the cache on Next's side
|
116 |
/*
|
117 |
await getChannelVideos({
|
|
|
198 |
</div>
|
199 |
</div>
|
200 |
|
201 |
+
<div className="flex flex-row space-x-2 items-start">
|
202 |
+
<label className="flex w-24 pt-1">Video model:</label>
|
203 |
+
<div className="flex flex-col space-y-2 flex-grow">
|
204 |
+
<Select
|
205 |
+
onValueChange={(value: string) => {
|
206 |
+
setModelDraft(parseVideoModelName(value, defaultVideoModel))
|
207 |
+
}}
|
208 |
+
defaultValue={defaultVideoModel}>
|
209 |
+
<SelectTrigger className="">
|
210 |
+
<SelectValue placeholder="Video model" />
|
211 |
+
</SelectTrigger>
|
212 |
+
<SelectContent>
|
213 |
+
<SelectItem value="SVD">SVD</SelectItem>
|
214 |
+
<SelectItem value="HotshotXL">HotshotXL</SelectItem>
|
215 |
+
<SelectItem value="LaVie">LaVie</SelectItem>
|
216 |
+
</SelectContent>
|
217 |
+
</Select>
|
218 |
+
</div>
|
219 |
+
</div>
|
220 |
+
|
221 |
<div className="flex flex-row space-x-2 items-start">
|
222 |
<label className="flex w-24 pt-1">Tags (optional):</label>
|
223 |
<div className="flex flex-col space-y-2 flex-grow">
|
src/types.ts
CHANGED
@@ -211,7 +211,7 @@ export type ChannelInfo = {
|
|
211 |
|
212 |
thumbnail: string
|
213 |
|
214 |
-
model:
|
215 |
|
216 |
lora: string
|
217 |
|
@@ -219,6 +219,8 @@ export type ChannelInfo = {
|
|
219 |
|
220 |
voice: string
|
221 |
|
|
|
|
|
222 |
/**
|
223 |
* The system prompt
|
224 |
*/
|
@@ -277,6 +279,31 @@ export type VideoRequest = {
|
|
277 |
*/
|
278 |
tags: string[]
|
279 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
280 |
/**
|
281 |
* ID of the channel
|
282 |
*/
|
@@ -344,12 +371,42 @@ export type VideoInfo = {
|
|
344 |
*/
|
345 |
tags: string[]
|
346 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
347 |
/**
|
348 |
* The channel
|
349 |
*/
|
350 |
channel: ChannelInfo
|
351 |
}
|
352 |
|
|
|
|
|
|
|
|
|
|
|
353 |
export type InterfaceDisplayMode =
|
354 |
| "desktop"
|
355 |
| "tv"
|
@@ -383,11 +440,12 @@ export type Settings = {
|
|
383 |
export type ParsedDatasetReadme = {
|
384 |
license: string
|
385 |
pretty_name: string
|
386 |
-
model:
|
387 |
lora: string
|
388 |
style: string
|
389 |
thumbnail: string
|
390 |
voice: string
|
|
|
391 |
tags: string[]
|
392 |
hf_tags: string[]
|
393 |
description: string
|
@@ -406,12 +464,16 @@ export type ParsedMetadataAndContent = {
|
|
406 |
export type ParsedDatasetPrompt = {
|
407 |
title: string
|
408 |
description: string
|
409 |
-
tags: string[]
|
410 |
prompt: string
|
|
|
|
|
|
|
|
|
411 |
thumbnail: string
|
|
|
|
|
412 |
}
|
413 |
|
414 |
-
|
415 |
export type UpdateQueueRequest = {
|
416 |
channel?: ChannelInfo
|
417 |
apiKey: string
|
|
|
211 |
|
212 |
thumbnail: string
|
213 |
|
214 |
+
model: VideoGenerationModel
|
215 |
|
216 |
lora: string
|
217 |
|
|
|
219 |
|
220 |
voice: string
|
221 |
|
222 |
+
music: string
|
223 |
+
|
224 |
/**
|
225 |
* The system prompt
|
226 |
*/
|
|
|
279 |
*/
|
280 |
tags: string[]
|
281 |
|
282 |
+
/**
|
283 |
+
* Model name
|
284 |
+
*/
|
285 |
+
model: VideoGenerationModel
|
286 |
+
|
287 |
+
/**
|
288 |
+
* LoRA name
|
289 |
+
*/
|
290 |
+
lora: string
|
291 |
+
|
292 |
+
/**
|
293 |
+
* style name
|
294 |
+
*/
|
295 |
+
style: string
|
296 |
+
|
297 |
+
/**
|
298 |
+
* Music prompt
|
299 |
+
*/
|
300 |
+
music: string
|
301 |
+
|
302 |
+
/**
|
303 |
+
* Voice prompt
|
304 |
+
*/
|
305 |
+
voice: string
|
306 |
+
|
307 |
/**
|
308 |
* ID of the channel
|
309 |
*/
|
|
|
371 |
*/
|
372 |
tags: string[]
|
373 |
|
374 |
+
/**
|
375 |
+
* Model name
|
376 |
+
*/
|
377 |
+
model: VideoGenerationModel
|
378 |
+
|
379 |
+
/**
|
380 |
+
* LoRA name
|
381 |
+
*/
|
382 |
+
lora: string
|
383 |
+
|
384 |
+
/**
|
385 |
+
* style name
|
386 |
+
*/
|
387 |
+
style: string
|
388 |
+
|
389 |
+
/**
|
390 |
+
* Music prompt
|
391 |
+
*/
|
392 |
+
music: string
|
393 |
+
|
394 |
+
/**
|
395 |
+
* Voice prompt
|
396 |
+
*/
|
397 |
+
voice: string
|
398 |
+
|
399 |
/**
|
400 |
* The channel
|
401 |
*/
|
402 |
channel: ChannelInfo
|
403 |
}
|
404 |
|
405 |
+
export type VideoGenerationModel =
|
406 |
+
| "HotshotXL"
|
407 |
+
| "SVD"
|
408 |
+
| "LaVie"
|
409 |
+
|
410 |
export type InterfaceDisplayMode =
|
411 |
| "desktop"
|
412 |
| "tv"
|
|
|
440 |
export type ParsedDatasetReadme = {
|
441 |
license: string
|
442 |
pretty_name: string
|
443 |
+
model: VideoGenerationModel
|
444 |
lora: string
|
445 |
style: string
|
446 |
thumbnail: string
|
447 |
voice: string
|
448 |
+
music: string
|
449 |
tags: string[]
|
450 |
hf_tags: string[]
|
451 |
description: string
|
|
|
464 |
export type ParsedDatasetPrompt = {
|
465 |
title: string
|
466 |
description: string
|
|
|
467 |
prompt: string
|
468 |
+
tags: string[]
|
469 |
+
model: VideoGenerationModel
|
470 |
+
lora: string
|
471 |
+
style: string
|
472 |
thumbnail: string
|
473 |
+
voice: string
|
474 |
+
music: string
|
475 |
}
|
476 |
|
|
|
477 |
export type UpdateQueueRequest = {
|
478 |
channel?: ChannelInfo
|
479 |
apiKey: string
|
tailwind.config.js
CHANGED
@@ -64,6 +64,9 @@ module.exports = {
|
|
64 |
21: '5.25rem', // 84px
|
65 |
22: '5.5rem', // 88px
|
66 |
26: '6.5rem', // 104px
|
|
|
|
|
|
|
67 |
}
|
68 |
},
|
69 |
},
|
|
|
64 |
21: '5.25rem', // 84px
|
65 |
22: '5.5rem', // 88px
|
66 |
26: '6.5rem', // 104px
|
67 |
+
42: '10.5rem', // 168px
|
68 |
+
50: '12.5rem', // 200px
|
69 |
+
51: '12.75rem', // 204px
|
70 |
}
|
71 |
},
|
72 |
},
|