Spaces:
Sleeping
Sleeping
preparing transition to user-generated content + gaussians
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .env +2 -0
- README.md +7 -7
- package-lock.json +0 -0
- package.json +36 -29
- src/app/api/login/route.ts +7 -7
- src/app/api/media/[mediaId]/route.ts +76 -0
- src/app/api/video/[videoId]/route.ts +7 -6
- src/app/embed/page.tsx +20 -13
- src/app/interface/about/index.tsx +3 -3
- src/app/interface/collection-card/index.tsx +1 -0
- src/app/interface/equirectangular-video-player/index.tsx +2 -2
- src/app/interface/equirectangular-video-player/viewer.tsx +17 -9
- src/app/interface/gsplat/index.tsx +120 -0
- src/app/interface/left-menu/index.tsx +28 -2
- src/app/interface/like-button/index.tsx +13 -13
- src/app/interface/media-list/index.tsx +3 -3
- src/app/interface/{video-player → media-player}/cartesian.tsx +24 -9
- src/app/interface/{video-player → media-player}/equirectangular.tsx +22 -15
- src/app/interface/media-player/gaussian.tsx +32 -0
- src/app/interface/{video-player → media-player}/index.tsx +28 -14
- src/app/interface/pending-video-card/index.tsx +3 -3
- src/app/interface/pending-video-list/index.tsx +3 -3
- src/app/interface/playlist-control/index.tsx +1 -1
- src/app/interface/recommended-videos/index.tsx +7 -8
- src/app/interface/search-input/index.tsx +1 -1
- src/app/interface/track-card/index.tsx +4 -3
- src/app/interface/video-card/index.tsx +5 -4
- src/app/layout.tsx +2 -2
- src/app/main.tsx +6 -6
- src/app/page.tsx +8 -8
- src/app/server/actions/ai-tube-hf/deleteVideoRequest.ts +2 -2
- src/app/server/actions/ai-tube-hf/downloadClapProject.ts +3 -3
- src/app/server/actions/ai-tube-hf/extendVideosWithStats.ts +4 -4
- src/app/server/actions/ai-tube-hf/getChannelVideos.ts +3 -3
- src/app/server/actions/ai-tube-hf/getVideo.ts +4 -4
- src/app/server/actions/ai-tube-hf/getVideoIndex.ts +3 -3
- src/app/server/actions/ai-tube-hf/getVideos.ts +3 -3
- src/app/server/actions/ai-tube-hf/parseChannel.ts +2 -2
- src/app/server/actions/ai-tube-hf/uploadVideoRequestToDataset.ts +5 -5
- src/app/server/actions/stats.ts +26 -26
- src/app/server/actions/submitVideoRequest.ts +2 -2
- src/app/server/actions/utils/computeOrientationProjectionWidthHeight.ts +2 -2
- src/app/server/actions/utils/isAntisocial.ts +2 -2
- src/app/server/actions/utils/isHighQuality.ts +2 -2
- src/app/server/actions/utils/parseProjectionFromLoRA.ts +2 -2
- src/app/state/categories.ts +1 -1
- src/app/state/useCurrentUser.ts +2 -2
- src/app/state/useStore.ts +35 -35
- src/app/views/home-view/index.tsx +2 -2
- src/app/views/public-music-videos-view/index.tsx +2 -2
.env
CHANGED
@@ -1,4 +1,6 @@
|
|
1 |
|
|
|
|
|
2 |
NEXT_PUBLIC_SHOW_BETA_FEATURES="false"
|
3 |
|
4 |
NEXT_PUBLIC_DEVELOPER_MODE="false"
|
|
|
1 |
|
2 |
+
NEXT_PUBLIC_DOMAIN="https://aitube.at"
|
3 |
+
|
4 |
NEXT_PUBLIC_SHOW_BETA_FEATURES="false"
|
5 |
|
6 |
NEXT_PUBLIC_DEVELOPER_MODE="false"
|
README.md
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
emoji: 🍿
|
4 |
colorFrom: red
|
5 |
colorTo: red
|
@@ -9,7 +9,7 @@ app_port: 3000
|
|
9 |
disable_embedding: false
|
10 |
---
|
11 |
|
12 |
-
# 🍿
|
13 |
|
14 |
## FAQ
|
15 |
|
@@ -19,7 +19,7 @@ There will be a UI to edit them in the future
|
|
19 |
|
20 |
### How often videos are generated?
|
21 |
|
22 |
-
There is a script (called the
|
23 |
|
24 |
However, once it starts generating a video, the bot will be kept busy for an hour or so (sometimes more),
|
25 |
and during this time the other videos will wait patiently.
|
@@ -27,7 +27,7 @@ and during this time the other videos will wait patiently.
|
|
27 |
### My video failed to generate! Is it lost?
|
28 |
|
29 |
That's the beauty of the dataset system: as long as you keep your video in your dataset,
|
30 |
-
and it is not published yet (is not visible on the
|
31 |
|
32 |
So.. you normally have nothing to do (unless your video config or channel config is really damaged or invalid).
|
33 |
|
@@ -42,7 +42,7 @@ This delay will be reduced in the future.
|
|
42 |
|
43 |
### Videos are taking too long to generate
|
44 |
|
45 |
-
|
46 |
|
47 |
It's the whole concept: to generate multi-minutes videos, with lot of stuff like audio, speech etc
|
48 |
(if you are only interested in generate a 2 to 4 sec silent video, I suggest you use ComfyUI, Automatic1111 stable-diffusion-webui, or RunwayML or Pika Labs if your prefer commercial services).
|
@@ -70,9 +70,9 @@ This is all new technology based on research tools, so sometimes they can crash,
|
|
70 |
|
71 |
This is a bug, it will be fixed in the future but I haven't had the opportunity to take a look yet (the cause is that I don't generate the video based on audio length yet).
|
72 |
|
73 |
-
### Can I clone
|
74 |
|
75 |
-
|
76 |
Maybe one day there will be an offline version (similar to how my latent browser project worked), but for the moment the focus is on developing it as a community rather than a tool that can be cloned, rebranded, wrapped into ads by someone else etc.
|
77 |
|
78 |
### My video has been generated, but I don't see it anymore
|
|
|
1 |
---
|
2 |
+
title: AiTube
|
3 |
emoji: 🍿
|
4 |
colorFrom: red
|
5 |
colorTo: red
|
|
|
9 |
disable_embedding: false
|
10 |
---
|
11 |
|
12 |
+
# 🍿 AiTube
|
13 |
|
14 |
## FAQ
|
15 |
|
|
|
19 |
|
20 |
### How often videos are generated?
|
21 |
|
22 |
+
There is a script (called the AiTube Robot - code is also available, see my profile) which checks the Hugging Face platform every 5 minutes for new content.
|
23 |
|
24 |
However, once it starts generating a video, the bot will be kept busy for an hour or so (sometimes more),
|
25 |
and during this time the other videos will wait patiently.
|
|
|
27 |
### My video failed to generate! Is it lost?
|
28 |
|
29 |
That's the beauty of the dataset system: as long as you keep your video in your dataset,
|
30 |
+
and it is not published yet (is not visible on the AiTube home page), then for the AiTube Robot it will still be marked as "TODO".
|
31 |
|
32 |
So.. you normally have nothing to do (unless your video config or channel config is really damaged or invalid).
|
33 |
|
|
|
42 |
|
43 |
### Videos are taking too long to generate
|
44 |
|
45 |
+
AiTube is about generating videos in the background, slowly.
|
46 |
|
47 |
It's the whole concept: to generate multi-minutes videos, with lot of stuff like audio, speech etc
|
48 |
(if you are only interested in generate a 2 to 4 sec silent video, I suggest you use ComfyUI, Automatic1111 stable-diffusion-webui, or RunwayML or Pika Labs if your prefer commercial services).
|
|
|
70 |
|
71 |
This is a bug, it will be fixed in the future but I haven't had the opportunity to take a look yet (the cause is that I don't generate the video based on audio length yet).
|
72 |
|
73 |
+
### Can I clone AiTube or download it to run on my machine?
|
74 |
|
75 |
+
AiTube is designed to be a unique community and platform, not a downloadable tool or app.
|
76 |
Maybe one day there will be an offline version (similar to how my latent browser project worked), but for the moment the focus is on developing it as a community rather than a tool that can be cloned, rebranded, wrapped into ads by someone else etc.
|
77 |
|
78 |
### My video has been generated, but I don't see it anymore
|
package-lock.json
CHANGED
The diff for this file is too large to render.
See raw diff
|
|
package.json
CHANGED
@@ -10,27 +10,34 @@
|
|
10 |
},
|
11 |
"dependencies": {
|
12 |
"@huggingface/hub": "0.12.3-oauth",
|
13 |
-
"@huggingface/inference": "^2.6.4",
|
14 |
"@jcoreio/async-throttle": "^1.6.0",
|
15 |
-
"@photo-sphere-viewer/core": "^5.
|
16 |
-
"@photo-sphere-viewer/video-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
17 |
"@radix-ui/react-accordion": "^1.1.2",
|
18 |
-
"@radix-ui/react-avatar": "^1.0.
|
19 |
"@radix-ui/react-checkbox": "^1.0.4",
|
20 |
"@radix-ui/react-collapsible": "^1.0.3",
|
21 |
-
"@radix-ui/react-dialog": "^1.0.
|
22 |
-
"@radix-ui/react-dropdown-menu": "^2.0.
|
23 |
"@radix-ui/react-icons": "^1.3.0",
|
24 |
"@radix-ui/react-label": "^2.0.2",
|
25 |
-
"@radix-ui/react-menubar": "^1.0.
|
26 |
-
"@radix-ui/react-popover": "^1.0.
|
27 |
-
"@radix-ui/react-select": "^
|
28 |
"@radix-ui/react-separator": "^1.0.3",
|
29 |
"@radix-ui/react-slider": "^1.1.2",
|
30 |
"@radix-ui/react-slot": "^1.0.2",
|
31 |
"@radix-ui/react-switch": "^1.0.3",
|
32 |
-
"@radix-ui/react-toast": "^1.1.
|
33 |
-
"@radix-ui/react-tooltip": "^1.0.
|
34 |
"@react-spring/web": "^9.7.3",
|
35 |
"@types/lodash.debounce": "^4.0.9",
|
36 |
"@types/node": "20.4.2",
|
@@ -39,49 +46,49 @@
|
|
39 |
"@types/uuid": "^9.0.2",
|
40 |
"@upstash/query": "^0.0.2",
|
41 |
"@upstash/redis": "^1.28.3",
|
42 |
-
"alchemy-sdk": "^3.1
|
43 |
-
"autoprefixer": "10.4.
|
44 |
-
"class-variance-authority": "^0.
|
45 |
-
"clsx": "^2.
|
46 |
-
"cmdk": "^0.
|
47 |
"cookies-next": "^2.1.2",
|
48 |
-
"date-fns": "^
|
49 |
"eslint": "8.45.0",
|
50 |
"eslint-config-next": "13.4.10",
|
51 |
"fastest-levenshtein": "^1.0.16",
|
|
|
52 |
"hash-wasm": "^4.11.0",
|
53 |
"lodash.debounce": "^4.0.8",
|
54 |
"lucide-react": "^0.260.0",
|
55 |
"markdown-yaml-metadata-parser": "^3.0.0",
|
56 |
"minisearch": "^6.3.0",
|
57 |
-
"next": "^14.1.
|
58 |
-
"photo-sphere-viewer-lensflare-plugin": "^2.
|
59 |
"pick": "^0.0.1",
|
60 |
-
"postcss": "8.4.
|
61 |
-
"qs": "^6.
|
62 |
"react": "18.2.0",
|
63 |
"react-circular-progressbar": "^2.1.0",
|
64 |
"react-copy-to-clipboard": "^5.1.0",
|
65 |
"react-dom": "18.2.0",
|
66 |
"react-icons": "^4.12.0",
|
67 |
-
"react-photo-sphere-viewer": "^
|
68 |
"react-smooth-scroll-hook": "^1.3.4",
|
69 |
"react-string-avatar": "^1.2.2",
|
70 |
"react-tuby": "^0.1.24",
|
71 |
"react-virtualized-auto-sizer": "^1.0.20",
|
72 |
"react-window-infinite-loader": "^1.0.9",
|
73 |
-
"replicate": "^0.17.0",
|
74 |
"sbd": "^1.0.19",
|
75 |
"sentence-splitter": "^4.3.0",
|
76 |
-
"sharp": "^0.
|
77 |
-
"styled-components": "^6.
|
78 |
-
"tailwind-merge": "^2.
|
79 |
-
"tailwindcss": "3.4.
|
80 |
"tailwindcss-animate": "^1.0.7",
|
81 |
"temp-dir": "^3.0.0",
|
82 |
-
"ts-node": "^10.9.
|
83 |
"type-fest": "^4.8.2",
|
84 |
-
"typescript": "5.
|
85 |
"usehooks-ts": "^2.9.1",
|
86 |
"uuid": "^9.0.1",
|
87 |
"yaml": "^2.3.4",
|
|
|
10 |
},
|
11 |
"dependencies": {
|
12 |
"@huggingface/hub": "0.12.3-oauth",
|
|
|
13 |
"@jcoreio/async-throttle": "^1.6.0",
|
14 |
+
"@photo-sphere-viewer/core": "^5.7.2",
|
15 |
+
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2",
|
16 |
+
"@photo-sphere-viewer/gyroscope-plugin": "^5.7.2",
|
17 |
+
"@photo-sphere-viewer/markers-plugin": "^5.7.2-fix.1",
|
18 |
+
"@photo-sphere-viewer/overlays-plugin": "^5.7.2",
|
19 |
+
"@photo-sphere-viewer/resolution-plugin": "^5.7.2",
|
20 |
+
"@photo-sphere-viewer/settings-plugin": "^5.7.2",
|
21 |
+
"@photo-sphere-viewer/stereo-plugin": "^5.7.2",
|
22 |
+
"@photo-sphere-viewer/video-plugin": "^5.7.2",
|
23 |
+
"@photo-sphere-viewer/visible-range-plugin": "^5.7.2",
|
24 |
"@radix-ui/react-accordion": "^1.1.2",
|
25 |
+
"@radix-ui/react-avatar": "^1.0.4",
|
26 |
"@radix-ui/react-checkbox": "^1.0.4",
|
27 |
"@radix-ui/react-collapsible": "^1.0.3",
|
28 |
+
"@radix-ui/react-dialog": "^1.0.5",
|
29 |
+
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
30 |
"@radix-ui/react-icons": "^1.3.0",
|
31 |
"@radix-ui/react-label": "^2.0.2",
|
32 |
+
"@radix-ui/react-menubar": "^1.0.4",
|
33 |
+
"@radix-ui/react-popover": "^1.0.7",
|
34 |
+
"@radix-ui/react-select": "^2.0.0",
|
35 |
"@radix-ui/react-separator": "^1.0.3",
|
36 |
"@radix-ui/react-slider": "^1.1.2",
|
37 |
"@radix-ui/react-slot": "^1.0.2",
|
38 |
"@radix-ui/react-switch": "^1.0.3",
|
39 |
+
"@radix-ui/react-toast": "^1.1.5",
|
40 |
+
"@radix-ui/react-tooltip": "^1.0.7",
|
41 |
"@react-spring/web": "^9.7.3",
|
42 |
"@types/lodash.debounce": "^4.0.9",
|
43 |
"@types/node": "20.4.2",
|
|
|
46 |
"@types/uuid": "^9.0.2",
|
47 |
"@upstash/query": "^0.0.2",
|
48 |
"@upstash/redis": "^1.28.3",
|
49 |
+
"alchemy-sdk": "^3.2.1",
|
50 |
+
"autoprefixer": "10.4.19",
|
51 |
+
"class-variance-authority": "^0.7.0",
|
52 |
+
"clsx": "^2.1.0",
|
53 |
+
"cmdk": "^1.0.0",
|
54 |
"cookies-next": "^2.1.2",
|
55 |
+
"date-fns": "^3.6.0",
|
56 |
"eslint": "8.45.0",
|
57 |
"eslint-config-next": "13.4.10",
|
58 |
"fastest-levenshtein": "^1.0.16",
|
59 |
+
"gsplat": "^1.2.3",
|
60 |
"hash-wasm": "^4.11.0",
|
61 |
"lodash.debounce": "^4.0.8",
|
62 |
"lucide-react": "^0.260.0",
|
63 |
"markdown-yaml-metadata-parser": "^3.0.0",
|
64 |
"minisearch": "^6.3.0",
|
65 |
+
"next": "^14.1.4",
|
66 |
+
"photo-sphere-viewer-lensflare-plugin": "^2.1.2",
|
67 |
"pick": "^0.0.1",
|
68 |
+
"postcss": "8.4.38",
|
69 |
+
"qs": "^6.12.0",
|
70 |
"react": "18.2.0",
|
71 |
"react-circular-progressbar": "^2.1.0",
|
72 |
"react-copy-to-clipboard": "^5.1.0",
|
73 |
"react-dom": "18.2.0",
|
74 |
"react-icons": "^4.12.0",
|
75 |
+
"react-photo-sphere-viewer": "^5.0.2-psv5.7.1",
|
76 |
"react-smooth-scroll-hook": "^1.3.4",
|
77 |
"react-string-avatar": "^1.2.2",
|
78 |
"react-tuby": "^0.1.24",
|
79 |
"react-virtualized-auto-sizer": "^1.0.20",
|
80 |
"react-window-infinite-loader": "^1.0.9",
|
|
|
81 |
"sbd": "^1.0.19",
|
82 |
"sentence-splitter": "^4.3.0",
|
83 |
+
"sharp": "^0.33.3",
|
84 |
+
"styled-components": "^6.1.8",
|
85 |
+
"tailwind-merge": "^2.2.2",
|
86 |
+
"tailwindcss": "3.4.3",
|
87 |
"tailwindcss-animate": "^1.0.7",
|
88 |
"temp-dir": "^3.0.0",
|
89 |
+
"ts-node": "^10.9.2",
|
90 |
"type-fest": "^4.8.2",
|
91 |
+
"typescript": "5.4.4",
|
92 |
"usehooks-ts": "^2.9.1",
|
93 |
"uuid": "^9.0.1",
|
94 |
"yaml": "^2.3.4",
|
src/app/api/login/route.ts
CHANGED
@@ -3,12 +3,12 @@ import { NextResponse, NextRequest } from "next/server"
|
|
3 |
|
4 |
const defaultState = JSON.stringify({
|
5 |
nonce: "",
|
6 |
-
redirectUri:
|
7 |
state: JSON.stringify({ redirectTo: "/" })
|
8 |
})
|
9 |
|
10 |
export async function GET(req: NextRequest) {
|
11 |
-
// we are going to pass the whole thing unchanged to the
|
12 |
const params = req.url.split("/api/login").pop() || ""
|
13 |
|
14 |
|
@@ -26,7 +26,7 @@ export async function GET(req: NextRequest) {
|
|
26 |
state // <-- this is defined by us!
|
27 |
} = JSON.parse(`${query.state || defaultState}`)
|
28 |
|
29 |
-
// this is the path of the
|
30 |
// eg. this can be /account, /, or nothing
|
31 |
// const redirectTo = `${state.redirectTo || "/"}`
|
32 |
|
@@ -35,11 +35,11 @@ export async function GET(req: NextRequest) {
|
|
35 |
const redirectTo = "/account"
|
36 |
|
37 |
|
38 |
-
return NextResponse.redirect(
|
39 |
}
|
40 |
|
41 |
export async function POST(req: NextRequest, res: NextResponse) {
|
42 |
-
// we are going to pass the whole thing unchanged to the
|
43 |
const params = req.url.split("/api/login").pop() || ""
|
44 |
|
45 |
|
@@ -57,7 +57,7 @@ export async function POST(req: NextRequest, res: NextResponse) {
|
|
57 |
state // <-- this is defined by us!
|
58 |
} = JSON.parse(`${query.state || defaultState}`)
|
59 |
|
60 |
-
// this is the path of the
|
61 |
// eg. this can be /account, /, or nothing
|
62 |
// const redirectTo = `${state.redirectTo || "/"}`
|
63 |
|
@@ -66,5 +66,5 @@ export async function POST(req: NextRequest, res: NextResponse) {
|
|
66 |
const redirectTo = "/account"
|
67 |
|
68 |
|
69 |
-
return NextResponse.redirect(
|
70 |
}
|
|
|
3 |
|
4 |
const defaultState = JSON.stringify({
|
5 |
nonce: "",
|
6 |
+
redirectUri: `${process.env.NEXT_PUBLIC_DOMAIN}/api/login`,
|
7 |
state: JSON.stringify({ redirectTo: "/" })
|
8 |
})
|
9 |
|
10 |
export async function GET(req: NextRequest) {
|
11 |
+
// we are going to pass the whole thing unchanged to the AiTube frontend
|
12 |
const params = req.url.split("/api/login").pop() || ""
|
13 |
|
14 |
|
|
|
26 |
state // <-- this is defined by us!
|
27 |
} = JSON.parse(`${query.state || defaultState}`)
|
28 |
|
29 |
+
// this is the path of the AiTube page which the user was browser
|
30 |
// eg. this can be /account, /, or nothing
|
31 |
// const redirectTo = `${state.redirectTo || "/"}`
|
32 |
|
|
|
35 |
const redirectTo = "/account"
|
36 |
|
37 |
|
38 |
+
return NextResponse.redirect(`${process.env.NEXT_PUBLIC_DOMAIN}${redirectTo}${params}`)
|
39 |
}
|
40 |
|
41 |
export async function POST(req: NextRequest, res: NextResponse) {
|
42 |
+
// we are going to pass the whole thing unchanged to the AiTube frontend
|
43 |
const params = req.url.split("/api/login").pop() || ""
|
44 |
|
45 |
|
|
|
57 |
state // <-- this is defined by us!
|
58 |
} = JSON.parse(`${query.state || defaultState}`)
|
59 |
|
60 |
+
// this is the path of the AiTube page which the user was browser
|
61 |
// eg. this can be /account, /, or nothing
|
62 |
// const redirectTo = `${state.redirectTo || "/"}`
|
63 |
|
|
|
66 |
const redirectTo = "/account"
|
67 |
|
68 |
|
69 |
+
return NextResponse.redirect(`${process.env.NEXT_PUBLIC_DOMAIN}${redirectTo}${params}`)
|
70 |
}
|
src/app/api/media/[mediaId]/route.ts
ADDED
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { NextResponse, NextRequest } from "next/server"
|
2 |
+
|
3 |
+
import { getVideo } from "@/app/server/actions/ai-tube-hf/getVideo"
|
4 |
+
import { parseMediaProjectionType } from "@/lib/parseMediaProjectionType";
|
5 |
+
|
6 |
+
export async function GET(req: NextRequest) {
|
7 |
+
const mediaId = req.url.split("/").pop() || ""
|
8 |
+
const media = await getVideo({ videoId: mediaId, neverThrow: true })
|
9 |
+
if (!media) {
|
10 |
+
return new NextResponse("media not found", { status: 404 });
|
11 |
+
}
|
12 |
+
const isEquirectangular = parseMediaProjectionType(media) === "equirectangular"
|
13 |
+
|
14 |
+
const html = `
|
15 |
+
<!DOCTYPE html>
|
16 |
+
<html>
|
17 |
+
<head>
|
18 |
+
<meta charset="utf-8">
|
19 |
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
20 |
+
<title>${media.label} - AiTube</title>
|
21 |
+
<meta name="description" content="${media.description}<">
|
22 |
+
<script src="/aframe/aframe-master.js"></script>
|
23 |
+
<script src="/aframe/play-on-click.js"></script>
|
24 |
+
<script src="/aframe/hide-on-play.js"></script>
|
25 |
+
</head>
|
26 |
+
<body>
|
27 |
+
<a-scene>
|
28 |
+
<a-assets>
|
29 |
+
<video
|
30 |
+
id="video"
|
31 |
+
loop
|
32 |
+
crossorigin="anonymous"
|
33 |
+
playsinline
|
34 |
+
webkit-playsinline
|
35 |
+
src="${media.assetUrlHd || media.assetUrl}">
|
36 |
+
</video>
|
37 |
+
</a-assets>
|
38 |
+
${
|
39 |
+
isEquirectangular
|
40 |
+
? `
|
41 |
+
<a-videosphere
|
42 |
+
rotation="0 -90 0"
|
43 |
+
src="#video"
|
44 |
+
play-on-click>
|
45 |
+
</a-videosphere>
|
46 |
+
` :
|
47 |
+
`<a-video
|
48 |
+
src="#video"
|
49 |
+
width="${
|
50 |
+
3 // 1024
|
51 |
+
}" height="${
|
52 |
+
1.6875 // 576
|
53 |
+
}"
|
54 |
+
play-on-click>
|
55 |
+
</a-video>`
|
56 |
+
}
|
57 |
+
<a-camera>
|
58 |
+
<a-entity
|
59 |
+
position="0 0 -1.5"
|
60 |
+
text="align: center;
|
61 |
+
width: 6;
|
62 |
+
wrapCount: 100;
|
63 |
+
color: white;
|
64 |
+
value: Click or tap to start video"
|
65 |
+
hide-on-play="#video">
|
66 |
+
</a-entity>
|
67 |
+
</a-camera>
|
68 |
+
</a-scene>
|
69 |
+
</body>
|
70 |
+
</html>`
|
71 |
+
|
72 |
+
return new NextResponse(html, {
|
73 |
+
status: 200,
|
74 |
+
headers: new Headers({ "content-type": "text/html" }),
|
75 |
+
})
|
76 |
+
}
|
src/app/api/video/[videoId]/route.ts
CHANGED
@@ -1,18 +1,19 @@
|
|
1 |
import { NextResponse, NextRequest } from "next/server"
|
2 |
|
3 |
import { getVideo } from "@/app/server/actions/ai-tube-hf/getVideo"
|
4 |
-
import {
|
5 |
|
|
|
|
|
|
|
6 |
export async function GET(req: NextRequest) {
|
|
|
7 |
const videoId = req.url.split("/").pop() || ""
|
8 |
const video = await getVideo({ videoId, neverThrow: true })
|
9 |
if (!video) {
|
10 |
return new NextResponse("video not found", { status: 404 });
|
11 |
}
|
12 |
-
const isEquirectangular = (
|
13 |
-
video.projection === "equirectangular" ||
|
14 |
-
parseProjectionFromLoRA(video.lora) === "equirectangular"
|
15 |
-
)
|
16 |
|
17 |
const html = `
|
18 |
<!DOCTYPE html>
|
@@ -20,7 +21,7 @@ export async function GET(req: NextRequest) {
|
|
20 |
<head>
|
21 |
<meta charset="utf-8">
|
22 |
<meta name="apple-mobile-web-app-capable" content="yes">
|
23 |
-
<title>${video.label} -
|
24 |
<meta name="description" content="${video.description}<">
|
25 |
<script src="/aframe/aframe-master.js"></script>
|
26 |
<script src="/aframe/play-on-click.js"></script>
|
|
|
1 |
import { NextResponse, NextRequest } from "next/server"
|
2 |
|
3 |
import { getVideo } from "@/app/server/actions/ai-tube-hf/getVideo"
|
4 |
+
import { parseMediaProjectionType } from "@/lib/parseMediaProjectionType";
|
5 |
|
6 |
+
/**
|
7 |
+
* @deprecated
|
8 |
+
*/
|
9 |
export async function GET(req: NextRequest) {
|
10 |
+
|
11 |
const videoId = req.url.split("/").pop() || ""
|
12 |
const video = await getVideo({ videoId, neverThrow: true })
|
13 |
if (!video) {
|
14 |
return new NextResponse("video not found", { status: 404 });
|
15 |
}
|
16 |
+
const isEquirectangular = parseMediaProjectionType(video) === "equirectangular"
|
|
|
|
|
|
|
17 |
|
18 |
const html = `
|
19 |
<!DOCTYPE html>
|
|
|
21 |
<head>
|
22 |
<meta charset="utf-8">
|
23 |
<meta name="apple-mobile-web-app-capable" content="yes">
|
24 |
+
<title>${video.label} - AiTube</title>
|
25 |
<meta name="description" content="${video.description}<">
|
26 |
<script src="/aframe/aframe-master.js"></script>
|
27 |
<script src="/aframe/play-on-click.js"></script>
|
src/app/embed/page.tsx
CHANGED
@@ -17,29 +17,29 @@ export async function generateMetadata(
|
|
17 |
const metadataBase = new URL('https://huggingface.co/spaces/jbilcke-hf/ai-tube')
|
18 |
|
19 |
try {
|
20 |
-
const
|
21 |
|
22 |
-
if (!
|
23 |
-
throw new Error("
|
24 |
}
|
25 |
|
26 |
return {
|
27 |
-
title: `${
|
28 |
metadataBase,
|
29 |
openGraph: {
|
30 |
// some cool stuff we could use here:
|
31 |
// 'video.tv_show' | 'video.other' | 'video.movie' | 'video.episode';
|
32 |
type: "video.other",
|
33 |
// url: "https://example.com",
|
34 |
-
title:
|
35 |
-
description:
|
36 |
siteName: "AiTube",
|
37 |
images: [
|
38 |
-
`https://huggingface.co/datasets/jbilcke-hf/ai-tube-index/resolve/main/videos/${
|
39 |
],
|
40 |
videos: [
|
41 |
{
|
42 |
-
"url":
|
43 |
}
|
44 |
],
|
45 |
// images: ['/some-specific-page-image.jpg', ...previousImages],
|
@@ -47,11 +47,11 @@ export async function generateMetadata(
|
|
47 |
twitter: {
|
48 |
card: "player",
|
49 |
site: "@flngr",
|
50 |
-
description:
|
51 |
-
images: `https://huggingface.co/datasets/jbilcke-hf/ai-tube-index/resolve/main/videos/${
|
52 |
players: {
|
53 |
-
playerUrl:
|
54 |
-
streamUrl: `https://huggingface.co/datasets/jbilcke-hf/ai-tube-index/resolve/main/videos/${
|
55 |
width: 1024,
|
56 |
height: 576
|
57 |
}
|
@@ -76,7 +76,14 @@ export async function generateMetadata(
|
|
76 |
}
|
77 |
|
78 |
|
79 |
-
export default async function Embed({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
80 |
const publicVideo = await getVideo({ videoId, neverThrow: true })
|
81 |
// console.log("WatchPage: --> " + video?.id)
|
82 |
return (
|
|
|
17 |
const metadataBase = new URL('https://huggingface.co/spaces/jbilcke-hf/ai-tube')
|
18 |
|
19 |
try {
|
20 |
+
const media = await getVideo({ videoId, neverThrow: true })
|
21 |
|
22 |
+
if (!media) {
|
23 |
+
throw new Error("Media not found")
|
24 |
}
|
25 |
|
26 |
return {
|
27 |
+
title: `${media.label} - AiTube`,
|
28 |
metadataBase,
|
29 |
openGraph: {
|
30 |
// some cool stuff we could use here:
|
31 |
// 'video.tv_show' | 'video.other' | 'video.movie' | 'video.episode';
|
32 |
type: "video.other",
|
33 |
// url: "https://example.com",
|
34 |
+
title: media.label || "", // put the video title here
|
35 |
+
description: media.description || "", // put the video description here
|
36 |
siteName: "AiTube",
|
37 |
images: [
|
38 |
+
`https://huggingface.co/datasets/jbilcke-hf/ai-tube-index/resolve/main/videos/${media.id}.webp`
|
39 |
],
|
40 |
videos: [
|
41 |
{
|
42 |
+
"url": media.assetUrlHd || media.assetUrl
|
43 |
}
|
44 |
],
|
45 |
// images: ['/some-specific-page-image.jpg', ...previousImages],
|
|
|
47 |
twitter: {
|
48 |
card: "player",
|
49 |
site: "@flngr",
|
50 |
+
description: media.description || "",
|
51 |
+
images: `https://huggingface.co/datasets/jbilcke-hf/ai-tube-index/resolve/main/videos/${media.id}.webp`,
|
52 |
players: {
|
53 |
+
playerUrl: `${process.env.NEXT_PUBLIC_DOMAIN}/embed?v=${media.id}`,
|
54 |
+
streamUrl: `https://huggingface.co/datasets/jbilcke-hf/ai-tube-index/resolve/main/videos/${media.id}.mp4`,
|
55 |
width: 1024,
|
56 |
height: 576
|
57 |
}
|
|
|
76 |
}
|
77 |
|
78 |
|
79 |
+
export default async function Embed({
|
80 |
+
searchParams: {
|
81 |
+
v: videoId
|
82 |
+
|
83 |
+
// TODO add:
|
84 |
+
// m: mediaId
|
85 |
+
}
|
86 |
+
}: AppQueryProps) {
|
87 |
const publicVideo = await getVideo({ videoId, neverThrow: true })
|
88 |
// console.log("WatchPage: --> " + video?.id)
|
89 |
return (
|
src/app/interface/about/index.tsx
CHANGED
@@ -43,14 +43,14 @@ export function About() {
|
|
43 |
</DialogTrigger>
|
44 |
<DialogContent className="sm:max-w-[800px]">
|
45 |
<DialogHeader>
|
46 |
-
<DialogTitle>
|
47 |
<DialogDescription className="w-full text-center text-lg font-bold text-stone-800">
|
48 |
-
What is
|
49 |
</DialogDescription>
|
50 |
</DialogHeader>
|
51 |
<div className="grid gap-4 py-4 text-stone-200 text-base">
|
52 |
<p className="">
|
53 |
-
|
54 |
</p>
|
55 |
<p>
|
56 |
To my knowledge, is the first platform to operate this way. As a research sandbox, it features other experiments such as being the first platform to autonomously generate VR videos using AI (<a href="api/video/37b626a8-3eb9-4127-8d91-20837bc08ae7" target="_blank" className="underline">open this example</a> with a WebXR-compatible device eg. an iPhone).
|
|
|
43 |
</DialogTrigger>
|
44 |
<DialogContent className="sm:max-w-[800px]">
|
45 |
<DialogHeader>
|
46 |
+
<DialogTitle>AiTube</DialogTitle>
|
47 |
<DialogDescription className="w-full text-center text-lg font-bold text-stone-800">
|
48 |
+
What is AiTube?
|
49 |
</DialogDescription>
|
50 |
</DialogHeader>
|
51 |
<div className="grid gap-4 py-4 text-stone-200 text-base">
|
52 |
<p className="">
|
53 |
+
AiTube is a sandbox platform launched in Nov 2023 to experiment with autonomous creation of long videos. The videos are generated from single text prompts by humans and by AI robots.
|
54 |
</p>
|
55 |
<p>
|
56 |
To my knowledge, is the first platform to operate this way. As a research sandbox, it features other experiments such as being the first platform to autonomously generate VR videos using AI (<a href="api/video/37b626a8-3eb9-4127-8d91-20837bc08ae7" target="_blank" className="underline">open this example</a> with a WebXR-compatible device eg. an iPhone).
|
src/app/interface/collection-card/index.tsx
CHANGED
@@ -112,6 +112,7 @@ export function CollectionCard({
|
|
112 |
`flex flex-col items-center justify-center text-center`,
|
113 |
`bg-neutral-900 rounded`,
|
114 |
`text-2xs font-semibold px-[3px] py-[1px]`,
|
|
|
115 |
)}
|
116 |
>{formatDuration(duration)}</div>
|
117 |
</div>
|
|
|
112 |
`flex flex-col items-center justify-center text-center`,
|
113 |
`bg-neutral-900 rounded`,
|
114 |
`text-2xs font-semibold px-[3px] py-[1px]`,
|
115 |
+
isFinite(duration) && !isNaN(duration) && duration > 0 ? 'opacity-100' : 'opacity-0'
|
116 |
)}
|
117 |
>{formatDuration(duration)}</div>
|
118 |
</div>
|
src/app/interface/equirectangular-video-player/index.tsx
CHANGED
@@ -3,7 +3,7 @@
|
|
3 |
import AutoSizer from "react-virtualized-auto-sizer"
|
4 |
|
5 |
import { cn } from "@/lib/utils"
|
6 |
-
import {
|
7 |
|
8 |
import { VideoSphereViewer } from "./viewer"
|
9 |
|
@@ -11,7 +11,7 @@ export function EquirectangularVideoPlayer({
|
|
11 |
video,
|
12 |
className = "",
|
13 |
}: {
|
14 |
-
video?:
|
15 |
className?: string
|
16 |
}) {
|
17 |
|
|
|
3 |
import AutoSizer from "react-virtualized-auto-sizer"
|
4 |
|
5 |
import { cn } from "@/lib/utils"
|
6 |
+
import { MediaInfo } from "@/types/general"
|
7 |
|
8 |
import { VideoSphereViewer } from "./viewer"
|
9 |
|
|
|
11 |
video,
|
12 |
className = "",
|
13 |
}: {
|
14 |
+
video?: MediaInfo
|
15 |
className?: string
|
16 |
}) {
|
17 |
|
src/app/interface/equirectangular-video-player/viewer.tsx
CHANGED
@@ -1,14 +1,21 @@
|
|
1 |
"use client"
|
2 |
|
3 |
-
import { useEffect, useRef
|
4 |
-
import
|
5 |
-
import { PanoramaPosition, PluginConstructor, Point, Position, SphericalPosition, Viewer } from "@photo-sphere-viewer/core"
|
6 |
-
import { EquirectangularVideoAdapter, LensflarePlugin, ReactPhotoSphereViewer, ResolutionPlugin, SettingsPlugin, VideoPlugin } from "react-photo-sphere-viewer"
|
7 |
|
8 |
-
import {
|
9 |
-
|
|
|
|
|
|
|
|
|
|
|
10 |
|
11 |
-
|
|
|
|
|
|
|
|
|
12 |
|
13 |
export function VideoSphereViewer({
|
14 |
video,
|
@@ -17,7 +24,7 @@ export function VideoSphereViewer({
|
|
17 |
height,
|
18 |
muted = false,
|
19 |
}: {
|
20 |
-
video:
|
21 |
className?: string
|
22 |
width: number
|
23 |
height: number
|
@@ -64,15 +71,16 @@ export function VideoSphereViewer({
|
|
64 |
// plugins={[[LensflarePlugin, { lensflares: [] }]]}
|
65 |
|
66 |
adapter={[EquirectangularVideoAdapter, { muted }]}
|
|
|
67 |
navbar="video"
|
68 |
src=""
|
69 |
plugins={[
|
|
|
70 |
[VideoPlugin, {
|
71 |
muted,
|
72 |
// progressbar: true,
|
73 |
bigbutton: false
|
74 |
}],
|
75 |
-
// SettingsPlugin,
|
76 |
[ResolutionPlugin, {
|
77 |
defaultResolution: 'HD',
|
78 |
resolutions: [
|
|
|
1 |
"use client"
|
2 |
|
3 |
+
import { useEffect, useRef } from "react"
|
4 |
+
import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer"
|
|
|
|
|
5 |
|
6 |
+
import { Viewer } from "@photo-sphere-viewer/core"
|
7 |
+
|
8 |
+
import { EquirectangularVideoAdapter } from "@photo-sphere-viewer/equirectangular-video-adapter"
|
9 |
+
|
10 |
+
import { SettingsPlugin } from "@photo-sphere-viewer/settings-plugin"
|
11 |
+
import { ResolutionPlugin } from "@photo-sphere-viewer/resolution-plugin"
|
12 |
+
import { VideoPlugin } from "@photo-sphere-viewer/video-plugin"
|
13 |
|
14 |
+
import "@photo-sphere-viewer/settings-plugin/index.css"
|
15 |
+
import "@photo-sphere-viewer/video-plugin/index.css"
|
16 |
+
|
17 |
+
import { cn } from "@/lib/utils"
|
18 |
+
import { MediaInfo } from "@/types/general"
|
19 |
|
20 |
export function VideoSphereViewer({
|
21 |
video,
|
|
|
24 |
height,
|
25 |
muted = false,
|
26 |
}: {
|
27 |
+
video: MediaInfo
|
28 |
className?: string
|
29 |
width: number
|
30 |
height: number
|
|
|
71 |
// plugins={[[LensflarePlugin, { lensflares: [] }]]}
|
72 |
|
73 |
adapter={[EquirectangularVideoAdapter, { muted }]}
|
74 |
+
|
75 |
navbar="video"
|
76 |
src=""
|
77 |
plugins={[
|
78 |
+
[SettingsPlugin, {}],
|
79 |
[VideoPlugin, {
|
80 |
muted,
|
81 |
// progressbar: true,
|
82 |
bigbutton: false
|
83 |
}],
|
|
|
84 |
[ResolutionPlugin, {
|
85 |
defaultResolution: 'HD',
|
86 |
resolutions: [
|
src/app/interface/gsplat/index.tsx
ADDED
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useEffect, useRef } from "react"
|
2 |
+
import * as SPLAT from "gsplat"
|
3 |
+
|
4 |
+
type GsplatStatus =
|
5 |
+
| "idle"
|
6 |
+
| "loading"
|
7 |
+
| "loaded"
|
8 |
+
| "failed"
|
9 |
+
|
10 |
+
export function Gsplat({
|
11 |
+
url,
|
12 |
+
width,
|
13 |
+
height,
|
14 |
+
className = "" }: {
|
15 |
+
url: string
|
16 |
+
width?: number
|
17 |
+
height?: number
|
18 |
+
className?: string
|
19 |
+
}) {
|
20 |
+
const canvasRef = useRef<HTMLCanvasElement>(null)
|
21 |
+
const sceneRef = useRef<SPLAT.Scene>()
|
22 |
+
const cameraRef = useRef<SPLAT.Camera>()
|
23 |
+
const controlsRef = useRef<SPLAT.OrbitControls>()
|
24 |
+
const rendererRef = useRef<SPLAT.WebGLRenderer>()
|
25 |
+
const frameIdRef = useRef<number>(0)
|
26 |
+
const statusRef = useRef<GsplatStatus>("idle")
|
27 |
+
|
28 |
+
function animate() {
|
29 |
+
const renderer = rendererRef.current
|
30 |
+
const scene = sceneRef.current
|
31 |
+
const camera = cameraRef.current
|
32 |
+
const controls = controlsRef.current
|
33 |
+
if (!scene || !renderer || !camera || !controls) { return }
|
34 |
+
|
35 |
+
|
36 |
+
controls.update()
|
37 |
+
renderer.render(scene, camera)
|
38 |
+
frameIdRef.current = requestAnimationFrame(animate)
|
39 |
+
}
|
40 |
+
|
41 |
+
async function loadScene() {
|
42 |
+
const canvas = canvasRef.current
|
43 |
+
if (!canvas) { return }
|
44 |
+
|
45 |
+
const status = statusRef.current
|
46 |
+
if (!status || status === "loaded" || status === "loading" || status === "failed") {
|
47 |
+
console.log(`Gsplat: a scene is already loading or loaded: skipping..`)
|
48 |
+
return
|
49 |
+
}
|
50 |
+
|
51 |
+
statusRef.current = "loading"
|
52 |
+
|
53 |
+
try {
|
54 |
+
const renderer = rendererRef.current = new SPLAT.WebGLRenderer(canvas)
|
55 |
+
|
56 |
+
let fileUrl = url.trim()
|
57 |
+
let fileExt = fileUrl.toLowerCase().split(".").pop() || "splat"
|
58 |
+
const isVideo = fileExt === "splatv"
|
59 |
+
|
60 |
+
if (isVideo) {
|
61 |
+
console.log("Gsplat: loading video splat..")
|
62 |
+
|
63 |
+
renderer.addProgram(new SPLAT.VideoRenderProgram(renderer))
|
64 |
+
|
65 |
+
const scene = sceneRef.current = new SPLAT.Scene()
|
66 |
+
const camera = cameraRef.current = new SPLAT.Camera()
|
67 |
+
const controls = controlsRef.current = new SPLAT.OrbitControls(camera, renderer.canvas)
|
68 |
+
|
69 |
+
await SPLAT.SplatvLoader.LoadAsync(url, scene, camera, (progress) => {
|
70 |
+
console.log(`${Math.round(progress * 100)}%`)
|
71 |
+
})
|
72 |
+
|
73 |
+
controls.setCameraTarget(camera.position.add(camera.forward.multiply(5)))
|
74 |
+
} else {
|
75 |
+
console.log("Gsplat: loading static splat..")
|
76 |
+
const scene = sceneRef.current = new SPLAT.Scene()
|
77 |
+
const camera = cameraRef.current = new SPLAT.Camera()
|
78 |
+
const controls = controlsRef.current = new SPLAT.OrbitControls(camera, renderer.canvas)
|
79 |
+
|
80 |
+
await SPLAT.Loader.LoadAsync(url, scene, (progress) => {
|
81 |
+
console.log(`${Math.round(progress * 100)}%`)
|
82 |
+
})
|
83 |
+
}
|
84 |
+
|
85 |
+
console.log("Gsplat: finished loading! rendering..")
|
86 |
+
statusRef.current = "loaded"
|
87 |
+
} catch (err) {
|
88 |
+
console.error(`Gsplat: failed to load the content`)
|
89 |
+
statusRef.current = "failed"
|
90 |
+
return
|
91 |
+
}
|
92 |
+
|
93 |
+
animate()
|
94 |
+
};
|
95 |
+
|
96 |
+
useEffect(() => {
|
97 |
+
if (!canvasRef.current) { return }
|
98 |
+
loadScene()
|
99 |
+
return () => { cancelAnimationFrame(frameIdRef?.current || 0) }
|
100 |
+
}, [])
|
101 |
+
|
102 |
+
// responsive width and height
|
103 |
+
useEffect(() => {
|
104 |
+
const canvas = canvasRef.current
|
105 |
+
const renderer = rendererRef.current
|
106 |
+
|
107 |
+
if (!canvas || !renderer) { return }
|
108 |
+
|
109 |
+
// renderer.setSize(canvas.clientWidth, canvas.clientHeight)
|
110 |
+
renderer.setSize(
|
111 |
+
width || canvas.clientWidth,
|
112 |
+
height || canvas.clientHeight
|
113 |
+
)
|
114 |
+
}, [width, height])
|
115 |
+
return (
|
116 |
+
<div style={{ width, height }} className={className}>
|
117 |
+
<canvas ref={canvasRef} style={{ width, height}}></canvas>
|
118 |
+
</div>
|
119 |
+
);
|
120 |
+
}
|
src/app/interface/left-menu/index.tsx
CHANGED
@@ -1,8 +1,8 @@
|
|
1 |
import Link from "next/link"
|
2 |
-
import { TbBrandDiscord } from "react-icons/tb"
|
3 |
import { AiOutlineQuestionCircle } from "react-icons/ai"
|
4 |
import { GrChannel } from "react-icons/gr"
|
5 |
-
import { MdVideoLibrary } from "react-icons/md"
|
6 |
import { RiHome8Line } from "react-icons/ri"
|
7 |
import { PiRobot } from "react-icons/pi"
|
8 |
import { CgProfile } from "react-icons/cg"
|
@@ -56,6 +56,32 @@ export function LeftMenu() {
|
|
56 |
Music
|
57 |
</MenuItem>
|
58 |
</Link>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
59 |
</div>
|
60 |
<div className={cn(
|
61 |
`flex flex-col w-full`,
|
|
|
1 |
import Link from "next/link"
|
2 |
+
import { TbBrandDiscord, TbOctahedron } from "react-icons/tb"
|
3 |
import { AiOutlineQuestionCircle } from "react-icons/ai"
|
4 |
import { GrChannel } from "react-icons/gr"
|
5 |
+
import { MdOutlineLiveTv, MdOutlineVideogameAsset, MdVideoLibrary } from "react-icons/md"
|
6 |
import { RiHome8Line } from "react-icons/ri"
|
7 |
import { PiRobot } from "react-icons/pi"
|
8 |
import { CgProfile } from "react-icons/cg"
|
|
|
56 |
Music
|
57 |
</MenuItem>
|
58 |
</Link>
|
59 |
+
{/*
|
60 |
+
<Link href="/gaming">
|
61 |
+
<MenuItem
|
62 |
+
icon={<TbOctahedron className="h-6.5 w-6.5" />}
|
63 |
+
selected={view === "public_4d"}
|
64 |
+
>
|
65 |
+
4D
|
66 |
+
</MenuItem>
|
67 |
+
</Link>
|
68 |
+
<Link href="/gaming">
|
69 |
+
<MenuItem
|
70 |
+
icon={<MdOutlineVideogameAsset className="h-6.5 w-6.5" />}
|
71 |
+
selected={view === "public_gaming"}
|
72 |
+
>
|
73 |
+
Gaming
|
74 |
+
</MenuItem>
|
75 |
+
</Link>
|
76 |
+
<Link href="/live">
|
77 |
+
<MenuItem
|
78 |
+
icon={<MdOutlineLiveTv className="h-6.5 w-6.5" />}
|
79 |
+
selected={view === "public_live"}
|
80 |
+
>
|
81 |
+
Live
|
82 |
+
</MenuItem>
|
83 |
+
</Link>
|
84 |
+
*/}
|
85 |
</div>
|
86 |
<div className={cn(
|
87 |
`flex flex-col w-full`,
|
src/app/interface/like-button/index.tsx
CHANGED
@@ -1,20 +1,20 @@
|
|
1 |
import { useEffect, useState, useTransition } from "react"
|
2 |
-
import { VideoInfo, VideoRating } from "@/types/general"
|
3 |
-
|
4 |
-
import { GenericLikeButton } from "./generic"
|
5 |
-
import { getVideoRating, rateVideo } from "@/app/server/actions/stats"
|
6 |
import { useLocalStorage } from "usehooks-ts"
|
|
|
|
|
|
|
7 |
import { localStorageKeys } from "@/app/state/localStorageKeys"
|
8 |
import { defaultSettings } from "@/app/state/defaultSettings"
|
9 |
|
|
|
10 |
export function LikeButton({
|
11 |
-
|
12 |
}: {
|
13 |
-
|
14 |
}) {
|
15 |
const [_pending, startTransition] = useTransition()
|
16 |
|
17 |
-
const [rating, setRating] = useState<
|
18 |
isLikedByUser: false,
|
19 |
isDislikedByUser: false,
|
20 |
numberOfLikes: 0,
|
@@ -28,15 +28,15 @@ export function LikeButton({
|
|
28 |
|
29 |
useEffect(() => {
|
30 |
startTransition(async () => {
|
31 |
-
if (!
|
32 |
|
33 |
-
const freshRating = await
|
34 |
setRating(freshRating)
|
35 |
|
36 |
})
|
37 |
-
}, [
|
38 |
|
39 |
-
if (!
|
40 |
|
41 |
if (!huggingfaceApiKey) { return null }
|
42 |
|
@@ -52,7 +52,7 @@ export function LikeButton({
|
|
52 |
})
|
53 |
startTransition(async () => {
|
54 |
try {
|
55 |
-
const freshRating = await
|
56 |
// setRating(freshRating)
|
57 |
} catch (err) {
|
58 |
setRating(previousRating)
|
@@ -72,7 +72,7 @@ export function LikeButton({
|
|
72 |
})
|
73 |
startTransition(async () => {
|
74 |
try {
|
75 |
-
const freshRating = await
|
76 |
// setRating(freshRating)
|
77 |
} catch (err) {
|
78 |
setRating(previousRating)
|
|
|
1 |
import { useEffect, useState, useTransition } from "react"
|
|
|
|
|
|
|
|
|
2 |
import { useLocalStorage } from "usehooks-ts"
|
3 |
+
|
4 |
+
import { MediaInfo, MediaRating } from "@/types/general"
|
5 |
+
import { getMediaRating, rateMedia } from "@/app/server/actions/stats"
|
6 |
import { localStorageKeys } from "@/app/state/localStorageKeys"
|
7 |
import { defaultSettings } from "@/app/state/defaultSettings"
|
8 |
|
9 |
+
import { GenericLikeButton } from "./generic"
|
10 |
export function LikeButton({
|
11 |
+
media
|
12 |
}: {
|
13 |
+
media?: MediaInfo
|
14 |
}) {
|
15 |
const [_pending, startTransition] = useTransition()
|
16 |
|
17 |
+
const [rating, setRating] = useState<MediaRating>({
|
18 |
isLikedByUser: false,
|
19 |
isDislikedByUser: false,
|
20 |
numberOfLikes: 0,
|
|
|
28 |
|
29 |
useEffect(() => {
|
30 |
startTransition(async () => {
|
31 |
+
if (!media || !media?.id) { return }
|
32 |
|
33 |
+
const freshRating = await getMediaRating(media.id, huggingfaceApiKey)
|
34 |
setRating(freshRating)
|
35 |
|
36 |
})
|
37 |
+
}, [media?.id, huggingfaceApiKey])
|
38 |
|
39 |
+
if (!media) { return null }
|
40 |
|
41 |
if (!huggingfaceApiKey) { return null }
|
42 |
|
|
|
52 |
})
|
53 |
startTransition(async () => {
|
54 |
try {
|
55 |
+
const freshRating = await rateMedia(media.id, true, huggingfaceApiKey)
|
56 |
// setRating(freshRating)
|
57 |
} catch (err) {
|
58 |
setRating(previousRating)
|
|
|
72 |
})
|
73 |
startTransition(async () => {
|
74 |
try {
|
75 |
+
const freshRating = await rateMedia(media.id, false, huggingfaceApiKey)
|
76 |
// setRating(freshRating)
|
77 |
} catch (err) {
|
78 |
setRating(previousRating)
|
src/app/interface/media-list/index.tsx
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
import { cn } from "@/lib/utils"
|
2 |
-
import { MediaDisplayLayout,
|
3 |
import { TrackCard } from "../track-card"
|
4 |
import { VideoCard } from "../video-card"
|
5 |
|
@@ -11,7 +11,7 @@ export function MediaList({
|
|
11 |
onSelect,
|
12 |
selectedId,
|
13 |
}: {
|
14 |
-
items:
|
15 |
|
16 |
/**
|
17 |
* Layout mode
|
@@ -31,7 +31,7 @@ export function MediaList({
|
|
31 |
|
32 |
className?: string
|
33 |
|
34 |
-
onSelect?: (media:
|
35 |
|
36 |
selectedId?: string
|
37 |
}) {
|
|
|
1 |
import { cn } from "@/lib/utils"
|
2 |
+
import { MediaDisplayLayout, MediaInfo } from "@/types/general"
|
3 |
import { TrackCard } from "../track-card"
|
4 |
import { VideoCard } from "../video-card"
|
5 |
|
|
|
11 |
onSelect,
|
12 |
selectedId,
|
13 |
}: {
|
14 |
+
items: MediaInfo[]
|
15 |
|
16 |
/**
|
17 |
* Layout mode
|
|
|
31 |
|
32 |
className?: string
|
33 |
|
34 |
+
onSelect?: (media: MediaInfo) => void
|
35 |
|
36 |
selectedId?: string
|
37 |
}) {
|
src/app/interface/{video-player → media-player}/cartesian.tsx
RENAMED
@@ -4,25 +4,37 @@ import { Player } from "react-tuby"
|
|
4 |
import "react-tuby/css/main.css"
|
5 |
|
6 |
import { cn } from "@/lib/utils"
|
7 |
-
import {
|
8 |
|
9 |
export function CartesianVideoPlayer({
|
10 |
-
|
11 |
enableShortcuts = true,
|
12 |
className = "",
|
13 |
// currentTime,
|
14 |
}: {
|
15 |
-
|
16 |
enableShortcuts?: boolean
|
17 |
className?: string
|
18 |
// currentTime?: number
|
19 |
}) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
return (
|
21 |
<div className={cn(
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
className
|
26 |
)}>
|
27 |
<div className={cn(
|
28 |
`w-[calc(100%+16px)]`,
|
@@ -36,7 +48,10 @@ export function CartesianVideoPlayer({
|
|
36 |
src={[
|
37 |
{
|
38 |
quality: "Full HD",
|
39 |
-
|
|
|
|
|
|
|
40 |
}
|
41 |
]}
|
42 |
|
@@ -44,7 +59,7 @@ export function CartesianVideoPlayer({
|
|
44 |
|
45 |
subtitles={[]}
|
46 |
poster={
|
47 |
-
`https://huggingface.co/datasets/jbilcke-hf/ai-tube-index/resolve/main/videos/${
|
48 |
}
|
49 |
/>
|
50 |
</div>
|
|
|
4 |
import "react-tuby/css/main.css"
|
5 |
|
6 |
import { cn } from "@/lib/utils"
|
7 |
+
import { MediaInfo } from "@/types/general"
|
8 |
|
9 |
export function CartesianVideoPlayer({
|
10 |
+
media,
|
11 |
enableShortcuts = true,
|
12 |
className = "",
|
13 |
// currentTime,
|
14 |
}: {
|
15 |
+
media: MediaInfo
|
16 |
enableShortcuts?: boolean
|
17 |
className?: string
|
18 |
// currentTime?: number
|
19 |
}) {
|
20 |
+
|
21 |
+
const assetUrl = media.assetUrlHd || media.assetUrl
|
22 |
+
|
23 |
+
if (!assetUrl) {
|
24 |
+
return (
|
25 |
+
<div className={cn(
|
26 |
+
`w-full`,
|
27 |
+
`flex flex-col items-center justify-center`,
|
28 |
+
className
|
29 |
+
)}>
|
30 |
+
</div>
|
31 |
+
)}
|
32 |
+
|
33 |
return (
|
34 |
<div className={cn(
|
35 |
+
`w-full`,
|
36 |
+
`flex flex-col items-center justify-center`,
|
37 |
+
className
|
|
|
38 |
)}>
|
39 |
<div className={cn(
|
40 |
`w-[calc(100%+16px)]`,
|
|
|
48 |
src={[
|
49 |
{
|
50 |
quality: "Full HD",
|
51 |
+
|
52 |
+
// TODO: separate the media asset URLs into separate source channels,
|
53 |
+
// one for each resolution
|
54 |
+
url: media.assetUrlHd || media.assetUrl,
|
55 |
}
|
56 |
]}
|
57 |
|
|
|
59 |
|
60 |
subtitles={[]}
|
61 |
poster={
|
62 |
+
`https://huggingface.co/datasets/jbilcke-hf/ai-tube-index/resolve/main/videos/${media.id}.webp`
|
63 |
}
|
64 |
/>
|
65 |
</div>
|
src/app/interface/{video-player → media-player}/equirectangular.tsx
RENAMED
@@ -1,23 +1,30 @@
|
|
1 |
"use client"
|
2 |
|
3 |
-
import { useEffect, useRef
|
4 |
|
5 |
-
import {
|
6 |
-
import { EquirectangularVideoAdapter, LensflarePlugin, ReactPhotoSphereViewer, ResolutionPlugin, SettingsPlugin, VideoPlugin } from "react-photo-sphere-viewer"
|
7 |
|
8 |
-
import {
|
9 |
-
import { VideoInfo } from "@/types/general"
|
10 |
|
11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
|
13 |
export function EquirectangularVideoPlayer({
|
14 |
-
|
15 |
className = "",
|
16 |
width,
|
17 |
height,
|
18 |
muted = false,
|
19 |
}: {
|
20 |
-
|
21 |
className?: string
|
22 |
width: number
|
23 |
height: number
|
@@ -37,7 +44,9 @@ export function EquirectangularVideoPlayer({
|
|
37 |
})
|
38 |
}, [width, height])
|
39 |
|
40 |
-
|
|
|
|
|
41 |
|
42 |
return (
|
43 |
<div
|
@@ -47,10 +56,7 @@ export function EquirectangularVideoPlayer({
|
|
47 |
<ReactPhotoSphereViewer
|
48 |
|
49 |
container=""
|
50 |
-
containerClass={
|
51 |
-
"rounded-xl overflow-hidden",
|
52 |
-
className
|
53 |
-
)}
|
54 |
|
55 |
width={`${width}px`}
|
56 |
height={`${height}px`}
|
@@ -67,19 +73,20 @@ export function EquirectangularVideoPlayer({
|
|
67 |
navbar="video"
|
68 |
src=""
|
69 |
plugins={[
|
|
|
70 |
[VideoPlugin, {
|
71 |
muted,
|
72 |
// progressbar: true,
|
73 |
bigbutton: false
|
74 |
}],
|
75 |
-
SettingsPlugin,
|
76 |
[ResolutionPlugin, {
|
77 |
defaultResolution: 'HD',
|
78 |
resolutions: [
|
79 |
{
|
80 |
id: 'HD',
|
81 |
label: 'Standard',
|
82 |
-
|
|
|
83 |
},
|
84 |
],
|
85 |
}],
|
|
|
1 |
"use client"
|
2 |
|
3 |
+
import { useEffect, useRef } from "react"
|
4 |
|
5 |
+
import { ReactPhotoSphereViewer } from "react-photo-sphere-viewer"
|
|
|
6 |
|
7 |
+
import { Viewer } from "@photo-sphere-viewer/core"
|
|
|
8 |
|
9 |
+
import { EquirectangularVideoAdapter } from "@photo-sphere-viewer/equirectangular-video-adapter"
|
10 |
+
|
11 |
+
import { SettingsPlugin } from "@photo-sphere-viewer/settings-plugin"
|
12 |
+
import { ResolutionPlugin } from "@photo-sphere-viewer/resolution-plugin"
|
13 |
+
import { VideoPlugin } from "@photo-sphere-viewer/video-plugin"
|
14 |
+
|
15 |
+
import "@photo-sphere-viewer/settings-plugin/index.css"
|
16 |
+
import "@photo-sphere-viewer/video-plugin/index.css"
|
17 |
+
|
18 |
+
import { MediaInfo } from "@/types/general"
|
19 |
|
20 |
export function EquirectangularVideoPlayer({
|
21 |
+
media,
|
22 |
className = "",
|
23 |
width,
|
24 |
height,
|
25 |
muted = false,
|
26 |
}: {
|
27 |
+
media: MediaInfo
|
28 |
className?: string
|
29 |
width: number
|
30 |
height: number
|
|
|
44 |
})
|
45 |
}, [width, height])
|
46 |
|
47 |
+
const assetUrl = media.assetUrlHd || media.assetUrl
|
48 |
+
|
49 |
+
if (!assetUrl) { return null }
|
50 |
|
51 |
return (
|
52 |
<div
|
|
|
56 |
<ReactPhotoSphereViewer
|
57 |
|
58 |
container=""
|
59 |
+
containerClass={className}
|
|
|
|
|
|
|
60 |
|
61 |
width={`${width}px`}
|
62 |
height={`${height}px`}
|
|
|
73 |
navbar="video"
|
74 |
src=""
|
75 |
plugins={[
|
76 |
+
[SettingsPlugin, {}],
|
77 |
[VideoPlugin, {
|
78 |
muted,
|
79 |
// progressbar: true,
|
80 |
bigbutton: false
|
81 |
}],
|
|
|
82 |
[ResolutionPlugin, {
|
83 |
defaultResolution: 'HD',
|
84 |
resolutions: [
|
85 |
{
|
86 |
id: 'HD',
|
87 |
label: 'Standard',
|
88 |
+
// TODO: separate the resolutions
|
89 |
+
panorama: { source: media.assetUrlHd || media.assetUrl },
|
90 |
},
|
91 |
],
|
92 |
}],
|
src/app/interface/media-player/gaussian.tsx
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use client"
|
2 |
+
|
3 |
+
import "react-tuby/css/main.css"
|
4 |
+
|
5 |
+
import { MediaInfo } from "@/types/general"
|
6 |
+
|
7 |
+
import { Gsplat } from "../gsplat"
|
8 |
+
|
9 |
+
export function GaussianSplattingPlayer({
|
10 |
+
media,
|
11 |
+
width,
|
12 |
+
height,
|
13 |
+
enableShortcuts = true,
|
14 |
+
className = "",
|
15 |
+
// currentTime,
|
16 |
+
}: {
|
17 |
+
media: MediaInfo
|
18 |
+
width?: number
|
19 |
+
height?: number
|
20 |
+
enableShortcuts?: boolean
|
21 |
+
className?: string
|
22 |
+
// currentTime?: number
|
23 |
+
}) {
|
24 |
+
return (
|
25 |
+
<Gsplat
|
26 |
+
url={media.assetUrlHd || media.assetUrl}
|
27 |
+
width={width}
|
28 |
+
height={height}
|
29 |
+
className={className}
|
30 |
+
/>
|
31 |
+
)
|
32 |
+
}
|
src/app/interface/{video-player → media-player}/index.tsx
RENAMED
@@ -3,43 +3,57 @@
|
|
3 |
import AutoSizer from "react-virtualized-auto-sizer"
|
4 |
|
5 |
import { cn } from "@/lib/utils"
|
6 |
-
import {
|
7 |
-
import {
|
8 |
|
9 |
import { EquirectangularVideoPlayer } from "./equirectangular"
|
10 |
import { CartesianVideoPlayer } from "./cartesian"
|
|
|
11 |
|
12 |
-
export function
|
13 |
-
|
14 |
enableShortcuts = true,
|
15 |
className = "",
|
16 |
// currentTime,
|
17 |
}: {
|
18 |
-
|
19 |
enableShortcuts?: boolean
|
20 |
className?: string
|
21 |
// currentTime?: number
|
22 |
}) {
|
23 |
-
|
24 |
-
if (!
|
25 |
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
30 |
|
31 |
-
if (
|
32 |
// note: for AutoSizer to work properly it needs to be inside a normal div with no display: "flex"
|
33 |
return (
|
34 |
<div className={cn(`w-full aspect-video`, className)}>
|
35 |
<AutoSizer>{({ height, width }) => (
|
36 |
-
<EquirectangularVideoPlayer
|
37 |
)}</AutoSizer>
|
38 |
</div>
|
39 |
)
|
40 |
}
|
41 |
|
42 |
return (
|
43 |
-
<CartesianVideoPlayer
|
44 |
)
|
45 |
}
|
|
|
3 |
import AutoSizer from "react-virtualized-auto-sizer"
|
4 |
|
5 |
import { cn } from "@/lib/utils"
|
6 |
+
import { MediaInfo } from "@/types/general"
|
7 |
+
import { parseMediaProjectionType } from "@/lib/parseMediaProjectionType"
|
8 |
|
9 |
import { EquirectangularVideoPlayer } from "./equirectangular"
|
10 |
import { CartesianVideoPlayer } from "./cartesian"
|
11 |
+
import { GaussianSplattingPlayer } from "./gaussian"
|
12 |
|
13 |
+
export function MediaPlayer({
|
14 |
+
media,
|
15 |
enableShortcuts = true,
|
16 |
className = "",
|
17 |
// currentTime,
|
18 |
}: {
|
19 |
+
media?: MediaInfo
|
20 |
enableShortcuts?: boolean
|
21 |
className?: string
|
22 |
// currentTime?: number
|
23 |
}) {
|
24 |
+
console.log("MediaPlayer called for " + media?.assetUrl)
|
25 |
+
if (!media || !media?.assetUrl) { return null }
|
26 |
|
27 |
+
// uncomment one of those to forcefully test the .splatv player!
|
28 |
+
// media.assetUrlHd = "https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/4d/flame/flame.splatv"
|
29 |
+
// media.assetUrlHd = "https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/4d/sear/sear.splatv"
|
30 |
+
// media.assetUrlHd = "https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/4d/birthday/birthday.splatv"
|
31 |
+
|
32 |
+
const projectionType = parseMediaProjectionType(media)
|
33 |
+
|
34 |
+
if (projectionType === "gaussian") {
|
35 |
+
// note: for AutoSizer to work properly it needs to be inside a normal div with no display: "flex"
|
36 |
+
return (
|
37 |
+
<div className={cn(`w-full aspect-video`, className)}>
|
38 |
+
<AutoSizer>{({ height, width }) => (
|
39 |
+
<GaussianSplattingPlayer media={media} className={className} width={width} height={height} />
|
40 |
+
)}</AutoSizer>
|
41 |
+
</div>
|
42 |
+
)
|
43 |
+
}
|
44 |
|
45 |
+
if (projectionType === "equirectangular") {
|
46 |
// note: for AutoSizer to work properly it needs to be inside a normal div with no display: "flex"
|
47 |
return (
|
48 |
<div className={cn(`w-full aspect-video`, className)}>
|
49 |
<AutoSizer>{({ height, width }) => (
|
50 |
+
<EquirectangularVideoPlayer media={media} className={className} width={width} height={height} />
|
51 |
)}</AutoSizer>
|
52 |
</div>
|
53 |
)
|
54 |
}
|
55 |
|
56 |
return (
|
57 |
+
<CartesianVideoPlayer media={media} enableShortcuts={enableShortcuts} className={className} />
|
58 |
)
|
59 |
}
|
src/app/interface/pending-video-card/index.tsx
CHANGED
@@ -3,7 +3,7 @@ import { PiTrashBold } from "react-icons/pi"
|
|
3 |
import { TableCell, TableRow } from "@/components/ui/table"
|
4 |
import { cn } from "@/lib/utils"
|
5 |
import { MdLockClock } from "react-icons/md"
|
6 |
-
import {
|
7 |
import { truncate } from "./truncate"
|
8 |
|
9 |
export function PendingVideoCard({
|
@@ -11,8 +11,8 @@ export function PendingVideoCard({
|
|
11 |
onDelete,
|
12 |
className = "",
|
13 |
}: {
|
14 |
-
video:
|
15 |
-
onDelete?: (video:
|
16 |
className?: string
|
17 |
}) {
|
18 |
|
|
|
3 |
import { TableCell, TableRow } from "@/components/ui/table"
|
4 |
import { cn } from "@/lib/utils"
|
5 |
import { MdLockClock } from "react-icons/md"
|
6 |
+
import { MediaInfo } from "@/types/general"
|
7 |
import { truncate } from "./truncate"
|
8 |
|
9 |
export function PendingVideoCard({
|
|
|
11 |
onDelete,
|
12 |
className = "",
|
13 |
}: {
|
14 |
+
video: MediaInfo
|
15 |
+
onDelete?: (video: MediaInfo) => void
|
16 |
className?: string
|
17 |
}) {
|
18 |
|
src/app/interface/pending-video-list/index.tsx
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
import { cn } from "@/lib/utils"
|
2 |
-
import {
|
3 |
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
4 |
|
5 |
import { PendingVideoCard } from "../pending-video-card"
|
@@ -9,8 +9,8 @@ export function PendingVideoList({
|
|
9 |
onDelete,
|
10 |
className = "",
|
11 |
}: {
|
12 |
-
videos:
|
13 |
-
onDelete?: (video:
|
14 |
className?: string
|
15 |
}) {
|
16 |
return (
|
|
|
1 |
import { cn } from "@/lib/utils"
|
2 |
+
import { MediaInfo } from "@/types/general"
|
3 |
import { Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
4 |
|
5 |
import { PendingVideoCard } from "../pending-video-card"
|
|
|
9 |
onDelete,
|
10 |
className = "",
|
11 |
}: {
|
12 |
+
videos: MediaInfo[]
|
13 |
+
onDelete?: (video: MediaInfo) => void
|
14 |
className?: string
|
15 |
}) {
|
16 |
return (
|
src/app/interface/playlist-control/index.tsx
CHANGED
@@ -3,7 +3,7 @@ import { IoIosPause } from "react-icons/io"
|
|
3 |
|
4 |
import { cn } from "@/lib/utils"
|
5 |
import { usePlaylist } from "@/lib/usePlaylist"
|
6 |
-
import {
|
7 |
|
8 |
export function PlaylistControl() {
|
9 |
const playlist = usePlaylist()
|
|
|
3 |
|
4 |
import { cn } from "@/lib/utils"
|
5 |
import { usePlaylist } from "@/lib/usePlaylist"
|
6 |
+
import { MediaInfo } from "@/types/general"
|
7 |
|
8 |
export function PlaylistControl() {
|
9 |
const playlist = usePlaylist()
|
src/app/interface/recommended-videos/index.tsx
CHANGED
@@ -2,17 +2,16 @@
|
|
2 |
import { useEffect, useTransition } from "react"
|
3 |
|
4 |
import { useStore } from "@/app/state/useStore"
|
5 |
-
import {
|
6 |
-
import { VideoInfo } from "@/types/general"
|
7 |
|
8 |
import { VideoList } from "../video-list"
|
9 |
import { getVideos } from "@/app/server/actions/ai-tube-hf/getVideos"
|
10 |
|
11 |
export function RecommendedVideos({
|
12 |
-
|
13 |
}: {
|
14 |
-
// the
|
15 |
-
|
16 |
}) {
|
17 |
const [_isPending, startTransition] = useTransition()
|
18 |
const setRecommendedVideos = useStore(s => s.setRecommendedVideos)
|
@@ -22,12 +21,12 @@ export function RecommendedVideos({
|
|
22 |
startTransition(async () => {
|
23 |
setRecommendedVideos(await getVideos({
|
24 |
sortBy: "random",
|
25 |
-
niceToHaveTags:
|
26 |
-
ignoreVideoIds: [
|
27 |
maxVideos: 16,
|
28 |
}))
|
29 |
})
|
30 |
-
},
|
31 |
|
32 |
return (
|
33 |
<VideoList
|
|
|
2 |
import { useEffect, useTransition } from "react"
|
3 |
|
4 |
import { useStore } from "@/app/state/useStore"
|
5 |
+
import { MediaInfo } from "@/types/general"
|
|
|
6 |
|
7 |
import { VideoList } from "../video-list"
|
8 |
import { getVideos } from "@/app/server/actions/ai-tube-hf/getVideos"
|
9 |
|
10 |
export function RecommendedVideos({
|
11 |
+
media,
|
12 |
}: {
|
13 |
+
// the media to use for the recommendations
|
14 |
+
media: MediaInfo
|
15 |
}) {
|
16 |
const [_isPending, startTransition] = useTransition()
|
17 |
const setRecommendedVideos = useStore(s => s.setRecommendedVideos)
|
|
|
21 |
startTransition(async () => {
|
22 |
setRecommendedVideos(await getVideos({
|
23 |
sortBy: "random",
|
24 |
+
niceToHaveTags: media.tags,
|
25 |
+
ignoreVideoIds: [media.id],
|
26 |
maxVideos: 16,
|
27 |
}))
|
28 |
})
|
29 |
+
}, media.tags)
|
30 |
|
31 |
return (
|
32 |
<VideoList
|
src/app/interface/search-input/index.tsx
CHANGED
@@ -126,7 +126,7 @@ export function SearchInput() {
|
|
126 |
>
|
127 |
{searchAutocompleteResults.length === 0 ? <div>Nothing to show, type something and press enter</div> : null}
|
128 |
{searchAutocompleteResults.map(media => (
|
129 |
-
<Link key={media.id} href={
|
130 |
<div
|
131 |
className={cn(
|
132 |
`dark:hover:bg-neutral-800 hover:bg-neutral-800`,
|
|
|
126 |
>
|
127 |
{searchAutocompleteResults.length === 0 ? <div>Nothing to show, type something and press enter</div> : null}
|
128 |
{searchAutocompleteResults.map(media => (
|
129 |
+
<Link key={media.id} href={`${process.env.NEXT_PUBLIC_DOMAIN}/watch?v=${media.id}`}>
|
130 |
<div
|
131 |
className={cn(
|
132 |
`dark:hover:bg-neutral-800 hover:bg-neutral-800`,
|
src/app/interface/track-card/index.tsx
CHANGED
@@ -5,7 +5,7 @@ import Link from "next/link"
|
|
5 |
import { RiCheckboxCircleFill } from "react-icons/ri"
|
6 |
|
7 |
import { cn } from "@/lib/utils"
|
8 |
-
import { MediaDisplayLayout,
|
9 |
import { formatDuration } from "@/lib/formatDuration"
|
10 |
import { formatTimeAgo } from "@/lib/formatTimeAgo"
|
11 |
import { isCertifiedUser } from "@/app/certification"
|
@@ -22,10 +22,10 @@ export function TrackCard({
|
|
22 |
selected,
|
23 |
index
|
24 |
}: {
|
25 |
-
media:
|
26 |
className?: string
|
27 |
layout?: MediaDisplayLayout
|
28 |
-
onSelect?: (media:
|
29 |
selected?: boolean
|
30 |
index: number
|
31 |
}) {
|
@@ -169,6 +169,7 @@ export function TrackCard({
|
|
169 |
`flex flex-col items-center justify-center text-center`,
|
170 |
`bg-neutral-900 rounded`,
|
171 |
`text-2xs font-semibold px-[3px] py-[1px]`,
|
|
|
172 |
)}
|
173 |
>{formatDuration(duration)}</div>
|
174 |
</div>
|
|
|
5 |
import { RiCheckboxCircleFill } from "react-icons/ri"
|
6 |
|
7 |
import { cn } from "@/lib/utils"
|
8 |
+
import { MediaDisplayLayout, MediaInfo } from "@/types/general"
|
9 |
import { formatDuration } from "@/lib/formatDuration"
|
10 |
import { formatTimeAgo } from "@/lib/formatTimeAgo"
|
11 |
import { isCertifiedUser } from "@/app/certification"
|
|
|
22 |
selected,
|
23 |
index
|
24 |
}: {
|
25 |
+
media: MediaInfo
|
26 |
className?: string
|
27 |
layout?: MediaDisplayLayout
|
28 |
+
onSelect?: (media: MediaInfo) => void
|
29 |
selected?: boolean
|
30 |
index: number
|
31 |
}) {
|
|
|
169 |
`flex flex-col items-center justify-center text-center`,
|
170 |
`bg-neutral-900 rounded`,
|
171 |
`text-2xs font-semibold px-[3px] py-[1px]`,
|
172 |
+
isFinite(duration) && !isNaN(duration) && duration > 0 ? 'opacity-100' : 'opacity-0'
|
173 |
)}
|
174 |
>{formatDuration(duration)}</div>
|
175 |
</div>
|
src/app/interface/video-card/index.tsx
CHANGED
@@ -5,7 +5,7 @@ import Link from "next/link"
|
|
5 |
import { RiCheckboxCircleFill } from "react-icons/ri"
|
6 |
|
7 |
import { cn } from "@/lib/utils"
|
8 |
-
import { MediaDisplayLayout,
|
9 |
import { formatDuration } from "@/lib/formatDuration"
|
10 |
import { formatTimeAgo } from "@/lib/formatTimeAgo"
|
11 |
import { isCertifiedUser } from "@/app/certification"
|
@@ -21,10 +21,10 @@ export function VideoCard({
|
|
21 |
selected,
|
22 |
index
|
23 |
}: {
|
24 |
-
media:
|
25 |
className?: string
|
26 |
layout?: MediaDisplayLayout
|
27 |
-
onSelect?: (media:
|
28 |
selected?: boolean
|
29 |
index: number
|
30 |
}) {
|
@@ -75,7 +75,7 @@ export function VideoCard({
|
|
75 |
}, [index])
|
76 |
|
77 |
return (
|
78 |
-
<Link href={
|
79 |
<div
|
80 |
className={cn(
|
81 |
`w-full flex`,
|
@@ -163,6 +163,7 @@ export function VideoCard({
|
|
163 |
`flex flex-col items-center justify-center text-center`,
|
164 |
`bg-neutral-900 rounded`,
|
165 |
`text-2xs font-semibold px-[3px] py-[1px]`,
|
|
|
166 |
)}
|
167 |
>{formatDuration(duration)}</div>
|
168 |
</div>
|
|
|
5 |
import { RiCheckboxCircleFill } from "react-icons/ri"
|
6 |
|
7 |
import { cn } from "@/lib/utils"
|
8 |
+
import { MediaDisplayLayout, MediaInfo } from "@/types/general"
|
9 |
import { formatDuration } from "@/lib/formatDuration"
|
10 |
import { formatTimeAgo } from "@/lib/formatTimeAgo"
|
11 |
import { isCertifiedUser } from "@/app/certification"
|
|
|
21 |
selected,
|
22 |
index
|
23 |
}: {
|
24 |
+
media: MediaInfo
|
25 |
className?: string
|
26 |
layout?: MediaDisplayLayout
|
27 |
+
onSelect?: (media: MediaInfo) => void
|
28 |
selected?: boolean
|
29 |
index: number
|
30 |
}) {
|
|
|
75 |
}, [index])
|
76 |
|
77 |
return (
|
78 |
+
<Link href={`${process.env.NEXT_PUBLIC_DOMAIN}/watch?v=${media.id}`}>
|
79 |
<div
|
80 |
className={cn(
|
81 |
`w-full flex`,
|
|
|
163 |
`flex flex-col items-center justify-center text-center`,
|
164 |
`bg-neutral-900 rounded`,
|
165 |
`text-2xs font-semibold px-[3px] py-[1px]`,
|
166 |
+
isFinite(duration) && !isNaN(duration) && duration > 0 ? 'opacity-100' : 'opacity-0'
|
167 |
)}
|
168 |
>{formatDuration(duration)}</div>
|
169 |
</div>
|
src/app/layout.tsx
CHANGED
@@ -14,8 +14,8 @@ const roboto = Roboto({
|
|
14 |
})
|
15 |
|
16 |
export const metadata: Metadata = {
|
17 |
-
title: '🍿
|
18 |
-
description: '🍿
|
19 |
}
|
20 |
|
21 |
export default function RootLayout({
|
|
|
14 |
})
|
15 |
|
16 |
export const metadata: Metadata = {
|
17 |
+
title: '🍿 AiTube',
|
18 |
+
description: '🍿 AiTube',
|
19 |
}
|
20 |
|
21 |
export default function RootLayout({
|
src/app/main.tsx
CHANGED
@@ -8,7 +8,7 @@ import { UserChannelView } from "./views/user-channel-view"
|
|
8 |
import { PublicVideoView } from "./views/public-video-view"
|
9 |
import { UserAccountView } from "./views/user-account-view"
|
10 |
import { NotFoundView } from "./views/not-found-view"
|
11 |
-
import { ChannelInfo,
|
12 |
import { useEffect } from "react"
|
13 |
import { usePathname, useRouter } from "next/navigation"
|
14 |
import { TubeLayout } from "./interface/tube-layout"
|
@@ -35,11 +35,11 @@ export function Main({
|
|
35 |
}: {
|
36 |
// server side params
|
37 |
// view?: InterfaceView
|
38 |
-
publicVideo?:
|
39 |
-
publicVideos?:
|
40 |
-
publicChannelVideos?:
|
41 |
-
publicTracks?:
|
42 |
-
publicTrack?:
|
43 |
channel?: ChannelInfo
|
44 |
}) {
|
45 |
// this could be also a parameter of main, where we pass this manually
|
|
|
8 |
import { PublicVideoView } from "./views/public-video-view"
|
9 |
import { UserAccountView } from "./views/user-account-view"
|
10 |
import { NotFoundView } from "./views/not-found-view"
|
11 |
+
import { ChannelInfo, MediaInfo } from "@/types/general"
|
12 |
import { useEffect } from "react"
|
13 |
import { usePathname, useRouter } from "next/navigation"
|
14 |
import { TubeLayout } from "./interface/tube-layout"
|
|
|
35 |
}: {
|
36 |
// server side params
|
37 |
// view?: InterfaceView
|
38 |
+
publicVideo?: MediaInfo
|
39 |
+
publicVideos?: MediaInfo[]
|
40 |
+
publicChannelVideos?: MediaInfo[]
|
41 |
+
publicTracks?: MediaInfo[]
|
42 |
+
publicTrack?: MediaInfo
|
43 |
channel?: ChannelInfo
|
44 |
}) {
|
45 |
// this could be also a parameter of main, where we pass this manually
|
src/app/page.tsx
CHANGED
@@ -16,14 +16,14 @@ export async function generateMetadata(
|
|
16 |
|
17 |
if (!videoId) {
|
18 |
return {
|
19 |
-
title: `🍿
|
20 |
metadataBase,
|
21 |
openGraph: {
|
22 |
type: "website",
|
23 |
// url: "https://example.com",
|
24 |
-
title: "
|
25 |
description: "The first fully AI generated video platform",
|
26 |
-
siteName: "🍿
|
27 |
|
28 |
videos: [],
|
29 |
images: [],
|
@@ -39,14 +39,14 @@ export async function generateMetadata(
|
|
39 |
}
|
40 |
|
41 |
return {
|
42 |
-
title: `${video.label} -
|
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: "
|
50 |
|
51 |
videos: [
|
52 |
{
|
@@ -58,14 +58,14 @@ export async function generateMetadata(
|
|
58 |
}
|
59 |
} catch (err) {
|
60 |
return {
|
61 |
-
title: "
|
62 |
metadataBase,
|
63 |
openGraph: {
|
64 |
type: "website",
|
65 |
// url: "https://example.com",
|
66 |
-
title: "
|
67 |
description: "", // put the vide description here
|
68 |
-
siteName: "
|
69 |
|
70 |
videos: [],
|
71 |
images: [],
|
|
|
16 |
|
17 |
if (!videoId) {
|
18 |
return {
|
19 |
+
title: `🍿 AiTube`,
|
20 |
metadataBase,
|
21 |
openGraph: {
|
22 |
type: "website",
|
23 |
// url: "https://example.com",
|
24 |
+
title: "AiTube",
|
25 |
description: "The first fully AI generated video platform",
|
26 |
+
siteName: "🍿 AiTube",
|
27 |
|
28 |
videos: [],
|
29 |
images: [],
|
|
|
39 |
}
|
40 |
|
41 |
return {
|
42 |
+
title: `${video.label} - AiTube`,
|
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: "AiTube",
|
50 |
|
51 |
videos: [
|
52 |
{
|
|
|
58 |
}
|
59 |
} catch (err) {
|
60 |
return {
|
61 |
+
title: "AiTube",
|
62 |
metadataBase,
|
63 |
openGraph: {
|
64 |
type: "website",
|
65 |
// url: "https://example.com",
|
66 |
+
title: "AiTube", // put the video title here
|
67 |
description: "", // put the vide description here
|
68 |
+
siteName: "AiTube",
|
69 |
|
70 |
videos: [],
|
71 |
images: [],
|
src/app/server/actions/ai-tube-hf/deleteVideoRequest.ts
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
|
2 |
-
import {
|
3 |
|
4 |
import { deleteFileFromDataset } from "./deleteFileFromDataset"
|
5 |
import { formatPromptFileName } from "../utils/formatPromptFileName"
|
@@ -9,7 +9,7 @@ export async function deleteVideoRequest({
|
|
9 |
apiKey,
|
10 |
neverThrow,
|
11 |
}: {
|
12 |
-
video:
|
13 |
apiKey: string
|
14 |
neverThrow?: boolean
|
15 |
}): Promise<boolean> {
|
|
|
1 |
|
2 |
+
import { MediaInfo } from "@/types/general"
|
3 |
|
4 |
import { deleteFileFromDataset } from "./deleteFileFromDataset"
|
5 |
import { formatPromptFileName } from "../utils/formatPromptFileName"
|
|
|
9 |
apiKey,
|
10 |
neverThrow,
|
11 |
}: {
|
12 |
+
video: MediaInfo
|
13 |
apiKey: string
|
14 |
neverThrow?: boolean
|
15 |
}): Promise<boolean> {
|
src/app/server/actions/ai-tube-hf/downloadClapProject.ts
CHANGED
@@ -2,7 +2,7 @@ import { v4 as uuidv4 } from "uuid"
|
|
2 |
import { Credentials } from "@/huggingface/hub/src"
|
3 |
|
4 |
import { ClapProject } from "@/clap/types"
|
5 |
-
import { ChannelInfo,
|
6 |
import { defaultVideoModel } from "@/app/config"
|
7 |
import { parseClap } from "@/clap/parseClap"
|
8 |
|
@@ -21,7 +21,7 @@ export async function downloadClapProject({
|
|
21 |
credentials: Credentials
|
22 |
}): Promise<{
|
23 |
videoRequest: VideoRequest
|
24 |
-
videoInfo:
|
25 |
clapProject: ClapProject
|
26 |
}> {
|
27 |
// we recover the repo from the cnannel info
|
@@ -62,7 +62,7 @@ export async function downloadClapProject({
|
|
62 |
}),
|
63 |
}
|
64 |
|
65 |
-
const videoInfo:
|
66 |
id,
|
67 |
status: "submitted",
|
68 |
label: videoRequest.label || "",
|
|
|
2 |
import { Credentials } from "@/huggingface/hub/src"
|
3 |
|
4 |
import { ClapProject } from "@/clap/types"
|
5 |
+
import { ChannelInfo, MediaInfo, VideoRequest } from "@/types/general"
|
6 |
import { defaultVideoModel } from "@/app/config"
|
7 |
import { parseClap } from "@/clap/parseClap"
|
8 |
|
|
|
21 |
credentials: Credentials
|
22 |
}): Promise<{
|
23 |
videoRequest: VideoRequest
|
24 |
+
videoInfo: MediaInfo
|
25 |
clapProject: ClapProject
|
26 |
}> {
|
27 |
// we recover the repo from the cnannel info
|
|
|
62 |
}),
|
63 |
}
|
64 |
|
65 |
+
const videoInfo: MediaInfo = {
|
66 |
id,
|
67 |
status: "submitted",
|
68 |
label: videoRequest.label || "",
|
src/app/server/actions/ai-tube-hf/extendVideosWithStats.ts
CHANGED
@@ -1,12 +1,12 @@
|
|
1 |
"use server"
|
2 |
|
3 |
-
import {
|
4 |
|
5 |
-
import {
|
6 |
|
7 |
-
export async function extendVideosWithStats(videos:
|
8 |
|
9 |
-
const allStats = await
|
10 |
|
11 |
return videos.map(v => {
|
12 |
const stats = allStats[v.id] || {
|
|
|
1 |
"use server"
|
2 |
|
3 |
+
import { MediaInfo } from "@/types/general"
|
4 |
|
5 |
+
import { getStatsForMedias } from "../stats"
|
6 |
|
7 |
+
export async function extendVideosWithStats(videos: MediaInfo[]): Promise<MediaInfo[]> {
|
8 |
|
9 |
+
const allStats = await getStatsForMedias(videos.map(v => v.id))
|
10 |
|
11 |
return videos.map(v => {
|
12 |
const stats = allStats[v.id] || {
|
src/app/server/actions/ai-tube-hf/getChannelVideos.ts
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
"use server"
|
2 |
|
3 |
-
import { ChannelInfo,
|
4 |
|
5 |
import { getVideoRequestsFromChannel } from "./getVideoRequestsFromChannel"
|
6 |
import { adminApiKey } from "../config"
|
@@ -21,7 +21,7 @@ export async function getChannelVideos({
|
|
21 |
status?: VideoStatus
|
22 |
|
23 |
neverThrow?: boolean
|
24 |
-
}): Promise<
|
25 |
|
26 |
if (!channel) { return [] }
|
27 |
|
@@ -38,7 +38,7 @@ export async function getChannelVideos({
|
|
38 |
const published = await getVideoIndex({ status: "published" })
|
39 |
|
40 |
const validVideos = videos.map(v => {
|
41 |
-
let video:
|
42 |
id: v.id,
|
43 |
status: "submitted",
|
44 |
label: v.label || "",
|
|
|
1 |
"use server"
|
2 |
|
3 |
+
import { ChannelInfo, MediaInfo, VideoStatus } from "@/types/general"
|
4 |
|
5 |
import { getVideoRequestsFromChannel } from "./getVideoRequestsFromChannel"
|
6 |
import { adminApiKey } from "../config"
|
|
|
21 |
status?: VideoStatus
|
22 |
|
23 |
neverThrow?: boolean
|
24 |
+
}): Promise<MediaInfo[]> {
|
25 |
|
26 |
if (!channel) { return [] }
|
27 |
|
|
|
38 |
const published = await getVideoIndex({ status: "published" })
|
39 |
|
40 |
const validVideos = videos.map(v => {
|
41 |
+
let video: MediaInfo = {
|
42 |
id: v.id,
|
43 |
status: "submitted",
|
44 |
label: v.label || "",
|
src/app/server/actions/ai-tube-hf/getVideo.ts
CHANGED
@@ -1,9 +1,9 @@
|
|
1 |
"use server"
|
2 |
|
3 |
-
import {
|
4 |
|
5 |
import { getVideoIndex } from "./getVideoIndex"
|
6 |
-
import {
|
7 |
|
8 |
export async function getVideo({
|
9 |
videoId,
|
@@ -11,7 +11,7 @@ export async function getVideo({
|
|
11 |
}: {
|
12 |
videoId?: string | string[] | null,
|
13 |
neverThrow?: boolean
|
14 |
-
}): Promise<
|
15 |
try {
|
16 |
const id = `${videoId || ""}`
|
17 |
|
@@ -26,7 +26,7 @@ export async function getVideo({
|
|
26 |
throw new Error(`cannot get the video, nothing found for id "${id}"`)
|
27 |
}
|
28 |
|
29 |
-
const allStats = await
|
30 |
|
31 |
const stats = allStats[video.id] || {
|
32 |
numberOfViews: 0,
|
|
|
1 |
"use server"
|
2 |
|
3 |
+
import { MediaInfo } from "@/types/general"
|
4 |
|
5 |
import { getVideoIndex } from "./getVideoIndex"
|
6 |
+
import { getStatsForMedias } from "../stats"
|
7 |
|
8 |
export async function getVideo({
|
9 |
videoId,
|
|
|
11 |
}: {
|
12 |
videoId?: string | string[] | null,
|
13 |
neverThrow?: boolean
|
14 |
+
}): Promise<MediaInfo | undefined> {
|
15 |
try {
|
16 |
const id = `${videoId || ""}`
|
17 |
|
|
|
26 |
throw new Error(`cannot get the video, nothing found for id "${id}"`)
|
27 |
}
|
28 |
|
29 |
+
const allStats = await getStatsForMedias([video.id])
|
30 |
|
31 |
const stats = allStats[video.id] || {
|
32 |
numberOfViews: 0,
|
src/app/server/actions/ai-tube-hf/getVideoIndex.ts
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
import {
|
2 |
|
3 |
import { adminUsername } from "../config"
|
4 |
|
@@ -11,7 +11,7 @@ export async function getVideoIndex({
|
|
11 |
|
12 |
renewCache?: boolean
|
13 |
neverThrow?: boolean
|
14 |
-
}): Promise<Record<string,
|
15 |
try {
|
16 |
const response = await fetch(
|
17 |
`https://huggingface.co/datasets/${adminUsername}/ai-tube-index/raw/main/${status}.json`
|
@@ -29,7 +29,7 @@ export async function getVideoIndex({
|
|
29 |
throw new Error("index is not an object, admin repair needed")
|
30 |
}
|
31 |
|
32 |
-
const videos = jsonResponse as Record<string,
|
33 |
|
34 |
return videos
|
35 |
} catch (err) {
|
|
|
1 |
+
import { MediaInfo, VideoStatus } from "@/types/general"
|
2 |
|
3 |
import { adminUsername } from "../config"
|
4 |
|
|
|
11 |
|
12 |
renewCache?: boolean
|
13 |
neverThrow?: boolean
|
14 |
+
}): Promise<Record<string, MediaInfo>> {
|
15 |
try {
|
16 |
const response = await fetch(
|
17 |
`https://huggingface.co/datasets/${adminUsername}/ai-tube-index/raw/main/${status}.json`
|
|
|
29 |
throw new Error("index is not an object, admin repair needed")
|
30 |
}
|
31 |
|
32 |
+
const videos = jsonResponse as Record<string, MediaInfo>
|
33 |
|
34 |
return videos
|
35 |
} catch (err) {
|
src/app/server/actions/ai-tube-hf/getVideos.ts
CHANGED
@@ -3,7 +3,7 @@
|
|
3 |
// import { distance } from "fastest-levenshtein"
|
4 |
import MiniSearch from "minisearch"
|
5 |
|
6 |
-
import {
|
7 |
|
8 |
import { getVideoIndex } from "./getVideoIndex"
|
9 |
import { extendVideosWithStats } from "./extendVideosWithStats"
|
@@ -47,7 +47,7 @@ export async function getVideos({
|
|
47 |
neverThrow?: boolean
|
48 |
|
49 |
renewCache?: boolean
|
50 |
-
}): Promise<
|
51 |
try {
|
52 |
// the index is gonna grow more and more,
|
53 |
// but in the future we will use some DB eg. Prisma or sqlite
|
@@ -96,7 +96,7 @@ export async function getVideos({
|
|
96 |
allPotentiallyValidVideos.sort(() => Math.random() - 0.5)
|
97 |
}
|
98 |
|
99 |
-
let videosMatchingFilters:
|
100 |
|
101 |
// filter videos by mandatory tags, or else we return everything
|
102 |
const mandatoryTagsList = mandatoryTags.map(tag => tag.toLowerCase().trim()).filter(tag => tag)
|
|
|
3 |
// import { distance } from "fastest-levenshtein"
|
4 |
import MiniSearch from "minisearch"
|
5 |
|
6 |
+
import { MediaInfo } from "@/types/general"
|
7 |
|
8 |
import { getVideoIndex } from "./getVideoIndex"
|
9 |
import { extendVideosWithStats } from "./extendVideosWithStats"
|
|
|
47 |
neverThrow?: boolean
|
48 |
|
49 |
renewCache?: boolean
|
50 |
+
}): Promise<MediaInfo[]> {
|
51 |
try {
|
52 |
// the index is gonna grow more and more,
|
53 |
// but in the future we will use some DB eg. Prisma or sqlite
|
|
|
96 |
allPotentiallyValidVideos.sort(() => Math.random() - 0.5)
|
97 |
}
|
98 |
|
99 |
+
let videosMatchingFilters: MediaInfo[] = allPotentiallyValidVideos
|
100 |
|
101 |
// filter videos by mandatory tags, or else we return everything
|
102 |
const mandatoryTagsList = mandatoryTags.map(tag => tag.toLowerCase().trim()).filter(tag => tag)
|
src/app/server/actions/ai-tube-hf/parseChannel.ts
CHANGED
@@ -54,7 +54,7 @@ export async function parseChannel(options: {
|
|
54 |
|
55 |
// ignore channels which don't start with ai-tube
|
56 |
if (!datasetName.startsWith(prefix)) {
|
57 |
-
throw new Error("this is not an
|
58 |
}
|
59 |
|
60 |
// ignore the video index
|
@@ -64,7 +64,7 @@ export async function parseChannel(options: {
|
|
64 |
|
65 |
const slug = datasetName.replaceAll(prefix, "")
|
66 |
|
67 |
-
// console.log(`found an
|
68 |
|
69 |
// TODO parse the README to get the proper label
|
70 |
let label = slug.replaceAll("-", " ")
|
|
|
54 |
|
55 |
// ignore channels which don't start with ai-tube
|
56 |
if (!datasetName.startsWith(prefix)) {
|
57 |
+
throw new Error("this is not an AiTube channel")
|
58 |
}
|
59 |
|
60 |
// ignore the video index
|
|
|
64 |
|
65 |
const slug = datasetName.replaceAll(prefix, "")
|
66 |
|
67 |
+
// console.log(`found an AiTube channel: "${slug}"`)
|
68 |
|
69 |
// TODO parse the README to get the proper label
|
70 |
let label = slug.replaceAll("-", " ")
|
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, VideoGenerationModel,
|
7 |
import { formatPromptFileName } from "../utils/formatPromptFileName"
|
8 |
import { computeOrientationProjectionWidthHeight } from "../utils/computeOrientationProjectionWidthHeight"
|
9 |
|
@@ -41,7 +41,7 @@ export async function uploadVideoRequestToDataset({
|
|
41 |
orientation: VideoOrientation
|
42 |
}): Promise<{
|
43 |
videoRequest: VideoRequest
|
44 |
-
videoInfo:
|
45 |
}> {
|
46 |
if (!apiKey) {
|
47 |
throw new Error(`the apiKey is required`)
|
@@ -127,7 +127,7 @@ ${prompt}
|
|
127 |
music,
|
128 |
thumbnailUrl: channel.thumbnail,
|
129 |
|
130 |
-
// for now
|
131 |
clapUrl: "",
|
132 |
|
133 |
updatedAt: new Date().toISOString(),
|
@@ -141,7 +141,7 @@ ${prompt}
|
|
141 |
}),
|
142 |
}
|
143 |
|
144 |
-
const newVideo:
|
145 |
id,
|
146 |
status: "submitted",
|
147 |
label: title,
|
@@ -154,7 +154,7 @@ ${prompt}
|
|
154 |
music,
|
155 |
thumbnailUrl: channel.thumbnail, // will be generated in async
|
156 |
|
157 |
-
// for now
|
158 |
clapUrl: "",
|
159 |
|
160 |
assetUrl: "", // will be generated in async
|
|
|
3 |
import { Blob } from "buffer"
|
4 |
|
5 |
import { Credentials, uploadFile, whoAmI } from "@/huggingface/hub/src"
|
6 |
+
import { ChannelInfo, VideoGenerationModel, MediaInfo, VideoOrientation, VideoRequest } from "@/types/general"
|
7 |
import { formatPromptFileName } from "../utils/formatPromptFileName"
|
8 |
import { computeOrientationProjectionWidthHeight } from "../utils/computeOrientationProjectionWidthHeight"
|
9 |
|
|
|
41 |
orientation: VideoOrientation
|
42 |
}): Promise<{
|
43 |
videoRequest: VideoRequest
|
44 |
+
videoInfo: MediaInfo
|
45 |
}> {
|
46 |
if (!apiKey) {
|
47 |
throw new Error(`the apiKey is required`)
|
|
|
127 |
music,
|
128 |
thumbnailUrl: channel.thumbnail,
|
129 |
|
130 |
+
// for now AiTube doesn't support upload of clap files
|
131 |
clapUrl: "",
|
132 |
|
133 |
updatedAt: new Date().toISOString(),
|
|
|
141 |
}),
|
142 |
}
|
143 |
|
144 |
+
const newVideo: MediaInfo = {
|
145 |
id,
|
146 |
status: "submitted",
|
147 |
label: title,
|
|
|
154 |
music,
|
155 |
thumbnailUrl: channel.thumbnail, // will be generated in async
|
156 |
|
157 |
+
// for now AiTube doesn't support upload of clap files
|
158 |
clapUrl: "",
|
159 |
|
160 |
assetUrl: "", // will be generated in async
|
src/app/server/actions/stats.ts
CHANGED
@@ -2,11 +2,11 @@
|
|
2 |
|
3 |
import { developerMode } from "@/app/config"
|
4 |
import { WhoAmIUser, whoAmI } from "@/huggingface/hub/src"
|
5 |
-
import {
|
6 |
import { redis } from "./redis";
|
7 |
|
8 |
-
export async function
|
9 |
-
if (!Array.isArray(
|
10 |
return {}
|
11 |
}
|
12 |
|
@@ -16,11 +16,11 @@ export async function getStatsForVideos(videoIds: string[]): Promise<Record<stri
|
|
16 |
|
17 |
const listOfRedisIDs: string[] = []
|
18 |
|
19 |
-
for (const
|
20 |
-
listOfRedisIDs.push(`videos:${
|
21 |
-
listOfRedisIDs.push(`videos:${
|
22 |
-
listOfRedisIDs.push(`videos:${
|
23 |
-
stats[
|
24 |
numberOfViews: 0,
|
25 |
numberOfLikes: 0,
|
26 |
numberOfDislikes: 0,
|
@@ -31,7 +31,7 @@ export async function getStatsForVideos(videoIds: string[]): Promise<Record<stri
|
|
31 |
|
32 |
let v = 0
|
33 |
for (let i = 0; i < listOfRedisValues.length; i += 3) {
|
34 |
-
stats[
|
35 |
numberOfViews: listOfRedisValues[i] || 0,
|
36 |
numberOfLikes: listOfRedisValues[i + 1] || 0,
|
37 |
numberOfDislikes: listOfRedisValues[i + 2] || 0
|
@@ -44,15 +44,15 @@ export async function getStatsForVideos(videoIds: string[]): Promise<Record<stri
|
|
44 |
}
|
45 |
}
|
46 |
|
47 |
-
export async function
|
48 |
if (developerMode) {
|
49 |
-
const stats = await
|
50 |
|
51 |
-
return stats[
|
52 |
}
|
53 |
|
54 |
try {
|
55 |
-
const result = await redis.incr(`videos:${
|
56 |
|
57 |
return result
|
58 |
} catch (err) {
|
@@ -60,7 +60,7 @@ export async function watchVideo(videoId: string): Promise<number> {
|
|
60 |
}
|
61 |
}
|
62 |
|
63 |
-
export async function
|
64 |
let numberOfLikes = 0
|
65 |
let numberOfDislikes = 0
|
66 |
let isLikedByUser = false
|
@@ -68,8 +68,8 @@ export async function getVideoRating(videoId: string, apiKey?: string): Promise<
|
|
68 |
|
69 |
try {
|
70 |
// update video likes counter
|
71 |
-
numberOfLikes = (await redis.get<number>(`videos:${
|
72 |
-
numberOfDislikes = (await redis.get<number>(`videos:${
|
73 |
} catch (err) {
|
74 |
}
|
75 |
|
@@ -79,7 +79,7 @@ export async function getVideoRating(videoId: string, apiKey?: string): Promise<
|
|
79 |
const credentials = { accessToken: apiKey }
|
80 |
|
81 |
const user = await whoAmI({ credentials }) as unknown as WhoAmIUser
|
82 |
-
const isLiked = await redis.get<boolean>(`users:${user.id}:activity:videos:${
|
83 |
if (isLiked !== null) {
|
84 |
isLikedByUser = !!isLiked
|
85 |
isDislikedByUser = !isLiked
|
@@ -97,7 +97,7 @@ export async function getVideoRating(videoId: string, apiKey?: string): Promise<
|
|
97 |
}
|
98 |
}
|
99 |
|
100 |
-
export async function
|
101 |
// note: we want the like to throw an exception if it failed
|
102 |
let numberOfLikes = 0
|
103 |
let numberOfDislikes = 0
|
@@ -108,21 +108,21 @@ export async function rateVideo(videoId: string, liked: boolean, apiKey: string)
|
|
108 |
|
109 |
const user = await whoAmI({ credentials }) as unknown as WhoAmIUser
|
110 |
|
111 |
-
const hasLiked = await redis.get<boolean>(`users:${user.id}:activity:videos:${
|
112 |
|
113 |
const hasAlreadyRatedTheSame = hasLiked !== null && liked === hasLiked
|
114 |
|
115 |
if (hasAlreadyRatedTheSame) {
|
116 |
return {
|
117 |
-
numberOfLikes: await redis.get(`videos:${
|
118 |
-
numberOfDislikes: await redis.get(`videos:${
|
119 |
isLikedByUser: liked,
|
120 |
isDislikedByUser: !liked
|
121 |
}
|
122 |
}
|
123 |
const hasAlreadyRatedAndDifferently = hasLiked !== null && liked !== hasLiked
|
124 |
|
125 |
-
await redis.set(`users:${user.id}:activity:videos:${
|
126 |
|
127 |
isLikedByUser = liked
|
128 |
isDislikedByUser = !liked
|
@@ -133,14 +133,14 @@ export async function rateVideo(videoId: string, liked: boolean, apiKey: string)
|
|
133 |
try {
|
134 |
if (liked) {
|
135 |
// update video likes counter
|
136 |
-
numberOfLikes = await redis.incr(`videos:${
|
137 |
if (hasAlreadyRatedAndDifferently) {
|
138 |
-
numberOfDislikes = await redis.decr(`videos:${
|
139 |
}
|
140 |
} else {
|
141 |
-
numberOfDislikes = await redis.incr(`videos:${
|
142 |
if (hasAlreadyRatedAndDifferently) {
|
143 |
-
numberOfLikes = await redis.decr(`videos:${
|
144 |
}
|
145 |
}
|
146 |
} catch (err) {
|
|
|
2 |
|
3 |
import { developerMode } from "@/app/config"
|
4 |
import { WhoAmIUser, whoAmI } from "@/huggingface/hub/src"
|
5 |
+
import { MediaRating } from "@/types/general"
|
6 |
import { redis } from "./redis";
|
7 |
|
8 |
+
export async function getStatsForMedias(mediaIds: string[]): Promise<Record<string, { numberOfViews: number; numberOfLikes: number; numberOfDislikes: number}>> {
|
9 |
+
if (!Array.isArray(mediaIds)) {
|
10 |
return {}
|
11 |
}
|
12 |
|
|
|
16 |
|
17 |
const listOfRedisIDs: string[] = []
|
18 |
|
19 |
+
for (const mediaId of mediaIds) {
|
20 |
+
listOfRedisIDs.push(`videos:${mediaId}:stats:views`)
|
21 |
+
listOfRedisIDs.push(`videos:${mediaId}:stats:likes`)
|
22 |
+
listOfRedisIDs.push(`videos:${mediaId}:stats:dislikes`)
|
23 |
+
stats[mediaId] = {
|
24 |
numberOfViews: 0,
|
25 |
numberOfLikes: 0,
|
26 |
numberOfDislikes: 0,
|
|
|
31 |
|
32 |
let v = 0
|
33 |
for (let i = 0; i < listOfRedisValues.length; i += 3) {
|
34 |
+
stats[mediaIds[v++]] = {
|
35 |
numberOfViews: listOfRedisValues[i] || 0,
|
36 |
numberOfLikes: listOfRedisValues[i + 1] || 0,
|
37 |
numberOfDislikes: listOfRedisValues[i + 2] || 0
|
|
|
44 |
}
|
45 |
}
|
46 |
|
47 |
+
export async function countNewMediaView(mediaId: string): Promise<number> {
|
48 |
if (developerMode) {
|
49 |
+
const stats = await getStatsForMedias([mediaId])
|
50 |
|
51 |
+
return stats[mediaId].numberOfViews
|
52 |
}
|
53 |
|
54 |
try {
|
55 |
+
const result = await redis.incr(`videos:${mediaId}:stats:views`)
|
56 |
|
57 |
return result
|
58 |
} catch (err) {
|
|
|
60 |
}
|
61 |
}
|
62 |
|
63 |
+
export async function getMediaRating(mediaId: string, apiKey?: string): Promise<MediaRating> {
|
64 |
let numberOfLikes = 0
|
65 |
let numberOfDislikes = 0
|
66 |
let isLikedByUser = false
|
|
|
68 |
|
69 |
try {
|
70 |
// update video likes counter
|
71 |
+
numberOfLikes = (await redis.get<number>(`videos:${mediaId}:stats:likes`)) || 0
|
72 |
+
numberOfDislikes = (await redis.get<number>(`videos:${mediaId}:stats:dislikes`)) || 0
|
73 |
} catch (err) {
|
74 |
}
|
75 |
|
|
|
79 |
const credentials = { accessToken: apiKey }
|
80 |
|
81 |
const user = await whoAmI({ credentials }) as unknown as WhoAmIUser
|
82 |
+
const isLiked = await redis.get<boolean>(`users:${user.id}:activity:videos:${mediaId}:liked`)
|
83 |
if (isLiked !== null) {
|
84 |
isLikedByUser = !!isLiked
|
85 |
isDislikedByUser = !isLiked
|
|
|
97 |
}
|
98 |
}
|
99 |
|
100 |
+
export async function rateMedia(mediaId: string, liked: boolean, apiKey: string): Promise<MediaRating> {
|
101 |
// note: we want the like to throw an exception if it failed
|
102 |
let numberOfLikes = 0
|
103 |
let numberOfDislikes = 0
|
|
|
108 |
|
109 |
const user = await whoAmI({ credentials }) as unknown as WhoAmIUser
|
110 |
|
111 |
+
const hasLiked = await redis.get<boolean>(`users:${user.id}:activity:videos:${mediaId}:liked`)
|
112 |
|
113 |
const hasAlreadyRatedTheSame = hasLiked !== null && liked === hasLiked
|
114 |
|
115 |
if (hasAlreadyRatedTheSame) {
|
116 |
return {
|
117 |
+
numberOfLikes: await redis.get(`videos:${mediaId}:stats:likes`) || 0,
|
118 |
+
numberOfDislikes: await redis.get(`videos:${mediaId}:stats:dislikes`) || 0,
|
119 |
isLikedByUser: liked,
|
120 |
isDislikedByUser: !liked
|
121 |
}
|
122 |
}
|
123 |
const hasAlreadyRatedAndDifferently = hasLiked !== null && liked !== hasLiked
|
124 |
|
125 |
+
await redis.set(`users:${user.id}:activity:videos:${mediaId}:liked`, liked)
|
126 |
|
127 |
isLikedByUser = liked
|
128 |
isDislikedByUser = !liked
|
|
|
133 |
try {
|
134 |
if (liked) {
|
135 |
// update video likes counter
|
136 |
+
numberOfLikes = await redis.incr(`videos:${mediaId}:stats:likes`)
|
137 |
if (hasAlreadyRatedAndDifferently) {
|
138 |
+
numberOfDislikes = await redis.decr(`videos:${mediaId}:stats:dislikes`)
|
139 |
}
|
140 |
} else {
|
141 |
+
numberOfDislikes = await redis.incr(`videos:${mediaId}:stats:dislikes`)
|
142 |
if (hasAlreadyRatedAndDifferently) {
|
143 |
+
numberOfLikes = await redis.decr(`videos:${mediaId}:stats:likes`)
|
144 |
}
|
145 |
}
|
146 |
} catch (err) {
|
src/app/server/actions/submitVideoRequest.ts
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
"use server"
|
2 |
|
3 |
-
import { ChannelInfo, VideoGenerationModel,
|
4 |
|
5 |
import { uploadVideoRequestToDataset } from "./ai-tube-hf/uploadVideoRequestToDataset"
|
6 |
|
@@ -32,7 +32,7 @@ export async function submitVideoRequest({
|
|
32 |
tags: string[]
|
33 |
duration: number
|
34 |
orientation: VideoOrientation
|
35 |
-
}): Promise<
|
36 |
if (!apiKey) {
|
37 |
throw new Error(`the apiKey is required`)
|
38 |
}
|
|
|
1 |
"use server"
|
2 |
|
3 |
+
import { ChannelInfo, VideoGenerationModel, MediaInfo, VideoOrientation } from "@/types/general"
|
4 |
|
5 |
import { uploadVideoRequestToDataset } from "./ai-tube-hf/uploadVideoRequestToDataset"
|
6 |
|
|
|
32 |
tags: string[]
|
33 |
duration: number
|
34 |
orientation: VideoOrientation
|
35 |
+
}): Promise<MediaInfo> {
|
36 |
if (!apiKey) {
|
37 |
throw new Error(`the apiKey is required`)
|
38 |
}
|
src/app/server/actions/utils/computeOrientationProjectionWidthHeight.ts
CHANGED
@@ -1,4 +1,4 @@
|
|
1 |
-
import { VideoOrientation,
|
2 |
|
3 |
import { parseVideoOrientation } from "./parseVideoOrientation"
|
4 |
import { parseProjectionFromLoRA } from "./parseProjectionFromLoRA"
|
@@ -13,7 +13,7 @@ export function computeOrientationProjectionWidthHeight({
|
|
13 |
orientation?: any
|
14 |
}): {
|
15 |
orientation: VideoOrientation
|
16 |
-
projection:
|
17 |
width: number
|
18 |
height: number
|
19 |
} {
|
|
|
1 |
+
import { VideoOrientation, MediaProjection } from "@/types/general"
|
2 |
|
3 |
import { parseVideoOrientation } from "./parseVideoOrientation"
|
4 |
import { parseProjectionFromLoRA } from "./parseProjectionFromLoRA"
|
|
|
13 |
orientation?: any
|
14 |
}): {
|
15 |
orientation: VideoOrientation
|
16 |
+
projection: MediaProjection
|
17 |
width: number
|
18 |
height: number
|
19 |
} {
|
src/app/server/actions/utils/isAntisocial.ts
CHANGED
@@ -1,8 +1,8 @@
|
|
1 |
-
import {
|
2 |
|
3 |
const winners = new Set(`${process.env.WINNERS || ""}`.toLowerCase().split(",").map(x => x.trim()).filter(x => x))
|
4 |
|
5 |
-
export function isAntisocial(video:
|
6 |
|
7 |
// some people are reported by the community for their anti-social behavior
|
8 |
// this include:
|
|
|
1 |
+
import { MediaInfo } from "@/types/general"
|
2 |
|
3 |
const winners = new Set(`${process.env.WINNERS || ""}`.toLowerCase().split(",").map(x => x.trim()).filter(x => x))
|
4 |
|
5 |
+
export function isAntisocial(video: MediaInfo): boolean {
|
6 |
|
7 |
// some people are reported by the community for their anti-social behavior
|
8 |
// this include:
|
src/app/server/actions/utils/isHighQuality.ts
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
-
import {
|
2 |
|
3 |
-
export function isHighQuality(video:
|
4 |
const numberOfViews = Math.abs(Math.max(0, video.numberOfViews))
|
5 |
const numberOfLikes = Math.abs(Math.max(0, video.numberOfLikes))
|
6 |
const numberOfDislikes = Math.abs(Math.max(0, video.numberOfDislikes))
|
|
|
1 |
+
import { MediaInfo } from "@/types/general"
|
2 |
|
3 |
+
export function isHighQuality(video: MediaInfo) {
|
4 |
const numberOfViews = Math.abs(Math.max(0, video.numberOfViews))
|
5 |
const numberOfLikes = Math.abs(Math.max(0, video.numberOfLikes))
|
6 |
const numberOfDislikes = Math.abs(Math.max(0, video.numberOfDislikes))
|
src/app/server/actions/utils/parseProjectionFromLoRA.ts
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
-
import {
|
2 |
|
3 |
-
export function parseProjectionFromLoRA(input?: any):
|
4 |
const name = `${input || ""}`.trim().toLowerCase()
|
5 |
|
6 |
const isEquirectangular = (
|
|
|
1 |
+
import { MediaProjection } from "@/types/general"
|
2 |
|
3 |
+
export function parseProjectionFromLoRA(input?: any): MediaProjection {
|
4 |
const name = `${input || ""}`.trim().toLowerCase()
|
5 |
|
6 |
const isEquirectangular = (
|
src/app/state/categories.ts
CHANGED
@@ -11,7 +11,7 @@ export const videoCategoriesWithLabels = {
|
|
11 |
"Time Travel": "Time Travel", // vlogs etc
|
12 |
// "gaming": "Gaming",
|
13 |
// "trailers": "Trailers",
|
14 |
-
// "aitubers": "
|
15 |
// "ads": "100% Ads",
|
16 |
}
|
17 |
|
|
|
11 |
"Time Travel": "Time Travel", // vlogs etc
|
12 |
// "gaming": "Gaming",
|
13 |
// "trailers": "Trailers",
|
14 |
+
// "aitubers": "AiTubers",
|
15 |
// "ads": "100% Ads",
|
16 |
}
|
17 |
|
src/app/state/useCurrentUser.ts
CHANGED
@@ -24,7 +24,7 @@ export function useCurrentUser({
|
|
24 |
apiKey: string
|
25 |
oauthResult?: OAuthResult
|
26 |
|
27 |
-
// the long standing API is a temporary solution for "PRO" users of
|
28 |
// (users who use Clap files using external tools,
|
29 |
// or want ot use their own HF account to generate videos)
|
30 |
longStandingApiKey: string
|
@@ -219,7 +219,7 @@ export function useCurrentUser({
|
|
219 |
*
|
220 |
* For Developer Applications, you can add any URL you want to the list of allowed redirect URIs at https://huggingface.co/settings/connected-applications.
|
221 |
*/
|
222 |
-
redirectUrl:
|
223 |
|
224 |
/**
|
225 |
* State to pass to the OAuth provider, which will be returned in the call to `oauthLogin` after the redirect.
|
|
|
24 |
apiKey: string
|
25 |
oauthResult?: OAuthResult
|
26 |
|
27 |
+
// the long standing API is a temporary solution for "PRO" users of AiTube
|
28 |
// (users who use Clap files using external tools,
|
29 |
// or want ot use their own HF account to generate videos)
|
30 |
longStandingApiKey: string
|
|
|
219 |
*
|
220 |
* For Developer Applications, you can add any URL you want to the list of allowed redirect URIs at https://huggingface.co/settings/connected-applications.
|
221 |
*/
|
222 |
+
redirectUrl: `${process.env.NEXT_PUBLIC_DOMAIN}/api/login`,
|
223 |
|
224 |
/**
|
225 |
* State to pass to the OAuth provider, which will be returned in the call to `oauthLogin` after the redirect.
|
src/app/state/useStore.ts
CHANGED
@@ -2,7 +2,7 @@
|
|
2 |
|
3 |
import { create } from "zustand"
|
4 |
|
5 |
-
import { ChannelInfo,
|
6 |
|
7 |
export const useStore = create<{
|
8 |
displayMode: InterfaceDisplayMode
|
@@ -28,11 +28,11 @@ export const useStore = create<{
|
|
28 |
searchAutocompleteQuery: string
|
29 |
setSearchAutocompleteQuery: (searchAutocompleteQuery?: string) => void
|
30 |
|
31 |
-
searchAutocompleteResults:
|
32 |
-
setSearchAutocompleteResults: (searchAutocompleteResults:
|
33 |
|
34 |
-
searchResults:
|
35 |
-
setSearchResults: (searchResults:
|
36 |
|
37 |
currentUser?: UserInfo
|
38 |
setCurrentUser: (currentUser?: UserInfo) => void
|
@@ -61,35 +61,35 @@ export const useStore = create<{
|
|
61 |
currentModel?: string
|
62 |
setCurrentModel: (currentModel?: string) => void
|
63 |
|
64 |
-
publicVideo?:
|
65 |
-
setPublicVideo: (publicVideo?:
|
66 |
|
67 |
publicComments: CommentInfo[]
|
68 |
setPublicComments: (publicComment: CommentInfo[]) => void
|
69 |
|
70 |
-
publicVideos:
|
71 |
-
setPublicVideos: (publicVideos:
|
72 |
|
73 |
-
publicChannelVideos:
|
74 |
-
setPublicChannelVideos: (publicChannelVideos:
|
75 |
|
76 |
-
publicTrack?:
|
77 |
-
setPublicTrack: (publicTrack?:
|
78 |
|
79 |
-
publicTracks:
|
80 |
-
setPublicTracks: (publicTracks:
|
81 |
|
82 |
-
userVideo?:
|
83 |
-
setUserVideo: (userVideo?:
|
84 |
|
85 |
-
userVideos:
|
86 |
-
setUserVideos: (userVideos:
|
87 |
|
88 |
-
recommendedVideos:
|
89 |
-
setRecommendedVideos: (recommendedVideos:
|
90 |
|
91 |
-
// currentPrompts:
|
92 |
-
// setCurrentPrompts: (currentPrompts:
|
93 |
}>((set, get) => ({
|
94 |
displayMode: "desktop",
|
95 |
setDisplayMode: (displayMode: InterfaceDisplayMode) => {
|
@@ -127,8 +127,8 @@ export const useStore = create<{
|
|
127 |
set({ showAutocompleteBox })
|
128 |
},
|
129 |
|
130 |
-
searchAutocompleteResults: [] as
|
131 |
-
setSearchAutocompleteResults: (searchAutocompleteResults:
|
132 |
set({ searchAutocompleteResults })
|
133 |
},
|
134 |
|
@@ -137,8 +137,8 @@ export const useStore = create<{
|
|
137 |
set({ searchQuery })
|
138 |
},
|
139 |
|
140 |
-
searchResults: [] as
|
141 |
-
setSearchResults: (searchResults:
|
142 |
set({ searchResults })
|
143 |
},
|
144 |
|
@@ -202,7 +202,7 @@ export const useStore = create<{
|
|
202 |
},
|
203 |
|
204 |
publicVideo: undefined,
|
205 |
-
setPublicVideo: (publicVideo?:
|
206 |
set({ publicVideo })
|
207 |
},
|
208 |
|
@@ -212,7 +212,7 @@ export const useStore = create<{
|
|
212 |
},
|
213 |
|
214 |
publicVideos: [],
|
215 |
-
setPublicVideos: (publicVideos:
|
216 |
set({
|
217 |
publicVideos: Array.isArray(publicVideos) ? publicVideos : []
|
218 |
})
|
@@ -220,36 +220,36 @@ export const useStore = create<{
|
|
220 |
|
221 |
|
222 |
publicTrack: undefined,
|
223 |
-
setPublicTrack: (publicTrack?:
|
224 |
set({ publicTrack })
|
225 |
},
|
226 |
|
227 |
publicTracks: [],
|
228 |
-
setPublicTracks: (publicTracks:
|
229 |
set({
|
230 |
publicTracks: Array.isArray(publicTracks) ? publicTracks : []
|
231 |
})
|
232 |
},
|
233 |
|
234 |
publicChannelVideos: [],
|
235 |
-
setPublicChannelVideos: (publicChannelVideos:
|
236 |
set({
|
237 |
publicVideos: Array.isArray(publicChannelVideos) ? publicChannelVideos : []
|
238 |
})
|
239 |
},
|
240 |
|
241 |
userVideo: undefined,
|
242 |
-
setUserVideo: (userVideo?:
|
243 |
|
244 |
userVideos: [],
|
245 |
-
setUserVideos: (userVideos:
|
246 |
set({
|
247 |
userVideos: Array.isArray(userVideos) ? userVideos : []
|
248 |
})
|
249 |
},
|
250 |
|
251 |
recommendedVideos: [],
|
252 |
-
setRecommendedVideos: (recommendedVideos:
|
253 |
set({
|
254 |
recommendedVideos: Array.isArray(recommendedVideos) ? recommendedVideos : []
|
255 |
})
|
|
|
2 |
|
3 |
import { create } from "zustand"
|
4 |
|
5 |
+
import { ChannelInfo, MediaInfo, InterfaceDisplayMode, InterfaceView, InterfaceMenuMode, InterfaceHeaderMode, CommentInfo, UserInfo } from "@/types/general"
|
6 |
|
7 |
export const useStore = create<{
|
8 |
displayMode: InterfaceDisplayMode
|
|
|
28 |
searchAutocompleteQuery: string
|
29 |
setSearchAutocompleteQuery: (searchAutocompleteQuery?: string) => void
|
30 |
|
31 |
+
searchAutocompleteResults: MediaInfo[]
|
32 |
+
setSearchAutocompleteResults: (searchAutocompleteResults: MediaInfo[]) => void
|
33 |
|
34 |
+
searchResults: MediaInfo[]
|
35 |
+
setSearchResults: (searchResults: MediaInfo[]) => void
|
36 |
|
37 |
currentUser?: UserInfo
|
38 |
setCurrentUser: (currentUser?: UserInfo) => void
|
|
|
61 |
currentModel?: string
|
62 |
setCurrentModel: (currentModel?: string) => void
|
63 |
|
64 |
+
publicVideo?: MediaInfo
|
65 |
+
setPublicVideo: (publicVideo?: MediaInfo) => void
|
66 |
|
67 |
publicComments: CommentInfo[]
|
68 |
setPublicComments: (publicComment: CommentInfo[]) => void
|
69 |
|
70 |
+
publicVideos: MediaInfo[]
|
71 |
+
setPublicVideos: (publicVideos: MediaInfo[]) => void
|
72 |
|
73 |
+
publicChannelVideos: MediaInfo[]
|
74 |
+
setPublicChannelVideos: (publicChannelVideos: MediaInfo[]) => void
|
75 |
|
76 |
+
publicTrack?: MediaInfo
|
77 |
+
setPublicTrack: (publicTrack?: MediaInfo) => void
|
78 |
|
79 |
+
publicTracks: MediaInfo[]
|
80 |
+
setPublicTracks: (publicTracks: MediaInfo[]) => void
|
81 |
|
82 |
+
userVideo?: MediaInfo
|
83 |
+
setUserVideo: (userVideo?: MediaInfo) => void
|
84 |
|
85 |
+
userVideos: MediaInfo[]
|
86 |
+
setUserVideos: (userVideos: MediaInfo[]) => void
|
87 |
|
88 |
+
recommendedVideos: MediaInfo[]
|
89 |
+
setRecommendedVideos: (recommendedVideos: MediaInfo[]) => void
|
90 |
|
91 |
+
// currentPrompts: MediaInfo[]
|
92 |
+
// setCurrentPrompts: (currentPrompts: MediaInfo[]) => void
|
93 |
}>((set, get) => ({
|
94 |
displayMode: "desktop",
|
95 |
setDisplayMode: (displayMode: InterfaceDisplayMode) => {
|
|
|
127 |
set({ showAutocompleteBox })
|
128 |
},
|
129 |
|
130 |
+
searchAutocompleteResults: [] as MediaInfo[],
|
131 |
+
setSearchAutocompleteResults: (searchAutocompleteResults: MediaInfo[]) => {
|
132 |
set({ searchAutocompleteResults })
|
133 |
},
|
134 |
|
|
|
137 |
set({ searchQuery })
|
138 |
},
|
139 |
|
140 |
+
searchResults: [] as MediaInfo[],
|
141 |
+
setSearchResults: (searchResults: MediaInfo[]) => {
|
142 |
set({ searchResults })
|
143 |
},
|
144 |
|
|
|
202 |
},
|
203 |
|
204 |
publicVideo: undefined,
|
205 |
+
setPublicVideo: (publicVideo?: MediaInfo) => {
|
206 |
set({ publicVideo })
|
207 |
},
|
208 |
|
|
|
212 |
},
|
213 |
|
214 |
publicVideos: [],
|
215 |
+
setPublicVideos: (publicVideos: MediaInfo[] = []) => {
|
216 |
set({
|
217 |
publicVideos: Array.isArray(publicVideos) ? publicVideos : []
|
218 |
})
|
|
|
220 |
|
221 |
|
222 |
publicTrack: undefined,
|
223 |
+
setPublicTrack: (publicTrack?: MediaInfo) => {
|
224 |
set({ publicTrack })
|
225 |
},
|
226 |
|
227 |
publicTracks: [],
|
228 |
+
setPublicTracks: (publicTracks: MediaInfo[] = []) => {
|
229 |
set({
|
230 |
publicTracks: Array.isArray(publicTracks) ? publicTracks : []
|
231 |
})
|
232 |
},
|
233 |
|
234 |
publicChannelVideos: [],
|
235 |
+
setPublicChannelVideos: (publicChannelVideos: MediaInfo[] = []) => {
|
236 |
set({
|
237 |
publicVideos: Array.isArray(publicChannelVideos) ? publicChannelVideos : []
|
238 |
})
|
239 |
},
|
240 |
|
241 |
userVideo: undefined,
|
242 |
+
setUserVideo: (userVideo?: MediaInfo) => { set({ userVideo }) },
|
243 |
|
244 |
userVideos: [],
|
245 |
+
setUserVideos: (userVideos: MediaInfo[] = []) => {
|
246 |
set({
|
247 |
userVideos: Array.isArray(userVideos) ? userVideos : []
|
248 |
})
|
249 |
},
|
250 |
|
251 |
recommendedVideos: [],
|
252 |
+
setRecommendedVideos: (recommendedVideos: MediaInfo[]) => {
|
253 |
set({
|
254 |
recommendedVideos: Array.isArray(recommendedVideos) ? recommendedVideos : []
|
255 |
})
|
src/app/views/home-view/index.tsx
CHANGED
@@ -4,7 +4,7 @@ import { useEffect, useTransition } from "react"
|
|
4 |
|
5 |
import { useStore } from "@/app/state/useStore"
|
6 |
import { cn } from "@/lib/utils"
|
7 |
-
import {
|
8 |
import { getVideos } from "@/app/server/actions/ai-tube-hf/getVideos"
|
9 |
import { VideoList } from "@/app/interface/video-list"
|
10 |
import { getTags } from "@/app/server/actions/ai-tube-hf/getTags"
|
@@ -33,7 +33,7 @@ export function HomeView() {
|
|
33 |
})
|
34 |
}, [currentTag])
|
35 |
|
36 |
-
const handleSelect = (video:
|
37 |
setView("public_video")
|
38 |
setPublicVideo(video)
|
39 |
}
|
|
|
4 |
|
5 |
import { useStore } from "@/app/state/useStore"
|
6 |
import { cn } from "@/lib/utils"
|
7 |
+
import { MediaInfo } from "@/types/general"
|
8 |
import { getVideos } from "@/app/server/actions/ai-tube-hf/getVideos"
|
9 |
import { VideoList } from "@/app/interface/video-list"
|
10 |
import { getTags } from "@/app/server/actions/ai-tube-hf/getTags"
|
|
|
33 |
})
|
34 |
}, [currentTag])
|
35 |
|
36 |
+
const handleSelect = (video: MediaInfo) => {
|
37 |
setView("public_video")
|
38 |
setPublicVideo(video)
|
39 |
}
|
src/app/views/public-music-videos-view/index.tsx
CHANGED
@@ -4,7 +4,7 @@ import { useEffect, useTransition } from "react"
|
|
4 |
|
5 |
import { useStore } from "@/app/state/useStore"
|
6 |
import { cn } from "@/lib/utils"
|
7 |
-
import {
|
8 |
import { getVideos } from "@/app/server/actions/ai-tube-hf/getVideos"
|
9 |
import { TrackList } from "@/app/interface/track-list"
|
10 |
import { PlaylistControl } from "@/app/interface/playlist-control"
|
@@ -34,7 +34,7 @@ export function PublicMusicVideosView() {
|
|
34 |
*/
|
35 |
}, [])
|
36 |
|
37 |
-
const handleSelect = (media:
|
38 |
// console.log("going to play:", media.assetUrl.replace(".mp4", ".mp3"))
|
39 |
playlist.playback({
|
40 |
url: media.assetUrl.replace(".mp4", ".mp3"),
|
|
|
4 |
|
5 |
import { useStore } from "@/app/state/useStore"
|
6 |
import { cn } from "@/lib/utils"
|
7 |
+
import { MediaInfo } from "@/types/general"
|
8 |
import { getVideos } from "@/app/server/actions/ai-tube-hf/getVideos"
|
9 |
import { TrackList } from "@/app/interface/track-list"
|
10 |
import { PlaylistControl } from "@/app/interface/playlist-control"
|
|
|
34 |
*/
|
35 |
}, [])
|
36 |
|
37 |
+
const handleSelect = (media: MediaInfo) => {
|
38 |
// console.log("going to play:", media.assetUrl.replace(".mp4", ".mp3"))
|
39 |
playlist.playback({
|
40 |
url: media.assetUrl.replace(".mp4", ".mp3"),
|