Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
Commit
•
761239a
1
Parent(s):
a6a67a3
add thumbnail fallback
Browse files- src/app/interface/top-header/index.tsx +1 -1
- src/app/interface/video-card/index.tsx +16 -1
- src/app/page.tsx +70 -1
- src/app/server/actions/ai-tube-hf/getChannel.ts +131 -0
- src/app/server/actions/ai-tube-hf/getChannels.ts +7 -64
- src/app/server/actions/utils/parseDatasetReadme.ts +9 -1
- src/app/watch/page.tsx +2 -4
- src/types.ts +10 -0
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 |
{/* 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 |
{/* 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=
|
|
|
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,
|
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 [
|
54 |
? chunks
|
55 |
: [name, name]
|
56 |
|
@@ -66,67 +66,10 @@ export async function getChannels(options: {
|
|
66 |
continue
|
67 |
}
|
68 |
|
69 |
-
const
|
70 |
-
|
71 |
-
|
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 |
-
|
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
|
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[]
|