jbilcke commited on
Commit
ac7030c
1 Parent(s): ca494c0

preparing transition to user-generated content + gaussians

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env +2 -0
  2. README.md +7 -7
  3. package-lock.json +0 -0
  4. package.json +36 -29
  5. src/app/api/login/route.ts +7 -7
  6. src/app/api/media/[mediaId]/route.ts +76 -0
  7. src/app/api/video/[videoId]/route.ts +7 -6
  8. src/app/embed/page.tsx +20 -13
  9. src/app/interface/about/index.tsx +3 -3
  10. src/app/interface/collection-card/index.tsx +1 -0
  11. src/app/interface/equirectangular-video-player/index.tsx +2 -2
  12. src/app/interface/equirectangular-video-player/viewer.tsx +17 -9
  13. src/app/interface/gsplat/index.tsx +120 -0
  14. src/app/interface/left-menu/index.tsx +28 -2
  15. src/app/interface/like-button/index.tsx +13 -13
  16. src/app/interface/media-list/index.tsx +3 -3
  17. src/app/interface/{video-player → media-player}/cartesian.tsx +24 -9
  18. src/app/interface/{video-player → media-player}/equirectangular.tsx +22 -15
  19. src/app/interface/media-player/gaussian.tsx +32 -0
  20. src/app/interface/{video-player → media-player}/index.tsx +28 -14
  21. src/app/interface/pending-video-card/index.tsx +3 -3
  22. src/app/interface/pending-video-list/index.tsx +3 -3
  23. src/app/interface/playlist-control/index.tsx +1 -1
  24. src/app/interface/recommended-videos/index.tsx +7 -8
  25. src/app/interface/search-input/index.tsx +1 -1
  26. src/app/interface/track-card/index.tsx +4 -3
  27. src/app/interface/video-card/index.tsx +5 -4
  28. src/app/layout.tsx +2 -2
  29. src/app/main.tsx +6 -6
  30. src/app/page.tsx +8 -8
  31. src/app/server/actions/ai-tube-hf/deleteVideoRequest.ts +2 -2
  32. src/app/server/actions/ai-tube-hf/downloadClapProject.ts +3 -3
  33. src/app/server/actions/ai-tube-hf/extendVideosWithStats.ts +4 -4
  34. src/app/server/actions/ai-tube-hf/getChannelVideos.ts +3 -3
  35. src/app/server/actions/ai-tube-hf/getVideo.ts +4 -4
  36. src/app/server/actions/ai-tube-hf/getVideoIndex.ts +3 -3
  37. src/app/server/actions/ai-tube-hf/getVideos.ts +3 -3
  38. src/app/server/actions/ai-tube-hf/parseChannel.ts +2 -2
  39. src/app/server/actions/ai-tube-hf/uploadVideoRequestToDataset.ts +5 -5
  40. src/app/server/actions/stats.ts +26 -26
  41. src/app/server/actions/submitVideoRequest.ts +2 -2
  42. src/app/server/actions/utils/computeOrientationProjectionWidthHeight.ts +2 -2
  43. src/app/server/actions/utils/isAntisocial.ts +2 -2
  44. src/app/server/actions/utils/isHighQuality.ts +2 -2
  45. src/app/server/actions/utils/parseProjectionFromLoRA.ts +2 -2
  46. src/app/state/categories.ts +1 -1
  47. src/app/state/useCurrentUser.ts +2 -2
  48. src/app/state/useStore.ts +35 -35
  49. src/app/views/home-view/index.tsx +2 -2
  50. 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: AI Tube
3
  emoji: 🍿
4
  colorFrom: red
5
  colorTo: red
@@ -9,7 +9,7 @@ app_port: 3000
9
  disable_embedding: false
10
  ---
11
 
12
- # 🍿 AI Tube
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 AI Tube 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,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 AI Tube home page), then for the AI Tube 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,7 +42,7 @@ This delay will be reduced in the future.
42
 
43
  ### Videos are taking too long to generate
44
 
45
- AI Tube 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,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 AI Tube or download it to run on my machine?
74
 
75
- AI Tube 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
 
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.5.1",
16
- "@photo-sphere-viewer/video-plugin": "^5.5.1",
 
 
 
 
 
 
 
 
17
  "@radix-ui/react-accordion": "^1.1.2",
