jbilcke-hf HF staff commited on
Commit
93f8352
1 Parent(s): 3942037

small beginnings..

Browse files
src/app/interface/channel-card/index.tsx CHANGED
@@ -16,9 +16,10 @@ export function ChannelCard({
16
  className={cn(
17
  `flex flex-col`,
18
  `items-center justify-center`,
 
19
  `w-52 h-52`,
20
  `rounded-lg`,
21
- `bg-neutral-900 hover:bg-neutral-700/80`,
22
  `text-neutral-100/80 hover:text-neutral-100/100`,
23
  `cursor-pointer`,
24
  className,
@@ -31,31 +32,33 @@ export function ChannelCard({
31
  >
32
  <div
33
  className={cn(
34
- `rounded-lg overflow-hidden`
 
 
35
  )}
36
  >
37
- <img src="" />
38
  </div>
39
 
40
  <div className={cn(
41
  `flex flex-col`,
42
  `items-center justify-center text-center`,
43
- `space-y-2`
44
  )}>
45
- <div className="text-center text-lg">{channel.label}</div>
46
  {/*<div className="text-center text-sm font-semibold">
47
  by <a href={
48
  `https://huggingface.co/${channel.datasetUser}`
49
  } target="_blank">@{channel.datasetUser}</a>
50
  </div>
51
  */}
52
- <div className="text-center text-sm font-semibold">
53
  @{channel.datasetUser}
54
  </div>
55
- <div className="flex flex-row items-center justify-center">
56
- <div className="text-center text-sm">{0} videos</div>
57
  <div className="px-1">-</div>
58
- <div className="text-center text-sm">{channel.likes} likes</div>
59
  </div>
60
  </div>
61
  </div>
 
16
  className={cn(
17
  `flex flex-col`,
18
  `items-center justify-center`,
19
+ `space-y-1`,
20
  `w-52 h-52`,
21
  `rounded-lg`,
22
+ `hover:bg-neutral-800/30`,
23
  `text-neutral-100/80 hover:text-neutral-100/100`,
24
  `cursor-pointer`,
25
  className,
 
32
  >
33
  <div
34
  className={cn(
35
+ `flex flex-col items-center justify-center`,
36
+ `rounded-full overflow-hidden`,
37
+ `w-26 h-26`
38
  )}
39
  >
40
+ <img src={channel.thumbnail} />
41
  </div>
42
 
43
  <div className={cn(
44
  `flex flex-col`,
45
  `items-center justify-center text-center`,
46
+ `space-y-1`
47
  )}>
48
+ <div className="text-center text-base font-medium text-zinc-100">{channel.label}</div>
49
  {/*<div className="text-center text-sm font-semibold">
50
  by <a href={
51
  `https://huggingface.co/${channel.datasetUser}`
52
  } target="_blank">@{channel.datasetUser}</a>
53
  </div>
54
  */}
55
+ <div className="text-center text-xs font-medium">
56
  @{channel.datasetUser}
57
  </div>
58
+ <div className="flex flex-row items-center justify-center text-neutral-400">
59
+ <div className="text-center text-xs">{0} videos</div>
60
  <div className="px-1">-</div>
61
+ <div className="text-center text-xs">{channel.likes} likes</div>
62
  </div>
63
  </div>
64
  </div>
src/app/interface/channel-list/index.tsx CHANGED
@@ -29,7 +29,7 @@ export function ChannelList({
29
  <div
30
  className={cn(
31
  layout === "grid"
32
- ? `grid grid-cols-4 gap-4`
33
  : `flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4`,
34
  className,
35
  )}
 
29
  <div
30
  className={cn(
31
  layout === "grid"
32
+ ? `grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4`
33
  : `flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4`,
34
  className,
35
  )}
src/app/interface/left-menu/index.tsx CHANGED
@@ -33,13 +33,13 @@ export function LeftMenu() {
33
  >
34
  Discover
35
  </MenuItem>
36
- {showBetaFeatures && <MenuItem
37
  icon={<GrChannel className="h-5 w-5" />}
38
  selected={view === "public_channels"}
39
  onClick={() => setView("public_channels")}
40
  >
41
  Channels
42
- </MenuItem>}
43
  </div>
44
  <div className={cn(
45
  `flex flex-col w-full`,
 
33
  >
34
  Discover
35
  </MenuItem>
36
+ <MenuItem
37
  icon={<GrChannel className="h-5 w-5" />}
38
  selected={view === "public_channels"}
39
  onClick={() => setView("public_channels")}
40
  >
41
  Channels
42
+ </MenuItem>
43
  </div>
44
  <div className={cn(
45
  `flex flex-col w-full`,
src/app/interface/top-header/index.tsx CHANGED
@@ -86,11 +86,7 @@ export function TopHeader() {
86
  </div>
87
  </div>
88
  <div className="font-semibold">
89
- {view === "user_channels"
90
- ? "My account"
91
- : view === "public_channels"
92
- ? "AI Channels"
93
- : "AiTube" }
94
  </div>
95
  </div>
96
  </div>
@@ -98,8 +94,9 @@ export function TopHeader() {
98
  `transition-all duration-200 ease-in-out`,
99
  `flex flex-col items-center justify-center`,
100
  `px-4 py-2 w-max-64`,
 
101
  )}>
102
- {/*[ Search bar goes here ]*/}
103
  </div>
104
  <div className={cn()}>
105
  &nbsp; {/* more buttons? unused for now */}
 
86
  </div>
87
  </div>
88
  <div className="font-semibold">
89
+ AiTube
 
 
 
 
90
  </div>
91
  </div>
92
  </div>
 
94
  `transition-all duration-200 ease-in-out`,
95
  `flex flex-col items-center justify-center`,
96
  `px-4 py-2 w-max-64`,
97
+ `text-neutral-400 text-sm italic`
98
  )}>
99
+ Ai Tube is a platform where all videos are generated using AI, for research and experimentation purposes.
100
  </div>
101
  <div className={cn()}>
102
  &nbsp; {/* more buttons? unused for now */}
src/app/server/actions/ai-tube-hf/getChannels.ts CHANGED
@@ -11,7 +11,7 @@ export async function getChannels(options: {
11
  owner?: string
12
  renewCache?: boolean
13
  } = {}): Promise<ChannelInfo[]> {
14
-
15
  let credentials: Credentials = adminCredentials
16
  let owner = options?.owner
17
 
@@ -38,13 +38,12 @@ export async function getChannels(options: {
38
  ? { owner } // search channels of a specific user
39
  : prefix // global search (note: might be costly?)
40
 
41
- console.log("search:", search)
42
-
43
  for await (const { id, name, likes, updatedAt } of listDatasets({
44
  search,
45
  credentials,
46
  requestInit: options?.renewCache
47
- ? { cache: "no-cache" }
48
  : undefined
49
  })) {
50
 
@@ -56,7 +55,8 @@ export async function getChannels(options: {
56
  : [name, name]
57
 
58
  // console.log(`found a candidate dataset "${datasetName}" owned by @${datasetUser}`)
59
-
 
60
  if (!datasetName.startsWith(prefix)) {
61
  continue
62
  }
@@ -73,9 +73,10 @@ export async function getChannels(options: {
73
  // TODO parse the README to get the proper label
74
  let label = slug.replaceAll("-", " ")
75
 
76
- const thumbnail = ""
77
  let prompt = ""
78
  let description = ""
 
79
  let tags: string[] = []
80
 
81
  // console.log(`going to read datasets/${name}`)
@@ -94,15 +95,21 @@ export async function getChannels(options: {
94
  prompt = parsedDatasetReadme.prompt
95
  label = parsedDatasetReadme.pretty_name
96
  description = parsedDatasetReadme.description
 
 
 
 
 
 
 
 
97
 
98
- const prefix = "ai-tube:"
99
 
100
  tags = parsedDatasetReadme.tags
101
- .filter(tag => tag.startsWith(prefix)) // remove any tag not belonging to us
102
- .map(tag => tag.replaceAll(prefix, "").trim()) // remove the prefix
103
  .filter(tag => tag) // remove empty tags
104
 
105
-
106
  } catch (err) {
107
  // console.log("failed to read the readme:", err)
108
  }
 
11
  owner?: string
12
  renewCache?: boolean
13
  } = {}): Promise<ChannelInfo[]> {
14
+ // console.log("getChannels")
15
  let credentials: Credentials = adminCredentials
16
  let owner = options?.owner
17
 
 
38
  ? { owner } // search channels of a specific user
39
  : prefix // global search (note: might be costly?)
40
 
41
+
 
42
  for await (const { id, name, likes, updatedAt } of listDatasets({
43
  search,
44
  credentials,
45
  requestInit: options?.renewCache
46
+ ? { cache: "no-store" }
47
  : undefined
48
  })) {
49
 
 
55
  : [name, name]
56
 
57
  // console.log(`found a candidate dataset "${datasetName}" owned by @${datasetUser}`)
58
+
59
+ // ignore channels which don't start with ai-tube
60
  if (!datasetName.startsWith(prefix)) {
61
  continue
62
  }
 
73
  // TODO parse the README to get the proper label
74
  let label = slug.replaceAll("-", " ")
75
 
76
+ let thumbnail = ""
77
  let prompt = ""
78
  let description = ""
79
+ let voice = ""
80
  let tags: string[] = []
81
 
82
  // console.log(`going to read datasets/${name}`)
 
95
  prompt = parsedDatasetReadme.prompt
96
  label = parsedDatasetReadme.pretty_name
97
  description = parsedDatasetReadme.description
98
+ thumbnail = parsedDatasetReadme.thumbnail || "thumbnail.jpg"
99
+
100
+ thumbnail =
101
+ thumbnail.startsWith("http")
102
+ ? thumbnail
103
+ : (thumbnail.endsWith(".jpg") || thumbnail.endsWith(".jpeg"))
104
+ ? `https://huggingface.co/datasets/${name}/resolve/main/${thumbnail}`
105
+ : ""
106
 
107
+ voice = parsedDatasetReadme.voice
108
 
109
  tags = parsedDatasetReadme.tags
110
+ .map(tag => tag.trim()) // clean them up
 
111
  .filter(tag => tag) // remove empty tags
112
 
 
113
  } catch (err) {
114
  // console.log("failed to read the readme:", err)
115
  }
src/app/server/actions/ai-tube-hf/getVideoRequestsFromChannel.ts CHANGED
@@ -71,22 +71,28 @@ export async function getVideoRequestsFromChannel({
71
  continue
72
  }
73
 
74
- const { title, description, tags, prompt } = parseDatasetPrompt(rawMarkdown)
75
 
76
  if (!title || !description || !prompt) {
77
  // console.log("dataset prompt is incomplete or unparseable")
78
  continue
79
  }
80
  // console.log("prompt parsed markdown:", { title, description, tags })
 
 
 
 
 
 
81
 
82
  const video: VideoRequest = {
83
  id,
84
  label: title,
85
  description,
86
  prompt,
87
- thumbnailUrl: "",
88
 
89
- updatedAt: file.lastCommit?.date || "",
90
  tags, // read them from the file?
91
  channel,
92
  }
 
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")
78
  continue
79
  }
80
  // console.log("prompt parsed markdown:", { title, description, tags })
81
+ let thumbnailUrl =
82
+ thumbnail.startsWith("http")
83
+ ? thumbnail
84
+ : (thumbnail.endsWith(".jpg") || thumbnail.endsWith(".jpeg"))
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
  }
src/app/server/actions/ai-tube-hf/uploadVideoRequestToDataset.ts CHANGED
@@ -75,7 +75,7 @@ ${prompt}
75
  label: title,
76
  description,
77
  prompt,
78
- thumbnailUrl: "",
79
  updatedAt: new Date().toISOString(),
80
  tags,
81
  channel,
@@ -87,7 +87,7 @@ ${prompt}
87
  label: title,
88
  description,
89
  prompt,
90
- thumbnailUrl: "", // will be generated in async
91
  assetUrl: "", // will be generated in async
92
  numberOfViews: 0,
93
  numberOfLikes: 0,
 
75
  label: title,
76
  description,
77
  prompt,
78
+ thumbnailUrl: channel.thumbnail,
79
  updatedAt: new Date().toISOString(),
80
  tags,
81
  channel,
 
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,
93
  numberOfLikes: 0,
src/app/server/actions/utils/parseDatasetPrompt.ts CHANGED
@@ -3,13 +3,14 @@ import { ParsedDatasetPrompt } from "@/types"
3
 
4
  export function parseDatasetPrompt(markdown: string = ""): ParsedDatasetPrompt {
5
  try {
6
- const { title, description, tags, prompt } = 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
  }
14
  } catch (err) {
15
  return {
@@ -17,6 +18,7 @@ export function parseDatasetPrompt(markdown: string = ""): ParsedDatasetPrompt {
17
  description: "",
18
  tags: [],
19
  prompt: "",
 
20
  }
21
  }
22
  }
@@ -31,26 +33,26 @@ function parseMarkdown(markdown: string): {
31
  description: string
32
  tags: string
33
  prompt: string
 
34
  } {
35
- // Regular expression to find markdown sections based on the provided structure
36
- const sectionRegex = /^#+ (.+?)\n+([\s\S]+?)(?=\n+? |$)/gm;
 
 
37
 
38
- let match;
39
  const sections: { [key: string]: string } = {};
40
 
41
- // Iterate over each section match to populate the sections object
42
  while ((match = sectionRegex.exec(markdown))) {
43
- const [, key, value] = match;
44
- sections[key.toLowerCase()] = value.trim();
45
  }
46
 
47
- // Create the resulting JSON object with "description" and "prompt" keys
48
- const result = {
49
- title: sections['title'] || '',
50
- description: sections['description'] || '',
51
- tags: sections['tags'] || '',
52
- prompt: sections['prompt'] || '',
53
  };
54
-
55
- return result;
56
  }
 
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 {
 
18
  description: "",
19
  tags: [],
20
  prompt: "",
21
+ thumbnail: "",
22
  }
23
  }
24
  }
 
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;
42
 
 
43
  const sections: { [key: string]: string } = {};
44
 
45
+ let match;
46
  while ((match = sectionRegex.exec(markdown))) {
47
+ const { key, content } = match.groups as { key: string; content: string };
48
+ sections[key.trim().toLowerCase()] = content.trim();
49
  }
50
 
51
+ return {
52
+ title: sections["title"] || "",
53
+ description: sections["description"] || "",
54
+ tags: sections["tags"] || "",
55
+ prompt: sections["prompt"] || "",
56
+ thumbnail: sections["thumbnail"] || "",
57
  };
 
 
58
  }
src/app/server/actions/utils/parseDatasetReadme.ts CHANGED
@@ -5,14 +5,22 @@ import { ParsedDatasetReadme, ParsedMetadataAndContent } from "@/types"
5
 
6
  export function parseDatasetReadme(markdown: string = ""): ParsedDatasetReadme {
7
  try {
 
 
8
  const { metadata, content } = metadataParser(markdown) as ParsedMetadataAndContent
9
 
10
- const { description, prompt } = parseMarkdown(content)
 
 
11
 
12
  return {
13
  license: typeof metadata?.license === "string" ? metadata.license : "",
14
  pretty_name: typeof metadata?.pretty_name === "string" ? metadata.pretty_name : "",
15
- tags: Array.isArray(metadata?.tags) ? metadata.tags : [],
 
 
 
 
16
  description,
17
  prompt,
18
  }
@@ -20,7 +28,11 @@ export function parseDatasetReadme(markdown: string = ""): ParsedDatasetReadme {
20
  return {
21
  license: "",
22
  pretty_name: "",
23
- tags: [], // Hugging Face tags
 
 
 
 
24
  description: "",
25
  prompt: "",
26
  }
@@ -33,28 +45,31 @@ export function parseDatasetReadme(markdown: string = ""): ParsedDatasetReadme {
33
  * @returns A JSON object with { "description": "...", "prompt": "..." }
34
  */
35
  function parseMarkdown(markdown: string): {
 
 
 
36
  description: string
37
  prompt: string
38
- // categories: string
39
  } {
40
- // Regular expression to find markdown sections based on the provided structure
41
- const sectionRegex = /^## (.+?)\n\n([\s\S]+?)(?=\n## |$)/gm;
 
42
 
43
- let match;
44
  const sections: { [key: string]: string } = {};
45
 
46
- // Iterate over each section match to populate the sections object
47
  while ((match = sectionRegex.exec(markdown))) {
48
- const [, key, value] = match;
49
- sections[key.toLowerCase()] = value.trim();
50
  }
51
 
52
- // Create the resulting JSON object with "description" and "prompt" keys
53
- const result = {
54
- description: sections['description'] || '',
55
- // categories: sections['categories'] || '',
56
- prompt: sections['prompt'] || '',
 
 
57
  };
58
-
59
- return result;
60
  }
 
5
 
6
  export function parseDatasetReadme(markdown: string = ""): ParsedDatasetReadme {
7
  try {
8
+ markdown = markdown.trim()
9
+
10
  const { metadata, content } = metadataParser(markdown) as ParsedMetadataAndContent
11
 
12
+ // console.log("DEBUG README:", { metadata, content })
13
+
14
+ const { model, 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
+ thumbnail,
23
+ voice,
24
  description,
25
  prompt,
26
  }
 
28
  return {
29
  license: "",
30
  pretty_name: "",
31
+ hf_tags: [], // Hugging Face tags
32
+ tags: [],
33
+ model: "",
34
+ thumbnail: "",
35
+ voice: "",
36
  description: "",
37
  prompt: "",
38
  }
 
45
  * @returns A JSON object with { "description": "...", "prompt": "..." }
46
  */
47
  function parseMarkdown(markdown: string): {
48
+ model: string
49
+ thumbnail: string
50
+ voice: string
51
  description: string
52
  prompt: string
53
+ tags: string
54
  } {
55
+ // console.log("markdown:", markdown)
56
+ // Improved regular expression to find markdown sections and accommodate multi-line content.
57
+ const sectionRegex = /^#+\s+(?<key>.+?)\n\n?(?<content>[^#]+)/gm;
58
 
 
59
  const sections: { [key: string]: string } = {};
60
 
61
+ let match;
62
  while ((match = sectionRegex.exec(markdown))) {
63
+ const { key, content } = match.groups as { key: string; content: string };
64
+ sections[key.trim().toLowerCase()] = content.trim();
65
  }
66
 
67
+ return {
68
+ description: sections["description"] || "",
69
+ model: sections["model"] || "",
70
+ thumbnail: sections["thumbnail"] || "",
71
+ voice: sections["voice"] || "",
72
+ prompt: sections["prompt"] || "",
73
+ tags: sections["tags"] || "",
74
  };
 
 
75
  }
src/app/views/public-channels-view/index.tsx CHANGED
@@ -33,6 +33,7 @@ export function PublicChannelsView() {
33
  return (
34
  <div className={cn(`flex flex-col`)}>
35
  <ChannelList
 
36
  channels={currentChannels}
37
  />
38
  </div>
 
33
  return (
34
  <div className={cn(`flex flex-col`)}>
35
  <ChannelList
36
+ layout="grid"
37
  channels={currentChannels}
38
  />
39
  </div>
src/app/views/user-channels-view/index.tsx CHANGED
@@ -69,6 +69,7 @@ export function UserChannelsView() {
69
  <div className="flex flex-col space-y-4">
70
  <h2 className="text-3xl font-bold">Your custom channels:</h2>
71
  {currentChannels?.length ? <ChannelList
 
72
  channels={currentChannels}
73
  onSelect={(channel) => {
74
  setCurrentChannel(channel)
 
69
  <div className="flex flex-col space-y-4">
70
  <h2 className="text-3xl font-bold">Your custom channels:</h2>
71
  {currentChannels?.length ? <ChannelList
72
+ layout="grid"
73
  channels={currentChannels}
74
  onSelect={(channel) => {
75
  setCurrentChannel(channel)
src/types.ts CHANGED
@@ -374,7 +374,11 @@ export type Settings = {
374
  export type ParsedDatasetReadme = {
375
  license: string
376
  pretty_name: string
 
 
 
377
  tags: string[]
 
378
  description: string
379
  prompt: string
380
  }
@@ -393,6 +397,7 @@ export type ParsedDatasetPrompt = {
393
  description: string
394
  tags: string[]
395
  prompt: string
 
396
  }
397
 
398
 
 
374
  export type ParsedDatasetReadme = {
375
  license: string
376
  pretty_name: string
377
+ model: string
378
+ thumbnail: string
379
+ voice: string
380
  tags: string[]
381
+ hf_tags: string[]
382
  description: string
383
  prompt: string
384
  }
 
397
  description: string
398
  tags: string[]
399
  prompt: string
400
+ thumbnail: string
401
  }
402
 
403
 
tailwind.config.js CHANGED
@@ -53,6 +53,8 @@ module.exports = {
53
  20: '5rem', // 80px
54
  21: '5.25rem', // 84px
55
  22: '5.5rem', // 88px
 
 
56
  },
57
  width: {
58
  17: '4.25rem', // 68px
@@ -61,6 +63,7 @@ module.exports = {
61
  20: '5rem', // 80px
62
  21: '5.25rem', // 84px
63
  22: '5.5rem', // 88px
 
64
  }
65
  },
66
  },
 
53
  20: '5rem', // 80px
54
  21: '5.25rem', // 84px
55
  22: '5.5rem', // 88px
56
+ 22: '5.5rem', // 88px
57
+ 26: '6.5rem', // 104px
58
  },
59
  width: {
60
  17: '4.25rem', // 68px
 
63
  20: '5rem', // 80px
64
  21: '5.25rem', // 84px
65
  22: '5.5rem', // 88px
66
+ 26: '6.5rem', // 104px
67
  }
68
  },
69
  },