jbilcke-hf HF staff commited on
Commit
4c34e70
1 Parent(s): 3a944ef

playing with stable video diffusion

Browse files
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 aspect-video items-center justify-center`,
72
  `rounded-xl overflow-hidden`,
 
73
  )}
74
  >
75
  <video
76
  ref={ref}
77
  src={video.assetUrl}
78
- className="w-full"
 
 
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 space-x-4`,
 
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="flex flex-col flex-grow">
115
- <h3 className="text-zinc-100 text-base font-medium mb-0 line-clamp-2">{video.label}</h3>
 
 
 
 
 
 
116
  <div className={cn(
117
  `flex flex-row items-center`,
118
- `text-neutral-400 text-sm font-normal space-x-1`,
 
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 text-sm font-normal`,
 
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 = "flex",
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" | "flex"
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, // read them from the file?
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
- tag = "",
 
12
  sortBy = "date",
 
13
  maxVideos = HARD_LIMIT,
14
  }: {
15
- tag?: string
16
- sortBy?: "random" | "date",
 
 
 
 
 
 
 
 
 
 
 
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 videos = Object.values(published)
28
 
29
- // filter videos by tag, or else we return everything
30
- const requestedTag = tag.toLowerCase().trim()
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
- videos.sort(((a, b) => a.updatedAt.localeCompare(b.updatedAt)))
40
  } else {
41
- videos.sort(() => Math.random() - 0.5)
42
  }
43
 
44
- // we enforce a max limit of HARD_LIMIT (eg. 100)
45
- return videos.slice(0, Math.min(HARD_LIMIT, maxVideos))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 = ""): ParsedDatasetPrompt {
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: tags && typeof tags === "string" ? tags.split("-").map(x => x.trim()).filter(x => x) : [],
 
 
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.trim()
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
- tag: currentTag,
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-96`,
185
  `hidden sm:flex flex-col`,
186
- `px-4`
187
  )}>
188
- {/*[ TO BE CONTINUED ]*/}
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: string
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: string
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
  },