18
- "@radix-ui/react-avatar": "^1.0.3",
19
  "@radix-ui/react-checkbox": "^1.0.4",
20
  "@radix-ui/react-collapsible": "^1.0.3",
21
- "@radix-ui/react-dialog": "^1.0.4",
22
- "@radix-ui/react-dropdown-menu": "^2.0.5",
23
  "@radix-ui/react-icons": "^1.3.0",
24
  "@radix-ui/react-label": "^2.0.2",
25
- "@radix-ui/react-menubar": "^1.0.3",
26
- "@radix-ui/react-popover": "^1.0.6",
27
- "@radix-ui/react-select": "^1.2.2",
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.4",
33
- "@radix-ui/react-tooltip": "^1.0.6",
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.2",
43
- "autoprefixer": "10.4.14",
44
- "class-variance-authority": "^0.6.1",
45
- "clsx": "^2.0.0",
46
- "cmdk": "^0.2.0",
47
  "cookies-next": "^2.1.2",
48
- "date-fns": "^2.30.0",
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.0",
58
- "photo-sphere-viewer-lensflare-plugin": "^2.0.1",
59
  "pick": "^0.0.1",
60
- "postcss": "8.4.26",
61
- "qs": "^6.11.2",
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": "^4.0.2-psv5.4.4",
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.32.5",
77
- "styled-components": "^6.0.7",
78
- "tailwind-merge": "^2.1.0",
79
- "tailwindcss": "3.4.1",
80
  "tailwindcss-animate": "^1.0.7",
81
  "temp-dir": "^3.0.0",
82
- "ts-node": "^10.9.1",
83
  "type-fest": "^4.8.2",
84
- "typescript": "5.1.6",
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: "https://aitube.at/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 AI Tube frontend
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 AI Tube page which the user was browser
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(`https://aitube.at${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 AI Tube frontend
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 AI Tube page which the user was browser
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(`https://aitube.at${redirectTo}${params}`)
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 { parseProjectionFromLoRA } from "@/app/server/actions/utils/parseProjectionFromLoRA";
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} - AI Tube</title>
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 video = await getVideo({ videoId, neverThrow: true })
21
 
22
- if (!video) {
23
- throw new Error("Video not found")
24
  }
25
 
26
  return {
27
- title: `${video.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: video.label || "", // put the video title here
35
- description: video.description || "", // put the video description here
36
  siteName: "AiTube",
37
  images: [
38
- `https://huggingface.co/datasets/jbilcke-hf/ai-tube-index/resolve/main/videos/${video.id}.webp`
39
  ],
