jbilcke-hf HF staff commited on
Commit
761239a
1 Parent(s): a6a67a3

add thumbnail fallback

Browse files
src/app/interface/top-header/index.tsx CHANGED
@@ -109,7 +109,7 @@ export function TopHeader() {
109
  `px-4 py-2 w-max-64`,
110
  `text-neutral-400 text-sm italic`
111
  )}>
112
- All the videos are generated using AI for research purposes. Some models might produce factually incorrect or biased outputs.
113
  </div>
114
  <div className={cn()}>
115
  &nbsp; {/* more buttons? unused for now */}
 
109
  `px-4 py-2 w-max-64`,
110
  `text-neutral-400 text-sm italic`
111
  )}>
112
+ All the videos are generated using AI, for research purposes only. Some models might produce factually incorrect or biased outputs.
113
  </div>
114
  <div className={cn()}>
115
  &nbsp; {/* more buttons? unused for now */}
src/app/interface/video-card/index.tsx CHANGED
@@ -7,6 +7,8 @@ import { formatDuration } from "@/lib/formatDuration"
7
  import { formatTimeAgo } from "@/lib/formatTimeAgo"
8
  import Link from "next/link"
9
 
 
 
10
  export function VideoCard({
11
  video,
12
  className = "",
@@ -19,6 +21,8 @@ export function VideoCard({
19
  const ref = useRef<HTMLVideoElement>(null)
20
  const [duration, setDuration] = useState(0)
21
 
 
 
22
  const handlePointerEnter = () => {
23
  // ref.current?.load()
24
  ref.current?.play()
@@ -37,6 +41,16 @@ export function VideoCard({
37
  onSelect?.(video)
38
  }
39
 
 
 
 
 
 
 
 
 
 
 
40
  return (
41
  <Link href={`/watch?v=${video.id}`}>
42
  <div
@@ -92,7 +106,8 @@ export function VideoCard({
92
  <div className="flex flex-col">
93
  <div className="flex w-9 rounded-full overflow-hidden">
94
  <img
95
- src="huggingface-avatar.jpeg"
 
96
  />
97
  </div>
98
  </div>
 
7
  import { formatTimeAgo } from "@/lib/formatTimeAgo"
8
  import Link from "next/link"
9
 
10
+ const defaultChannelThumbnail = "/huggingface-avatar.jpeg"
11
+
12
  export function VideoCard({
13
  video,
14
  className = "",
 
21
  const ref = useRef<HTMLVideoElement>(null)
22
  const [duration, setDuration] = useState(0)
23
 
24
+ const [channelThumbnail, setChannelThumbnail] = useState(video.channel.thumbnail)
25
+
26
  const handlePointerEnter = () => {
27
  // ref.current?.load()
28
  ref.current?.play()
 
41
  onSelect?.(video)
42
  }
43
 
44
+ const handleBadChannelThumbnail = () => {
45
+ try {
46
+ if (channelThumbnail !== defaultChannelThumbnail) {
47
+ setChannelThumbnail(defaultChannelThumbnail)
48
+ }
49
+ } catch (err) {
50
+
51
+ }
52
+ }
53
+
54
  return (
55
  <Link href={`/watch?v=${video.id}`}>
56
  <div
 
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>
src/app/page.tsx CHANGED
@@ -3,12 +3,81 @@ import { AppQueryProps } from "@/types"
3
 
4
  import { Main } from "./main"
5
  import { getVideo } from "./server/actions/ai-tube-hf/getVideo"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  // we have routes but on Hugging Face we don't see them
8
  // so.. let's use the work around
9
  export default async function Page({ searchParams: { v: videoId } }: AppQueryProps) {
10
  const video = await getVideo({ videoId, neverThrow: true })
11
- // console.log("Root page: videoId ----> ", video?.id)
12
  return (
13
  <Main video={video} />
14
  )
 
3
 
4
  import { Main } from "./main"
5
  import { getVideo } from "./server/actions/ai-tube-hf/getVideo"
6
+ import { Metadata, ResolvingMetadata } from "next"
7
+
8
+
9
+ export async function generateMetadata(
10
+ { params, searchParams: { v: videoId } }: AppQueryProps,
11
+ parent: ResolvingMetadata
12
+ ): Promise<Metadata> {
13
+ // read route params
14
+
15
+ const metadataBase = new URL('https://huggingface.co/spaces/jbilcke-hf/ai-tube')
16
+
17
+ if (!videoId) {
18
+ return {
19
+ title: `🍿 AI Tube`,
20
+ metadataBase,
21
+ openGraph: {
22
+ type: "website",
23
+ // url: "https://example.com",
24
+ title: "AI Tube",
25
+ description: "The first fully AI generated video platform",
26
+ siteName: "🍿 AI Tube",
27
+
28
+ videos: [],
29
+ images: [],
30
+ },
31
+ }
32
+ }
33
+
34
+ try {
35
+ const video = await getVideo({ videoId, neverThrow: true })
36
+
37
+ if (!video) {
38
+ throw new Error("Video not found")
39
+ }
40
+
41
+ return {
42
+ title: `${video.label} - AI Tube`,
43
+ metadataBase,
44
+ openGraph: {
45
+ type: "website",
46
+ // url: "https://example.com",
47
+ title: video.label || "", // put the video title here
48
+ description: video.description || "", // put the vide description here
49
+ siteName: "AI Tube",
50
+
51
+ videos: [
52
+ {
53
+ "url": video.assetUrl
54
+ }
55
+ ],
56
+ // images: ['/some-specific-page-image.jpg', ...previousImages],
57
+ },
58
+ }
59
+ } catch (err) {
60
+ return {
61
+ title: "AI Tube - 404 Video Not Found",
62
+ metadataBase,
63
+ openGraph: {
64
+ type: "website",
65
+ // url: "https://example.com",
66
+ title: "AI Tube - 404 Not Found", // put the video title here
67
+ description: "", // put the vide description here
68
+ siteName: "AI Tube",
69
+
70
+ videos: [],
71
+ images: [],
72
+ },
73
+ }
74
+ }
75
+ }
76
 
77
  // we have routes but on Hugging Face we don't see them
78
  // so.. let's use the work around
79
  export default async function Page({ searchParams: { v: videoId } }: AppQueryProps) {
80
  const video = await getVideo({ videoId, neverThrow: true })
 
81
  return (
82
  <Main video={video} />
83
  )
src/app/server/actions/ai-tube-hf/getChannel.ts ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use server"
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
+
9
+ export async function getChannel(options: {
10
+ id: string
11
+ name: string
12
+ likes: number
13
+ updatedAt: Date
14
+ apiKey?: string
15
+ owner?: string
16
+ renewCache?: boolean
17
+ }): Promise<ChannelInfo> {
18
+ // console.log("getChannels")
19
+ let credentials: Credentials = adminCredentials
20
+ let owner = options.owner
21
+
22
+ if (options.apiKey) {
23
+ try {
24
+ credentials = { accessToken: options.apiKey }
25
+ const { name: username } = await whoAmI({ credentials })
26
+ if (!username) {
27
+ throw new Error(`couldn't get the username`)
28
+ }
29
+ // everything is in order,
30
+ owner = username
31
+ } catch (err) {
32
+ console.error(err)
33
+ throw err
34
+ }
35
+ }
36
+
37
+
38
+ const prefix = "ai-tube-"
39
+
40
+ const name = options.name
41
+
42
+ const chunks = name.split("/")
43
+ const [datasetUser, datasetName] = chunks.length === 2
44
+ ? chunks
45
+ : [name, name]
46
+
47
+ // console.log(`found a candidate dataset "${datasetName}" owned by @${datasetUser}`)
48
+
49
+ // ignore channels which don't start with ai-tube
50
+ if (!datasetName.startsWith(prefix)) {
51
+ throw new Error("this is not an AI Tube channel")
52
+ }
53
+
54
+ // ignore the video index
55
+ if (datasetName === "ai-tube-index") {
56
+ throw new Error("cannot get channel of ai-tube-index: time-space continuum broken!")
57
+ }
58
+
59
+ const slug = datasetName.replaceAll(prefix, "")
60
+
61
+ // console.log(`found an AI Tube channel: "${slug}"`)
62
+
63
+ // TODO parse the README to get the proper label
64
+ let label = slug.replaceAll("-", " ")
65
+
66
+ let model = ""
67
+ let lora = ""
68
+ let style = ""
69
+ let thumbnail = ""
70
+ let prompt = ""
71
+ let description = ""
72
+ let voice = ""
73
+ let tags: string[] = []
74
+
75
+ // console.log(`going to read datasets/${name}`)
76
+ try {
77
+ const response = await downloadFile({
78
+ repo: `datasets/${name}`,
79
+ path: "README.md",
80
+ credentials
81
+ })
82
+ const readme = await response?.text()
83
+
84
+ const parsedDatasetReadme = parseDatasetReadme(readme)
85
+
86
+ // console.log("parsedDatasetReadme: ", parsedDatasetReadme)
87
+
88
+ prompt = parsedDatasetReadme.prompt
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
+
97
+ thumbnail =
98
+ thumbnail.startsWith("http")
99
+ ? thumbnail
100
+ : (thumbnail.endsWith(".jpg") || thumbnail.endsWith(".jpeg"))
101
+ ? `https://huggingface.co/datasets/${name}/resolve/main/${thumbnail}`
102
+ : ""
103
+
104
+ tags = parsedDatasetReadme.tags
105
+ .map(tag => tag.trim()) // clean them up
106
+ .filter(tag => tag) // remove empty tags
107
+
108
+ } catch (err) {
109
+ // console.log("failed to read the readme:", err)
110
+ }
111
+
112
+ const channel: ChannelInfo = {
113
+ id: options.id,
114
+ datasetUser,
115
+ datasetName,
116
+ slug,
117
+ label,
118
+ description,
119
+ model,
120
+ lora,
121
+ style,
122
+ voice,
123
+ thumbnail,
124
+ prompt,
125
+ likes: options.likes,
126
+ tags,
127
+ updatedAt: options.updatedAt.toISOString()
128
+ }
129
+
130
+ return channel
131
+ }
src/app/server/actions/ai-tube-hf/getChannels.ts CHANGED
@@ -1,10 +1,10 @@
1
  "use server"
2
 
3
- import { Credentials, downloadFile, listDatasets, 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
 
9
  export async function getChannels(options: {
10
  apiKey?: string
@@ -50,7 +50,7 @@ export async function getChannels(options: {
50
  // TODO: need to handle better cases where the username is missing
51
 
52
  const chunks = name.split("/")
53
- const [datasetUser, datasetName] = chunks.length === 2
54
  ? chunks
55
  : [name, name]
56
 
@@ -66,67 +66,10 @@ export async function getChannels(options: {
66
  continue
67
  }
68
 
69
- const slug = datasetName.replaceAll(prefix, "")
70
-
71
- // console.log(`found an AI Tube channel: "${slug}"`)
72
-
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}`)
83
- try {
84
- const response = await downloadFile({
85
- repo: `datasets/${name}`,
86
- path: "README.md",
87
- credentials
88
- })
89
- const readme = await response?.text()
90
-
91
- const parsedDatasetReadme = parseDatasetReadme(readme)
92
-
93
- // console.log("parsedDatasetReadme: ", parsedDatasetReadme)
94
-
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
- }
116
-
117
- const channel: ChannelInfo = {
118
- id,
119
- datasetUser,
120
- datasetName,
121
- slug,
122
- label,
123
- description,
124
- thumbnail,
125
- prompt,
126
- likes,
127
- tags,
128
- updatedAt: updatedAt.toISOString()
129
- }
130
 
131
  channels.push(channel)
132
  }
 
1
  "use server"
2
 
3
+ import { Credentials, listDatasets, whoAmI } from "@/huggingface/hub/src"
 
4
  import { ChannelInfo } from "@/types"
5
 
6
  import { adminCredentials } from "../config"
7
+ import { getChannel } from "./getChannel"
8
 
9
  export async function getChannels(options: {
10
  apiKey?: string
 
50
  // TODO: need to handle better cases where the username is missing
51
 
52
  const chunks = name.split("/")
53
+ const [_datasetUser, datasetName] = chunks.length === 2
54
  ? chunks
55
  : [name, name]
56
 
 
66
  continue
67
  }
68
 
69
+ const channel = await getChannel({
70
+ ...options,
71
+ id, name, likes, updatedAt
72
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
 
74
  channels.push(channel)
75
  }
src/app/server/actions/utils/parseDatasetReadme.ts CHANGED
@@ -11,7 +11,7 @@ export function parseDatasetReadme(markdown: string = ""): ParsedDatasetReadme {
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 : "",
@@ -19,6 +19,8 @@ export function parseDatasetReadme(markdown: string = ""): ParsedDatasetReadme {
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,
@@ -31,6 +33,8 @@ export function parseDatasetReadme(markdown: string = ""): ParsedDatasetReadme {
31
  hf_tags: [], // Hugging Face tags
32
  tags: [],
33
  model: "",
 
 
34
  thumbnail: "",
35
  voice: "",
36
  description: "",
@@ -46,6 +50,8 @@ export function parseDatasetReadme(markdown: string = ""): ParsedDatasetReadme {
46
  */
47
  function parseMarkdown(markdown: string): {
48
  model: string
 
 
49
  thumbnail: string
50
  voice: string
51
  description: string
@@ -67,6 +73,8 @@ function parseMarkdown(markdown: string): {
67
  return {
68
  description: sections["description"] || "",
69
  model: sections["model"] || "",
 
 
70
  thumbnail: sections["thumbnail"] || "",
71
  voice: sections["voice"] || "",
72
  prompt: sections["prompt"] || "",
 
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 : "",
 
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,
 
33
  hf_tags: [], // Hugging Face tags
34
  tags: [],
35
  model: "",
36
+ lora: "",
37
+ style: "",
38
  thumbnail: "",
39
  voice: "",
40
  description: "",
 
50
  */
51
  function parseMarkdown(markdown: string): {
52
  model: string
53
+ lora: string
54
+ style: string
55
  thumbnail: string
56
  voice: string
57
  description: string
 
73
  return {
74
  description: sections["description"] || "",
75
  model: sections["model"] || "",
76
+ lora: sections["lora"] || "",
77
+ style: sections["style"] || "",
78
  thumbnail: sections["thumbnail"] || "",
79
  voice: sections["voice"] || "",
80
  prompt: sections["prompt"] || "",
src/app/watch/page.tsx CHANGED
@@ -1,6 +1,4 @@
1
- import { useEffect, useState, useTransition } from "react"
2
- import Head from "next/head"
3
- import Script from "next/script"
4
  import { Metadata, ResolvingMetadata } from "next"
5
 
6
  import { AppQueryProps } from "@/types"
@@ -27,7 +25,7 @@ export async function generateMetadata(
27
 
28
  return {
29
  title: `${video.label} - AI Tube`,
30
- metadataBase: new URL('https://huggingface.co/spaces/jbilcke-hf/ai-tube'),
31
  openGraph: {
32
  type: "website",
33
  // url: "https://example.com",
 
1
+
 
 
2
  import { Metadata, ResolvingMetadata } from "next"
3
 
4
  import { AppQueryProps } from "@/types"
 
25
 
26
  return {
27
  title: `${video.label} - AI Tube`,
28
+ metadataBase,
29
  openGraph: {
30
  type: "website",
31
  // url: "https://example.com",
src/types.ts CHANGED
@@ -211,6 +211,14 @@ export type ChannelInfo = {
211
 
212
  thumbnail: string
213
 
 
 
 
 
 
 
 
 
214
  /**
215
  * The system prompt
216
  */
@@ -376,6 +384,8 @@ export type ParsedDatasetReadme = {
376
  license: string
377
  pretty_name: string
378
  model: string
 
 
379
  thumbnail: string
380
  voice: string
381
  tags: string[]
 
211
 
212
  thumbnail: string
213
 
214
+ model: string
215
+
216
+ lora: string
217
+
218
+ style: string
219
+
220
+ voice: string
221
+
222
  /**
223
  * The system prompt
224
  */
 
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[]