40
  videos: [
41
  {
42
- "url": video.assetUrlHd || video.assetUrl
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: video.description || "",
51
- images: `https://huggingface.co/datasets/jbilcke-hf/ai-tube-index/resolve/main/videos/${video.id}.webp`,
52
  players: {
53
- playerUrl: `https://jbilcke-hf-ai-tube.hf.space/embed?v=${video.id}`,
54
- streamUrl: `https://huggingface.co/datasets/jbilcke-hf/ai-tube-index/resolve/main/videos/${video.id}.mp4`,
55
  width: 1024,
56
  height: 576
57
  }
@@ -76,7 +76,14 @@ export async function generateMetadata(
76
  }
77
 
78
 
79
- export default async function Embed({ searchParams: { v: videoId } }: AppQueryProps) {
 
 
 
 
 
 
 
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>AI Tube</DialogTitle>
47
  <DialogDescription className="w-full text-center text-lg font-bold text-stone-800">
48
- What is AI Tube?
49
  </DialogDescription>
50
  </DialogHeader>
51
  <div className="grid gap-4 py-4 text-stone-200 text-base">
52
  <p className="">
53
- AI Tube 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).
 
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 { VideoInfo } from "@/types/general"
7
 
8
  import { VideoSphereViewer } from "./viewer"
9
 
@@ -11,7 +11,7 @@ export function EquirectangularVideoPlayer({
11
  video,
12
  className = "",
13
  }: {
14
- video?: VideoInfo
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, useState } from "react"
4
- import AutoSizer from "react-virtualized-auto-sizer"
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 { cn } from "@/lib/utils"
9
- import { VideoInfo } from "@/types/general"
 
 
 
 
 
10
 
11
- type PhotoSpherePlugin = (PluginConstructor | [PluginConstructor, any])
 
 
 
 
12
 
13
  export function VideoSphereViewer({
14
  video,
@@ -17,7 +24,7 @@ export function VideoSphereViewer({
17
  height,
18
  muted = false,
19
  }: {
20
- video: VideoInfo
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
- video
12
  }: {
13
- video?: VideoInfo
14
  }) {
15
  const [_pending, startTransition] = useTransition()
16
 
17
- const [rating, setRating] = useState<VideoRating>({
18
  isLikedByUser: false,
19
  isDislikedByUser: false,
20
  numberOfLikes: 0,
@@ -28,15 +28,15 @@ export function LikeButton({
28
 
29
  useEffect(() => {
30
  startTransition(async () => {
31
- if (!video || !video?.id) { return }
32
 
33
- const freshRating = await getVideoRating(video.id, huggingfaceApiKey)
34
  setRating(freshRating)
35
 
36
  })
37
- }, [video?.id, huggingfaceApiKey])
38
 
39
- if (!video) { return null }
40
 
41
  if (!huggingfaceApiKey) { return null }
42
 
@@ -52,7 +52,7 @@ export function LikeButton({
52
  })
53
  startTransition(async () => {
54
  try {
55
- const freshRating = await rateVideo(video.id, true, huggingfaceApiKey)
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 rateVideo(video.id, false, huggingfaceApiKey)
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, VideoInfo } from "@/types/general"
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: VideoInfo[]
15
 
16
  /**
17
  * Layout mode
@@ -31,7 +31,7 @@ export function MediaList({
31
 
32
  className?: string
33
 
34
- onSelect?: (media: VideoInfo) => void
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 { VideoInfo } from "@/types/general"
8
 
9
  export function CartesianVideoPlayer({
10
- video,
11
  enableShortcuts = true,
12
  className = "",
13
  // currentTime,
14
  }: {
15
- video: VideoInfo
16
  enableShortcuts?: boolean
17
  className?: string
18
  // currentTime?: number
19
  }) {
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  return (
21
  <div className={cn(
22
- `w-full`,
23
- `flex flex-col items-center justify-center`,
24
- `rounded-xl overflow-hidden`,
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
- url: video.assetUrlHd || video.assetUrl,
 
 
 
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/${video.id}.webp`
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, useState } from "react"
4
 
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 { cn } from "@/lib/utils"
9
- import { VideoInfo } from "@/types/general"
10
 
11
- type PhotoSpherePlugin = (PluginConstructor | [PluginConstructor, any])
 
 
 
 
 
 
 
 
 
12
 
13
  export function EquirectangularVideoPlayer({
14
- video,
15
  className = "",
16
  width,
17
  height,
18
  muted = false,
19
  }: {
20
- video: VideoInfo
21
  className?: string
22
  width: number
23
  height: number
@@ -37,7 +44,9 @@ export function EquirectangularVideoPlayer({
37
  })
38
  }, [width, height])
39
 
40
- if (!video.assetUrl) { return null }
 
 
41
 
42
  return (
43
  <div
@@ -47,10 +56,7 @@ export function EquirectangularVideoPlayer({
47
  <ReactPhotoSphereViewer
48
 
49
  container=""
50
- containerClass={cn(
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
- panorama: { source: video.assetUrlHd || video.assetUrl },
 
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 { VideoInfo } from "@/types/general"
7
- import { parseProjectionFromLoRA } from "@/app/server/actions/utils/parseProjectionFromLoRA"
8
 
9
  import { EquirectangularVideoPlayer } from "./equirectangular"
10
  import { CartesianVideoPlayer } from "./cartesian"
 
11
 
12
- export function VideoPlayer({
13
- video,
14
  enableShortcuts = true,
15
  className = "",
16
  // currentTime,
17
  }: {
18
- video?: VideoInfo
19
  enableShortcuts?: boolean
20
  className?: string
21
  // currentTime?: number
22
  }) {
23
- // we should our video players from misssing data
24
- if (!video?.assetUrl) { return null }
25
 
26
- const isEquirectangular = (
27
- video.projection === "equirectangular" ||
28
- parseProjectionFromLoRA(video.lora) === "equirectangular"
29
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
- if (isEquirectangular) {
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 video={video} className={className} width={width} height={height} />
37
  )}</AutoSizer>
38
  </div>
39
  )
40
  }
41
 
42
  return (
43
- <CartesianVideoPlayer video={video} enableShortcuts={enableShortcuts} className={className} />
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 { VideoInfo } from "@/types/general"
7
  import { truncate } from "./truncate"
8
 
9
  export function PendingVideoCard({
@@ -11,8 +11,8 @@ export function PendingVideoCard({
11
  onDelete,
12
  className = "",
13
  }: {
14
- video: VideoInfo
15
- onDelete?: (video: VideoInfo) => void
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 { VideoInfo } 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,8 +9,8 @@ export function PendingVideoList({
9
  onDelete,
10
  className = "",
11
  }: {
12
- videos: VideoInfo[]
13
- onDelete?: (video: VideoInfo) => void
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 { VideoInfo } from "@/types/general"
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 { cn } from "@/lib/utils"
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
- video,
13
  }: {
14
- // the video to use for the recommendations
15
- video: VideoInfo
16
  }) {
17
  const [_isPending, startTransition] = useTransition()
18
  const setRecommendedVideos = useStore(s => s.setRecommendedVideos)
@@ -22,12 +21,12 @@ export function RecommendedVideos({
22
  startTransition(async () => {
23
  setRecommendedVideos(await getVideos({
24
  sortBy: "random",
25
- niceToHaveTags: video.tags,
26
- ignoreVideoIds: [video.id],
27
  maxVideos: 16,
28
  }))
29
  })
30
- }, video.tags)
31
 
32
  return (
33
  <VideoList
 
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={`https://aitube.at/watch?v=${media.id}`}>
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, VideoInfo } from "@/types/general"
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: VideoInfo
26
  className?: string
27
  layout?: MediaDisplayLayout
28
- onSelect?: (media: VideoInfo) => void
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, VideoInfo } from "@/types/general"
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: VideoInfo
25
  className?: string
26
  layout?: MediaDisplayLayout
27
- onSelect?: (media: VideoInfo) => void
28
  selected?: boolean
29
  index: number
30
  }) {
@@ -75,7 +75,7 @@ export function VideoCard({
75
  }, [index])
76
 
77
  return (
78
- <Link href={`https://aitube.at/watch?v=${media.id}`}>
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: '🍿 AI Tube',
18
- description: '🍿 AI Tube',
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, VideoInfo } from "@/types/general"
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?: VideoInfo
39
- publicVideos?: VideoInfo[]
40
- publicChannelVideos?: VideoInfo[]
41
- publicTracks?: VideoInfo[]
42
- publicTrack?: VideoInfo
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: `🍿 AI Tube`,
20
  metadataBase,
21
  openGraph: {
22
  type: "website",
23
  // url: "https://example.com",
24
- title: "AI Tube",
25
  description: "The first fully AI generated video platform",
26
- siteName: "🍿 AI Tube",
27
 
28
  videos: [],
29
  images: [],
@@ -39,14 +39,14 @@ export async function generateMetadata(
39
  }
40
 
41
  return {
42
- title: `${video.label} - AI Tube`,
43
  metadataBase,
44
  openGraph: {
45
  type: "website",
46
  // url: "https://example.com",
47
  title: video.label || "", // put the video title here
48
  description: video.description || "", // put the vide description here
49
- siteName: "AI Tube",
50
 
51
  videos: [
52
  {
@@ -58,14 +58,14 @@ export async function generateMetadata(
58
  }
59
  } catch (err) {
60
  return {
61
- title: "AI Tube",
62
  metadataBase,
63
  openGraph: {
64
  type: "website",
65
  // url: "https://example.com",
66
- title: "AI Tube", // put the video title here
67
  description: "", // put the vide description here
68
- siteName: "AI Tube",
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 { VideoInfo } from "@/types/general"
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: VideoInfo
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, VideoInfo, VideoRequest } from "@/types/general"
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: 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: 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 { VideoInfo } from "@/types/general"
4
 
5
- import { getStatsForVideos } from "../stats"
6
 
7
- export async function extendVideosWithStats(videos: VideoInfo[]): Promise<VideoInfo[]> {
8
 
9
- const allStats = await getStatsForVideos(videos.map(v => v.id))
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, VideoInfo, VideoStatus } from "@/types/general"
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<VideoInfo[]> {
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: VideoInfo = {
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 { VideoInfo } from "@/types/general"
4
 
5
  import { getVideoIndex } from "./getVideoIndex"
6
- import { getStatsForVideos } from "../stats"
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<VideoInfo | undefined> {
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 getStatsForVideos([video.id])
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 { VideoInfo, VideoStatus } from "@/types/general"
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, VideoInfo>> {
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, VideoInfo>
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 { VideoInfo } from "@/types/general"
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<VideoInfo[]> {
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: VideoInfo[] = 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)
 
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 AI Tube channel")
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 AI Tube channel: "${slug}"`)
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, VideoInfo, VideoOrientation, VideoRequest } from "@/types/general"
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: 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 AI Tube doesn't support upload of clap files
131
  clapUrl: "",
132
 
133
  updatedAt: new Date().toISOString(),
@@ -141,7 +141,7 @@ ${prompt}
141
  }),
142
  }
143
 
144
- const newVideo: VideoInfo = {
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 AI Tube doesn't support upload of clap files
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 { VideoRating } from "@/types/general"
6
  import { redis } from "./redis";
7
 
8
- export async function getStatsForVideos(videoIds: string[]): Promise<Record<string, { numberOfViews: number; numberOfLikes: number; numberOfDislikes: number}>> {
9
- if (!Array.isArray(videoIds)) {
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 videoId of videoIds) {
20
- listOfRedisIDs.push(`videos:${videoId}:stats:views`)
21
- listOfRedisIDs.push(`videos:${videoId}:stats:likes`)
22
- listOfRedisIDs.push(`videos:${videoId}:stats:dislikes`)
23
- stats[videoId] = {
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[videoIds[v++]] = {
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 watchVideo(videoId: string): Promise<number> {
48
  if (developerMode) {
49
- const stats = await getStatsForVideos([videoId])
50
 
51
- return stats[videoId].numberOfViews
52
  }
53
 
54
  try {
55
- const result = await redis.incr(`videos:${videoId}:stats:views`)
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 getVideoRating(videoId: string, apiKey?: string): Promise<VideoRating> {
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:${videoId}:stats:likes`)) || 0
72
- numberOfDislikes = (await redis.get<number>(`videos:${videoId}:stats:dislikes`)) || 0
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:${videoId}:liked`)
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 rateVideo(videoId: string, liked: boolean, apiKey: string): Promise<VideoRating> {
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:${videoId}:liked`)
112
 
113
  const hasAlreadyRatedTheSame = hasLiked !== null && liked === hasLiked
114
 
115
  if (hasAlreadyRatedTheSame) {
116
  return {
117
- numberOfLikes: await redis.get(`videos:${videoId}:stats:likes`) || 0,
118
- numberOfDislikes: await redis.get(`videos:${videoId}: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:${videoId}:liked`, liked)
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:${videoId}:stats:likes`)
137
  if (hasAlreadyRatedAndDifferently) {
138
- numberOfDislikes = await redis.decr(`videos:${videoId}:stats:dislikes`)
139
  }
140
  } else {
141
- numberOfDislikes = await redis.incr(`videos:${videoId}:stats:dislikes`)
142
  if (hasAlreadyRatedAndDifferently) {
143
- numberOfLikes = await redis.decr(`videos:${videoId}:stats:likes`)
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, VideoInfo, VideoOrientation } from "@/types/general"
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<VideoInfo> {
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, VideoProjection } from "@/types/general"
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: VideoProjection
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 { VideoInfo } 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: VideoInfo): boolean {
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 { VideoInfo } from "@/types/general"
2
 
3
- export function isHighQuality(video: VideoInfo) {
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 { VideoProjection } from "@/types/general"
2
 
3
- export function parseProjectionFromLoRA(input?: any): VideoProjection {
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": "AI tubers",
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 AI Tube
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: "https://aitube.at/api/login",
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, VideoInfo, InterfaceDisplayMode, InterfaceView, InterfaceMenuMode, InterfaceHeaderMode, CommentInfo, UserInfo } from "@/types/general"
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: VideoInfo[]
32
- setSearchAutocompleteResults: (searchAutocompleteResults: VideoInfo[]) => void
33
 
34
- searchResults: VideoInfo[]
35
- setSearchResults: (searchResults: VideoInfo[]) => void
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?: VideoInfo
65
- setPublicVideo: (publicVideo?: VideoInfo) => void
66
 
67
  publicComments: CommentInfo[]
68
  setPublicComments: (publicComment: CommentInfo[]) => void
69
 
70
- publicVideos: VideoInfo[]
71
- setPublicVideos: (publicVideos: VideoInfo[]) => void
72
 
73
- publicChannelVideos: VideoInfo[]
74
- setPublicChannelVideos: (publicChannelVideos: VideoInfo[]) => void
75
 
76
- publicTrack?: VideoInfo
77
- setPublicTrack: (publicTrack?: VideoInfo) => void
78
 
79
- publicTracks: VideoInfo[]
80
- setPublicTracks: (publicTracks: VideoInfo[]) => void
81
 
82
- userVideo?: VideoInfo
83
- setUserVideo: (userVideo?: VideoInfo) => void
84
 
85
- userVideos: VideoInfo[]
86
- setUserVideos: (userVideos: VideoInfo[]) => void
87
 
88
- recommendedVideos: VideoInfo[]
89
- setRecommendedVideos: (recommendedVideos: VideoInfo[]) => void
90
 
91
- // currentPrompts: VideoInfo[]
92
- // setCurrentPrompts: (currentPrompts: VideoInfo[]) => void
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 VideoInfo[],
131
- setSearchAutocompleteResults: (searchAutocompleteResults: VideoInfo[]) => {
132
  set({ searchAutocompleteResults })
133
  },
134
 
@@ -137,8 +137,8 @@ export const useStore = create<{
137
  set({ searchQuery })
138
  },
139
 
140
- searchResults: [] as VideoInfo[],
141
- setSearchResults: (searchResults: VideoInfo[]) => {
142
  set({ searchResults })
143
  },
144
 
@@ -202,7 +202,7 @@ export const useStore = create<{
202
  },
203
 
204
  publicVideo: undefined,
205
- setPublicVideo: (publicVideo?: VideoInfo) => {
206
  set({ publicVideo })
207
  },
208
 
@@ -212,7 +212,7 @@ export const useStore = create<{
212
  },
213
 
214
  publicVideos: [],
215
- setPublicVideos: (publicVideos: VideoInfo[] = []) => {
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?: VideoInfo) => {
224
  set({ publicTrack })
225
  },
226
 
227
  publicTracks: [],
228
- setPublicTracks: (publicTracks: VideoInfo[] = []) => {
229
  set({
230
  publicTracks: Array.isArray(publicTracks) ? publicTracks : []
231
  })
232
  },
233
 
234
  publicChannelVideos: [],
235
- setPublicChannelVideos: (publicChannelVideos: VideoInfo[] = []) => {
236
  set({
237
  publicVideos: Array.isArray(publicChannelVideos) ? publicChannelVideos : []
238
  })
239
  },
240
 
241
  userVideo: undefined,
242
- setUserVideo: (userVideo?: VideoInfo) => { set({ userVideo }) },
243
 
244
  userVideos: [],
245
- setUserVideos: (userVideos: VideoInfo[] = []) => {
246
  set({
247
  userVideos: Array.isArray(userVideos) ? userVideos : []
248
  })
249
  },
250
 
251
  recommendedVideos: [],
252
- setRecommendedVideos: (recommendedVideos: VideoInfo[]) => {
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 { VideoInfo } 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,7 +33,7 @@ export function HomeView() {
33
  })
34
  }, [currentTag])
35
 
36
- const handleSelect = (video: VideoInfo) => {
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 { VideoInfo } 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,7 +34,7 @@ export function PublicMusicVideosView() {
34
  */
35
  }, [])
36
 
37
- const handleSelect = (media: VideoInfo) => {
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"),