jbilcke-hf HF staff commited on
Commit
6215321
1 Parent(s): 8101ed0

Modifying AiTube to support Stories Factory use cases

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 +4 -0
  2. package-lock.json +67 -58
  3. package.json +3 -2
  4. public/blanks/blank_1sec_1024x576.webm +0 -0
  5. public/blanks/blank_1sec_512x288.webm +0 -0
  6. public/logos/latent-engine/latent-engine.png +0 -0
  7. public/logos/latent-engine/latent-engine.xcf +0 -0
  8. src/app/api/auth/getToken.ts +20 -0
  9. src/app/api/generate/story/route.ts +28 -0
  10. src/app/api/generate/storyboards/route.ts +84 -0
  11. src/app/api/generate/video/route.ts +19 -0
  12. src/app/api/generators/clap/generateClap.ts +2 -1
  13. src/app/api/resolvers/image/route.ts +44 -5
  14. src/app/api/resolvers/video/route.ts +86 -9
  15. src/app/dream/{page.tsx → spoiler.tsx} +17 -12
  16. src/app/main.tsx +28 -14
  17. src/app/state/useStore.ts +8 -0
  18. src/components/interface/latent-engine/components/content-layer/index.tsx +1 -1
  19. src/components/interface/latent-engine/components/{disclaimers/this-is-ai.tsx → intros/ai-content-disclaimer.tsx} +4 -5
  20. src/components/interface/latent-engine/components/intros/latent-engine.png +0 -0
  21. src/components/interface/latent-engine/components/intros/powered-by.tsx +31 -0
  22. src/components/interface/latent-engine/core/engine.tsx +69 -55
  23. src/components/interface/latent-engine/core/{fetchLatentClap.ts → generators/fetchLatentClap.ts} +0 -0
  24. src/components/interface/latent-engine/core/{fetchLatentSearchResults.ts → generators/fetchLatentSearchResults.ts} +0 -0
  25. src/components/interface/latent-engine/core/prompts/getCharacterPrompt.ts +26 -0
  26. src/components/interface/latent-engine/core/prompts/getVideoPrompt.ts +104 -0
  27. src/components/interface/latent-engine/core/types.ts +60 -31
  28. src/components/interface/latent-engine/{store → core}/useLatentEngine.ts +363 -147
  29. src/components/interface/latent-engine/core/video-buffer.tsx +57 -0
  30. src/components/interface/latent-engine/resolvers/generic/index.tsx +9 -6
  31. src/components/interface/latent-engine/resolvers/image/generateImage.ts +21 -2
  32. src/components/interface/latent-engine/resolvers/image/index.tsx +17 -6
  33. src/components/interface/latent-engine/resolvers/interface/index.tsx +20 -7
  34. src/components/interface/latent-engine/resolvers/resolveSegment.ts +2 -2
  35. src/components/interface/latent-engine/resolvers/resolveSegments.ts +3 -3
  36. src/components/interface/latent-engine/resolvers/video/THIS FOLDER CONTENT IS DEPRECATED +0 -0
  37. src/components/interface/latent-engine/resolvers/video/basic-video.tsx +47 -0
  38. src/components/interface/latent-engine/resolvers/video/generateVideo.ts +20 -3
  39. src/components/interface/latent-engine/resolvers/video/index.tsx +26 -19
  40. src/components/interface/latent-engine/resolvers/video/index_legacy.tsx +86 -0
  41. src/components/interface/latent-engine/resolvers/video/index_notSoGood.tsx +82 -0
  42. src/components/interface/latent-engine/resolvers/video/video-loop.tsx +83 -0
  43. src/components/interface/latent-engine/{core → utils/canvas}/drawSegmentation.ts +1 -1
  44. src/components/interface/latent-engine/utils/data/getElementsSortedByStartAt.ts +13 -0
  45. src/components/interface/latent-engine/utils/data/getSegmentEndAt.ts +3 -0
  46. src/components/interface/latent-engine/utils/data/getSegmentId.ts +3 -0
  47. src/components/interface/latent-engine/utils/data/getSegmentStartAt.ts +3 -0
  48. src/components/interface/latent-engine/utils/data/getZIndexDepth.ts +3 -0
  49. src/components/interface/latent-engine/utils/data/setSegmentEndAt.ts +3 -0
  50. src/components/interface/latent-engine/utils/data/setSegmentId.ts +3 -0
.env CHANGED
@@ -1,4 +1,8 @@
1
 
 
 
 
 
2
  NEXT_PUBLIC_DOMAIN="https://aitube.at"
3
 
4
  NEXT_PUBLIC_SHOW_BETA_FEATURES="false"
 
1
 
2
+ API_SECRET_JWT_KEY=""
3
+ API_SECRET_JWT_ISSUER=""
4
+ API_SECRET_JWT_AUDIENCE=""
5
+
6
  NEXT_PUBLIC_DOMAIN="https://aitube.at"
7
 
8
  NEXT_PUBLIC_SHOW_BETA_FEATURES="false"
package-lock.json CHANGED
@@ -58,13 +58,14 @@
58
  "eslint": "8.45.0",
59
  "eslint-config-next": "13.4.10",
60
  "fastest-levenshtein": "^1.0.16",
61
- "gsplat": "^1.2.3",
62
  "hash-wasm": "^4.11.0",
 
63
  "lodash.debounce": "^4.0.8",
64
  "lucide-react": "^0.260.0",
65
  "markdown-yaml-metadata-parser": "^3.0.0",
66
  "minisearch": "^6.3.0",
67
- "next": "^14.1.4",
68
  "openai": "^4.36.0",
69
  "photo-sphere-viewer-lensflare-plugin": "^2.1.2",
70
  "pick": "^0.0.1",
@@ -1492,9 +1493,9 @@
1492
  }
1493
  },
1494
  "node_modules/@mediapipe/tasks-vision": {
1495
- "version": "0.10.13-rc.20240419",
1496
- "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.13-rc.20240419.tgz",
1497
- "integrity": "sha512-IfIFVSkektnlo9FjqhDYe5LzbOJTdx9kjs65lXmerqPQSOA3akYwp5zt2Rtm1G4JrG1Isv1A15yfiSlqm5pGdQ=="
1498
  },
1499
  "node_modules/@next/env": {
1500
  "version": "14.2.2",
@@ -1677,9 +1678,9 @@
1677
  }
1678
  },
1679
  "node_modules/@photo-sphere-viewer/core": {
1680
- "version": "5.7.2",
1681
- "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.7.2.tgz",
1682
- "integrity": "sha512-5RznXVRwuO+Izceae2SbwYM/H8GHtwxKlT26P4UcRFZYsYKllMAggAz9hhU729Vu+r1+il5PHvomIsmPHVTTaw==",
1683
  "dependencies": {
1684
  "three": "^0.161.0"
1685
  }
@@ -1690,77 +1691,77 @@
1690
  "integrity": "sha512-LC28VFtjbOyEu5b93K0bNRLw1rQlMJ85lilKsYj6dgTu+7i17W+JCCEbvrpmNHF1F3NAUqDSWq50UD7w9H2xQw=="
1691
  },
1692
  "node_modules/@photo-sphere-viewer/equirectangular-video-adapter": {
1693
- "version": "5.7.2",
1694
- "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/equirectangular-video-adapter/-/equirectangular-video-adapter-5.7.2.tgz",
1695
- "integrity": "sha512-cAaot52nPqa2p77Xp1humRvuxRIa8cqbZ/XRhA8kBToFLT1Ugh9YBcDD7pM/358JtAjicUbLpT7Ioap9iEigxQ==",
1696
  "peerDependencies": {
1697
- "@photo-sphere-viewer/core": "5.7.2"
1698
  }
1699
  },
1700
  "node_modules/@photo-sphere-viewer/gyroscope-plugin": {
1701
- "version": "5.7.2",
1702
- "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/gyroscope-plugin/-/gyroscope-plugin-5.7.2.tgz",
1703
- "integrity": "sha512-GHbm96fBfcxzo1RXYsuuhCRxR0TIv3S+3HszyyNKF4x9Nc6dledl26s98ND5ivnviHtAzH/u4eIWHTO3t2+HMg==",
1704
  "peerDependencies": {
1705
- "@photo-sphere-viewer/core": "5.7.2"
1706
  }
1707
  },
1708
  "node_modules/@photo-sphere-viewer/markers-plugin": {
1709
- "version": "5.7.2-fix.1",
1710
- "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/markers-plugin/-/markers-plugin-5.7.2-fix.1.tgz",
1711
- "integrity": "sha512-zNJet/ACLBfZgKEl1fCz1qNd6aYey5/2Zqr2++xts0hx179qzA8V3ka2w55SQb/X3WQEWMjomgjtroNd/4wSwg==",
1712
  "peerDependencies": {
1713
- "@photo-sphere-viewer/core": "5.7.2"
1714
  }
1715
  },
1716
  "node_modules/@photo-sphere-viewer/overlays-plugin": {
1717
- "version": "5.7.2",
1718
- "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/overlays-plugin/-/overlays-plugin-5.7.2.tgz",
1719
- "integrity": "sha512-0Jp6yWqMzBWr+TRrp90Ua9+YqzmE1a/m+Trg+8FFnXWDc4u1SWPGvwCa4xk0N/x8b5Vx24UnMCRCTQo8B+Ldxw==",
1720
  "peerDependencies": {
1721
- "@photo-sphere-viewer/core": "5.7.2"
1722
  }
1723
  },
1724
  "node_modules/@photo-sphere-viewer/resolution-plugin": {
1725
- "version": "5.7.2",
1726
- "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/resolution-plugin/-/resolution-plugin-5.7.2.tgz",
1727
- "integrity": "sha512-4gUd4yI8o4GEmHMmBeF3+rN+ElrMXxflDxXKRll+VBGCYAsx3vBw0cHm4kVjYh07ep+6uCa35tyg2VFi1P4mrA==",
1728
  "peerDependencies": {
1729
- "@photo-sphere-viewer/core": "5.7.2",
1730
- "@photo-sphere-viewer/settings-plugin": "5.7.2"
1731
  }
1732
  },
1733
  "node_modules/@photo-sphere-viewer/settings-plugin": {
1734
- "version": "5.7.2",
1735
- "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/settings-plugin/-/settings-plugin-5.7.2.tgz",
1736
- "integrity": "sha512-RnDG0dPmLtZ6mWnUhL9dSDhztMYTVCd/PwwvhqL3rIHMxcteFxjVKRdVQyLvxUoWGj9O6bvrHYGLEQM1wMNX1g==",
1737
  "peerDependencies": {
1738
- "@photo-sphere-viewer/core": "5.7.2"
1739
  }
1740
  },
1741
  "node_modules/@photo-sphere-viewer/stereo-plugin": {
1742
- "version": "5.7.2",
1743
- "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/stereo-plugin/-/stereo-plugin-5.7.2.tgz",
1744
- "integrity": "sha512-vL0a0An5rlcMpERkuWRWlAkt5KoS8LovbMImAB0zZ9YFuorp/xZCKuODQ8HADz3YknoH0ohTAnuxusV7c5FQmA==",
1745
  "peerDependencies": {
1746
- "@photo-sphere-viewer/core": "5.7.2",
1747
- "@photo-sphere-viewer/gyroscope-plugin": "5.7.2"
1748
  }
1749
  },
1750
  "node_modules/@photo-sphere-viewer/video-plugin": {
1751
- "version": "5.7.2",
1752
- "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.7.2.tgz",
1753
- "integrity": "sha512-vrPV9RCr4HsYiORkto1unDPeUkbN2kbyogvNUoLiQ78M4xkPOqoKxtfxCxTYoM+7gECwNL9VTF81+okck498qA==",
1754
  "peerDependencies": {
1755
- "@photo-sphere-viewer/core": "5.7.2"
1756
  }
1757
  },
1758
  "node_modules/@photo-sphere-viewer/visible-range-plugin": {
1759
- "version": "5.7.2",
1760
- "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/visible-range-plugin/-/visible-range-plugin-5.7.2.tgz",
1761
- "integrity": "sha512-vvLoDOkQbuG0YQC8NgKPORmfbKXpAn4AFWi/bjB9ziYPsXU9LTVobiIjqAwbHxh6/yzCZoAscRFb601K85H1mA==",
1762
  "peerDependencies": {
1763
- "@photo-sphere-viewer/core": "5.7.2"
1764
  }
1765
  },
1766
  "node_modules/@pkgjs/parseargs": {
@@ -3698,9 +3699,9 @@
3698
  }
3699
  },
3700
  "node_modules/caniuse-lite": {
3701
- "version": "1.0.30001611",
3702
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001611.tgz",
3703
- "integrity": "sha512-19NuN1/3PjA3QI8Eki55N8my4LzfkMCRLgCVfrl/slbSAchQfV0+GwjPrK3rq37As4UCLlM/DHajbKkAqbv92Q==",
3704
  "funding": [
3705
  {
3706
  "type": "opencollective",
@@ -4272,9 +4273,9 @@
4272
  "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
4273
  },
4274
  "node_modules/electron-to-chromium": {
4275
- "version": "1.4.744",
4276
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.744.tgz",
4277
- "integrity": "sha512-nAGcF0yeKKfrP13LMFr5U1eghfFSvFLg302VUFzWlcjPOnUYd52yU5x6PBYrujhNbc4jYmZFrGZFK+xasaEzVA=="
4278
  },
4279
  "node_modules/elliptic": {
4280
  "version": "6.5.4",
@@ -6005,6 +6006,14 @@
6005
  "jiti": "bin/jiti.js"
6006
  }
6007
  },
 
 
 
 
 
 
 
 
6008
  "node_modules/js-sha3": {
6009
  "version": "0.8.0",
6010
  "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
@@ -6606,9 +6615,9 @@
6606
  }
6607
  },
6608
  "node_modules/openai": {
6609
- "version": "4.38.1",
6610
- "resolved": "https://registry.npmjs.org/openai/-/openai-4.38.1.tgz",
6611
- "integrity": "sha512-nmSKE9O2piuoh9+AgDqwGHojIFSxToQ2jJqwaxjbzz2ebdD5LYY9s+bMe25b18t4QEgvtgW70JfK8BU3xf5dRw==",
6612
  "dependencies": {
6613
  "@types/node": "^18.11.18",
6614
  "@types/node-fetch": "^2.6.4",
@@ -8217,9 +8226,9 @@
8217
  }
8218
  },
8219
  "node_modules/type-fest": {
8220
- "version": "4.15.0",
8221
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.15.0.tgz",
8222
- "integrity": "sha512-tB9lu0pQpX5KJq54g+oHOLumOx+pMep4RaM6liXh2PKmVRFF+/vAtUP0ZaJ0kOySfVNjF6doBWPHhBhISKdlIA==",
8223
  "engines": {
8224
  "node": ">=16"
8225
  },
 
58
  "eslint": "8.45.0",
59
  "eslint-config-next": "13.4.10",
60
  "fastest-levenshtein": "^1.0.16",
61
+ "gsplat": "^1.2.4",
62
  "hash-wasm": "^4.11.0",
63
+ "jose": "^5.2.4",
64
  "lodash.debounce": "^4.0.8",
65
  "lucide-react": "^0.260.0",
66
  "markdown-yaml-metadata-parser": "^3.0.0",
67
  "minisearch": "^6.3.0",
68
+ "next": "^14.2.2",
69
  "openai": "^4.36.0",
70
  "photo-sphere-viewer-lensflare-plugin": "^2.1.2",
71
  "pick": "^0.0.1",
 
1493
  }
1494
  },
1495
  "node_modules/@mediapipe/tasks-vision": {
1496
+ "version": "0.10.13-rc.20240422",
1497
+ "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.13-rc.20240422.tgz",
1498
+ "integrity": "sha512-yKUS+Qidsw0pttv8Bx/EOdGkcWLH0b1tO4D0HfM6PaBjBAFUr7l5OmRfToFh4k8/XPto6d3X8Ujvm37Da0n2nw=="
1499
  },
1500
  "node_modules/@next/env": {
1501
  "version": "14.2.2",
 
1678
  }
1679
  },
1680
  "node_modules/@photo-sphere-viewer/core": {
1681
+ "version": "5.7.3",
1682
+ "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.7.3.tgz",
1683
+ "integrity": "sha512-F2YYQVHwRxrFFvBXdfx0o9rBVOiHgHyMCGgtnJvo4dKVtoUzJdTjXXKVYiOG1ZCVpx1jsyhZeY5DykHnU+7NSw==",
1684
  "dependencies": {
1685
  "three": "^0.161.0"
1686
  }
 
1691
  "integrity": "sha512-LC28VFtjbOyEu5b93K0bNRLw1rQlMJ85lilKsYj6dgTu+7i17W+JCCEbvrpmNHF1F3NAUqDSWq50UD7w9H2xQw=="
1692
  },
1693
  "node_modules/@photo-sphere-viewer/equirectangular-video-adapter": {
1694
+ "version": "5.7.3",
1695
+ "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/equirectangular-video-adapter/-/equirectangular-video-adapter-5.7.3.tgz",
1696
+ "integrity": "sha512-a0vUihauMhWuNVkp6dWvE1dkmHjMNIe1GEQYUwpduBJlGNabzx7mp3iJNll9NqmLpz85iHvGNpunn5J+zVhfsg==",
1697
  "peerDependencies": {
1698
+ "@photo-sphere-viewer/core": "5.7.3"
1699
  }
1700
  },
1701
  "node_modules/@photo-sphere-viewer/gyroscope-plugin": {
1702
+ "version": "5.7.3",
1703
+ "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/gyroscope-plugin/-/gyroscope-plugin-5.7.3.tgz",
1704
+ "integrity": "sha512-hS5ePszcR80Lb6ItLRK5xcUMDHsHyf1ebWeEeIwgMfURMdpenRA3phrrlSz8nVmeG/IQB0NRahHubUFfrIv/8g==",
1705
  "peerDependencies": {
1706
+ "@photo-sphere-viewer/core": "5.7.3"
1707
  }
1708
  },
1709
  "node_modules/@photo-sphere-viewer/markers-plugin": {
1710
+ "version": "5.7.3",
1711
+ "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/markers-plugin/-/markers-plugin-5.7.3.tgz",
1712
+ "integrity": "sha512-m4f/vqCAMnwEssHiN1akvnsmD6yWdREI2t7Hs/k+nsbWd/vJ2XKb0iio/JyZWbCJhgsKGZ5sasqzhdxSOQBI8A==",
1713
  "peerDependencies": {
1714
+ "@photo-sphere-viewer/core": "5.7.3"
1715
  }
1716
  },
1717
  "node_modules/@photo-sphere-viewer/overlays-plugin": {
1718
+ "version": "5.7.3",
1719
+ "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/overlays-plugin/-/overlays-plugin-5.7.3.tgz",
1720
+ "integrity": "sha512-OaoDXjsG6r5RuC7sKft+tfFFTJE1dbQokZM3/rB34kKbVpLWt9L8NJNBr1oBYyZt+3Fv5EYhL0MsmpTqf/gZTw==",
1721
  "peerDependencies": {
1722
+ "@photo-sphere-viewer/core": "5.7.3"
1723
  }
1724
  },
1725
  "node_modules/@photo-sphere-viewer/resolution-plugin": {
1726
+ "version": "5.7.3",
1727
+ "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/resolution-plugin/-/resolution-plugin-5.7.3.tgz",
1728
+ "integrity": "sha512-DbSnIWnwFNa6jeIMGnVsmmuQGn5yZfUN5J9Ew7Xg3CYn5y3HT8EMF/A+yIZQCe7J1AnA30BRhOK1sqgLivDHjA==",
1729
  "peerDependencies": {
1730
+ "@photo-sphere-viewer/core": "5.7.3",
1731
+ "@photo-sphere-viewer/settings-plugin": "5.7.3"
1732
  }
1733
  },
1734
  "node_modules/@photo-sphere-viewer/settings-plugin": {
1735
+ "version": "5.7.3",
1736
+ "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/settings-plugin/-/settings-plugin-5.7.3.tgz",
1737
+ "integrity": "sha512-YYhDd2xKYpxwnwgTq2h04+Aq19UALJbySc7B7GNlLilWuGE9Uy/u47PfUHkA5rJiUdJ0cOaXi7DOtPQZvPKiBQ==",
1738
  "peerDependencies": {
1739
+ "@photo-sphere-viewer/core": "5.7.3"
1740
  }
1741
  },
1742
  "node_modules/@photo-sphere-viewer/stereo-plugin": {
1743
+ "version": "5.7.3",
1744
+ "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/stereo-plugin/-/stereo-plugin-5.7.3.tgz",
1745
+ "integrity": "sha512-UGl9M3YilHcb1HhlTSl+hK+wfVdHrqKj4xSseJ5WDfFV3EErWhpSbg796GX+KWRrvF11EamhkXzAlR7ei5Y4hw==",
1746
  "peerDependencies": {
1747
+ "@photo-sphere-viewer/core": "5.7.3",
1748
+ "@photo-sphere-viewer/gyroscope-plugin": "5.7.3"
1749
  }
1750
  },
1751
  "node_modules/@photo-sphere-viewer/video-plugin": {
1752
+ "version": "5.7.3",
1753
+ "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.7.3.tgz",
1754
+ "integrity": "sha512-bUo6qLT2Tnbc8d/Q7iZEDor2jWqL91ZKgAozxtXB86FqkYtzVoqmjhP008fMRxtoNCJFe8biisjTryYJ7oYp9g==",
1755
  "peerDependencies": {
1756
+ "@photo-sphere-viewer/core": "5.7.3"
1757
  }
1758
  },
1759
  "node_modules/@photo-sphere-viewer/visible-range-plugin": {
1760
+ "version": "5.7.3",
1761
+ "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/visible-range-plugin/-/visible-range-plugin-5.7.3.tgz",
1762
+ "integrity": "sha512-dO+qHQsTxuiyrhd3ESRxHOZbE0ZCtAotzv8XSPcOrgnnbHp+2AMKKJhCwuQB58vUb9sxHf4NBo13MWU37vZm1g==",
1763
  "peerDependencies": {
1764
+ "@photo-sphere-viewer/core": "5.7.3"
1765
  }
1766
  },
1767
  "node_modules/@pkgjs/parseargs": {
 
3699
  }
3700
  },
3701
  "node_modules/caniuse-lite": {
3702
+ "version": "1.0.30001612",
3703
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001612.tgz",
3704
+ "integrity": "sha512-lFgnZ07UhaCcsSZgWW0K5j4e69dK1u/ltrL9lTUiFOwNHs12S3UMIEYgBV0Z6C6hRDev7iRnMzzYmKabYdXF9g==",
3705
  "funding": [
3706
  {
3707
  "type": "opencollective",
 
4273
  "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
4274
  },
4275
  "node_modules/electron-to-chromium": {
4276
+ "version": "1.4.746",
4277
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.746.tgz",
4278
+ "integrity": "sha512-jeWaIta2rIG2FzHaYIhSuVWqC6KJYo7oSBX4Jv7g+aVujKztfvdpf+n6MGwZdC5hQXbax4nntykLH2juIQrfPg=="
4279
  },
4280
  "node_modules/elliptic": {
4281
  "version": "6.5.4",
 
6006
  "jiti": "bin/jiti.js"
6007
  }
6008
  },
6009
+ "node_modules/jose": {
6010
+ "version": "5.2.4",
6011
+ "resolved": "https://registry.npmjs.org/jose/-/jose-5.2.4.tgz",
6012
+ "integrity": "sha512-6ScbIk2WWCeXkmzF6bRPmEuaqy1m8SbsRFMa/FLrSCkGIhj8OLVG/IH+XHVmNMx/KUo8cVWEE6oKR4dJ+S0Rkg==",
6013
+ "funding": {
6014
+ "url": "https://github.com/sponsors/panva"
6015
+ }
6016
+ },
6017
  "node_modules/js-sha3": {
6018
  "version": "0.8.0",
6019
  "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
 
6615
  }
6616
  },
6617
  "node_modules/openai": {
6618
+ "version": "4.38.3",
6619
+ "resolved": "https://registry.npmjs.org/openai/-/openai-4.38.3.tgz",
6620
+ "integrity": "sha512-mIL9WtrFNOanpx98mJ+X/wkoepcxdqqu0noWFoNQHl/yODQ47YM7NEYda7qp8JfjqpLFVxY9mQhshoS/Fqac0A==",
6621
  "dependencies": {
6622
  "@types/node": "^18.11.18",
6623
  "@types/node-fetch": "^2.6.4",
 
8226
  }
8227
  },
8228
  "node_modules/type-fest": {
8229
+ "version": "4.16.0",
8230
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.16.0.tgz",
8231
+ "integrity": "sha512-z7Rf5PXxIhbI6eJBTwdqe5bO02nUUmctq4WqviFSstBAWV0YNtEQRhEnZw73WJ8sZOqgFG6Jdl8gYZu7NBJZnA==",
8232
  "engines": {
8233
  "node": ">=16"
8234
  },
package.json CHANGED
@@ -59,13 +59,14 @@
59
  "eslint": "8.45.0",
60
  "eslint-config-next": "13.4.10",
61
  "fastest-levenshtein": "^1.0.16",
62
- "gsplat": "^1.2.3",
63
  "hash-wasm": "^4.11.0",
 
64
  "lodash.debounce": "^4.0.8",
65
  "lucide-react": "^0.260.0",
66
  "markdown-yaml-metadata-parser": "^3.0.0",
67
  "minisearch": "^6.3.0",
68
- "next": "^14.1.4",
69
  "openai": "^4.36.0",
70
  "photo-sphere-viewer-lensflare-plugin": "^2.1.2",
71
  "pick": "^0.0.1",
 
59
  "eslint": "8.45.0",
60
  "eslint-config-next": "13.4.10",
61
  "fastest-levenshtein": "^1.0.16",
62
+ "gsplat": "^1.2.4",
63
  "hash-wasm": "^4.11.0",
64
+ "jose": "^5.2.4",
65
  "lodash.debounce": "^4.0.8",
66
  "lucide-react": "^0.260.0",
67
  "markdown-yaml-metadata-parser": "^3.0.0",
68
  "minisearch": "^6.3.0",
69
+ "next": "^14.2.2",
70
  "openai": "^4.36.0",
71
  "photo-sphere-viewer-lensflare-plugin": "^2.1.2",
72
  "pick": "^0.0.1",
public/blanks/blank_1sec_1024x576.webm ADDED
Binary file (2.17 kB). View file
 
public/blanks/blank_1sec_512x288.webm ADDED
Binary file (1.87 kB). View file
 
public/logos/latent-engine/latent-engine.png ADDED
public/logos/latent-engine/latent-engine.xcf ADDED
Binary file (445 kB). View file
 
src/app/api/auth/getToken.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createSecretKey } from "crypto"
2
+ import { SignJWT } from "jose"
3
+
4
+ // https://jmswrnr.com/blog/protecting-next-js-api-routes-query-parameters
5
+
6
+ export async function getToken(data: Record<string, any> = {}): Promise<string> {
7
+ const secretKey = createSecretKey(`${process.env.API_SECRET_JWT_KEY || ""}`, 'utf-8');
8
+
9
+ const jwtToken = await new SignJWT(data)
10
+ .setProtectedHeader({
11
+ alg: 'HS256'
12
+ }) // algorithm
13
+ .setIssuedAt()
14
+ .setIssuer(`${process.env.API_SECRET_JWT_ISSUER || ""}`) // issuer
15
+ .setAudience(`${process.env.API_SECRET_JWT_AUDIENCE || ""}`) // audience
16
+ .setExpirationTime("1 day") // token expiration time - to prevent hackers from re-using our URLs more than a day
17
+ .sign(secretKey); // secretKey generated from previous step
18
+
19
+ return jwtToken
20
+ }
src/app/api/generate/story/route.ts ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse, NextRequest } from "next/server"
2
+
3
+ import { generateClapFromSimpleStory } from "@/lib/clap/generateClapFromSimpleStory"
4
+ import { serializeClap } from "@/lib/clap/serializeClap"
5
+
6
+ // a helper to generate Clap stories from a few sentences
7
+ // this is mostly used by external apps such as the Stories Factory
8
+ export async function POST(req: NextRequest) {
9
+
10
+ const request = await req.json() as {
11
+ story: string[]
12
+ // can add more stuff for the V2 of Stories Factory
13
+ }
14
+
15
+ const story = Array.isArray(request?.story) ? request.story : []
16
+
17
+ if (!story.length) { throw new Error(`please provide at least oen sentence for the story`) }
18
+
19
+ const clap = generateClapFromSimpleStory({
20
+ story,
21
+ // can add more stuff for the V2 of Stories Factory
22
+ })
23
+
24
+ return new NextResponse(await serializeClap(clap), {
25
+ status: 200,
26
+ headers: new Headers({ "content-type": "application/x-gzip" }),
27
+ })
28
+ }
src/app/api/generate/storyboards/route.ts ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse, NextRequest } from "next/server"
2
+
3
+ import { serializeClap } from "@/lib/clap/serializeClap"
4
+ import { parseClap } from "@/lib/clap/parseClap"
5
+ import { startOfSegment1IsWithinSegment2 } from "@/lib/utils/startOfSegment1IsWithinSegment2"
6
+ import { getVideoPrompt } from "@/components/interface/latent-engine/core/prompts/getVideoPrompt"
7
+ import { newSegment } from "@/lib/clap/newSegment"
8
+ import { generateImage } from "@/components/interface/latent-engine/resolvers/image/generateImage"
9
+ import { getToken } from "@/app/api/auth/getToken"
10
+
11
+ // a helper to generate storyboards for a Clap
12
+ // this is mostly used by external apps such as the Stories Factory
13
+ // this function will:
14
+ //
15
+ // - add missing storyboards to the shots
16
+ // - add missing storyboard prompts
17
+ // - add missing storyboard images
18
+ export async function POST(req: NextRequest) {
19
+
20
+ const jwtToken = await getToken({ user: "anonymous" })
21
+
22
+ const blob = await req.blob()
23
+ const clap = await parseClap(blob)
24
+
25
+ if (!clap.segments) { throw new Error(`no segment found in the provided clap!`) }
26
+
27
+ const shotsSegments = clap.segments.filter(s => s.category === "camera")
28
+
29
+ if (shotsSegments.length > 32) {
30
+ throw new Error(`Error, this endpoint being synchronous, it is designed for short stories only (max 32 shots).`)
31
+ }
32
+
33
+ for (const shotSegment of shotsSegments) {
34
+
35
+ const shotSegments = clap.segments.filter(s =>
36
+ startOfSegment1IsWithinSegment2(s, shotSegment)
37
+ )
38
+
39
+ const shotStoryboardSegments = shotSegments.filter(s =>
40
+ s.category === "storyboard"
41
+ )
42
+
43
+ let shotStoryboardSegment = shotStoryboardSegments.at(0)
44
+
45
+ // TASK 1: GENERATE MISSING STORYBOARD SEGMENT
46
+ if (!shotStoryboardSegment) {
47
+ shotStoryboardSegment = newSegment({
48
+ track: 1,
49
+ startTimeInMs: shotSegment.startTimeInMs,
50
+ endTimeInMs: shotSegment.endTimeInMs,
51
+ assetDurationInMs: shotSegment.assetDurationInMs,
52
+ category: "storyboard",
53
+ prompt: "",
54
+ assetUrl: "",
55
+ outputType: "image"
56
+ })
57
+ }
58
+
59
+ // TASK 2: GENERATE MISSING STORYBOARD PROMPT
60
+ if (!shotStoryboardSegment.prompt) {
61
+ // storyboard is missing, let's generate it
62
+ shotStoryboardSegment.prompt = getVideoPrompt(shotSegments, {}, [])
63
+ }
64
+
65
+ // TASK 3: GENERATE MISSING STORYBOARD BITMAP
66
+ if (!shotStoryboardSegment.assetUrl) {
67
+ // note this will do a fetch to AiTube API
68
+ // which is a bit weird since we are already inside the API, but it works
69
+ //TODO Julian: maybe we could use an internal function call instead?
70
+ shotStoryboardSegment.assetUrl = await generateImage({
71
+ prompt: shotStoryboardSegment.prompt,
72
+ width: clap.meta.width,
73
+ height: clap.meta.height,
74
+ token: jwtToken,
75
+ })
76
+ }
77
+ }
78
+ // TODO: generate the storyboards for the clap
79
+
80
+ return new NextResponse(await serializeClap(clap), {
81
+ status: 200,
82
+ headers: new Headers({ "content-type": "application/x-gzip" }),
83
+ })
84
+ }
src/app/api/generate/video/route.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse, NextRequest } from "next/server"
2
+
3
+ // we hide/wrap the micro-service under a unified AiTube API
4
+ export async function POST(req: NextRequest, res: NextResponse) {
5
+ NextResponse.redirect("https://jbilcke-hf-ai-tube-clap-exporter.hf.space")
6
+ }
7
+ /*
8
+ Alternative solution (in case the redirect doesn't work):
9
+
10
+ We could also grab the blob and forward it, like this:
11
+
12
+ const data = fetch(
13
+ "https://jbilcke-hf-ai-tube-clap-exporter.hf.space",
14
+ { method: "POST", body: await req.blob() }
15
+ )
16
+ const blob = data.blob()
17
+
18
+ Then return the blob with the right Content-Type using NextResponse
19
+ */
src/app/api/generators/clap/generateClap.ts CHANGED
@@ -39,7 +39,8 @@ export async function generateClap({
39
  defaultVideoModel: "SDXL",
40
  extraPositivePrompt: [],
41
  screenplay: "",
42
- streamType: "interactive"
 
43
  }
44
  })
45
 
 
39
  defaultVideoModel: "SDXL",
40
  extraPositivePrompt: [],
41
  screenplay: "",
42
+ isLoop: true,
43
+ isInteractive: true,
44
  }
45
  })
46
 
src/app/api/resolvers/image/route.ts CHANGED
@@ -1,4 +1,6 @@
1
  import { NextResponse, NextRequest } from "next/server"
 
 
2
  import queryString from "query-string"
3
 
4
  import { newRender, getRender } from "../../providers/videochain/renderWithVideoChain"
@@ -6,13 +8,37 @@ import { generateSeed } from "@/lib/utils/generateSeed"
6
  import { sleep } from "@/lib/utils/sleep"
7
  import { getNegativePrompt, getPositivePrompt } from "../../utils/imagePrompts"
8
  import { getContentType } from "@/lib/data/getContentType"
 
 
 
9
 
10
  export async function GET(req: NextRequest) {
11
 
12
- const qs = queryString.parseUrl(req.url || "")
13
- const query = (qs || {}).query
14
 
15
- let prompt = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  try {
17
  prompt = decodeURIComponent(query?.p?.toString() || "").trim()
18
  } catch (err) {}
@@ -20,6 +46,19 @@ let prompt = ""
20
  return NextResponse.json({ error: 'no prompt provided' }, { status: 400 });
21
  }
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  let format = "binary"
24
  try {
25
  const f = decodeURIComponent(query?.f?.toString() || "").trim()
@@ -36,8 +75,8 @@ let prompt = ""
36
  nbFrames: 1,
37
  nbFPS: 1,
38
  nbSteps: 8,
39
- width: 1024,
40
- height: 576,
41
  turbo: true,
42
  shouldRenewCache: true,
43
  seed: generateSeed()
 
1
  import { NextResponse, NextRequest } from "next/server"
2
+ import { createSecretKey } from "node:crypto"
3
+
4
  import queryString from "query-string"
5
 
6
  import { newRender, getRender } from "../../providers/videochain/renderWithVideoChain"
 
8
  import { sleep } from "@/lib/utils/sleep"
9
  import { getNegativePrompt, getPositivePrompt } from "../../utils/imagePrompts"
10
  import { getContentType } from "@/lib/data/getContentType"
11
+ import { getValidNumber } from "@/lib/utils/getValidNumber"
12
+
13
+ const secretKey = createSecretKey(`${process.env.API_SECRET_JWT_KEY || ""}`, 'utf-8');
14
 
15
  export async function GET(req: NextRequest) {
16
 
17
+ const qs = queryString.parseUrl(req.url || "")
18
+ const query = (qs || {}).query
19
 
20
+ /*
21
+ TODO: check the validity of the JWT token
22
+ let token = ""
23
+ try {
24
+ token = decodeURIComponent(query?.t?.toString() || "").trim()
25
+
26
+ // verify token
27
+ const { payload, protectedHeader } = await jwtVerify(token, secretKey, {
28
+ issuer: `${process.env.API_SECRET_JWT_ISSUER || ""}`, // issuer
29
+ audience: `${process.env.API_SECRET_JWT_AUDIENCE || ""}`, // audience
30
+ });
31
+ // log values to console
32
+ console.log(payload);
33
+ console.log(protectedHeader);
34
+ } catch (err) {
35
+ // token verification failed
36
+ console.log("Token is invalid");
37
+ return NextResponse.json({ error: `access denied ${err}` }, { status: 400 });
38
+ }
39
+ */
40
+
41
+ let prompt = ""
42
  try {
43
  prompt = decodeURIComponent(query?.p?.toString() || "").trim()
44
  } catch (err) {}
 
46
  return NextResponse.json({ error: 'no prompt provided' }, { status: 400 });
47
  }
48
 
49
+ let width = 512
50
+ try {
51
+ const rawString = decodeURIComponent(query?.w?.toString() || "").trim()
52
+ width = getValidNumber(rawString, 256, 8192, 512)
53
+ } catch (err) {}
54
+
55
+ let height = 288
56
+ try {
57
+ const rawString = decodeURIComponent(query?.h?.toString() || "").trim()
58
+ height = getValidNumber(rawString, 256, 8192, 288)
59
+ } catch (err) {}
60
+
61
+
62
  let format = "binary"
63
  try {
64
  const f = decodeURIComponent(query?.f?.toString() || "").trim()
 
75
  nbFrames: 1,
76
  nbFPS: 1,
77
  nbSteps: 8,
78
+ width,
79
+ height,
80
  turbo: true,
81
  shouldRenewCache: true,
82
  seed: generateSeed()
src/app/api/resolvers/video/route.ts CHANGED
@@ -1,17 +1,43 @@
1
  import { NextResponse, NextRequest } from "next/server"
2
  import queryString from "query-string"
 
 
3
 
4
  import { newRender, getRender } from "../../providers/videochain/renderWithVideoChain"
5
  import { generateSeed } from "@/lib/utils/generateSeed"
6
  import { sleep } from "@/lib/utils/sleep"
7
  import { getNegativePrompt, getPositivePrompt } from "../../utils/imagePrompts"
8
  import { getContentType } from "@/lib/data/getContentType"
 
 
 
 
9
 
10
  export async function GET(req: NextRequest) {
11
 
12
  const qs = queryString.parseUrl(req.url || "")
13
  const query = (qs || {}).query
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  let prompt = ""
17
  try {
@@ -22,6 +48,19 @@ export async function GET(req: NextRequest) {
22
  return NextResponse.json({ error: 'no prompt provided' }, { status: 400 });
23
  }
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  let format = "binary"
26
  try {
27
  const f = decodeURIComponent(query?.f?.toString() || "").trim()
@@ -40,19 +79,52 @@ export async function GET(req: NextRequest) {
40
  // ATTENTION: changing those will slow things to 5-6s of loading time (compared to 3-4s)
41
  // and with no real visible change
42
 
43
- nbFrames: 20, // apparently the model can only do 2 seconds at 10, so be it
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
- nbFPS: 10,
 
 
46
 
47
  // possibles values are 1, 2, 4, and 8
48
- // but I don't see much improvements with 8 to be honest
49
- // the best one seems to be 4
 
50
  nbSteps: 4,
51
 
52
  // this corresponds roughly to 16:9
53
  // which is the aspect ratio video used by AiTube
54
 
55
- // unfortunately, this is too compute intensive, so we have to take half of that
 
 
 
56
  // width: 1024,
57
  // height: 576,
58
 
@@ -68,10 +140,15 @@ export async function GET(req: NextRequest) {
68
  //
69
  // that's not the only constraint: you also need to respect this:
70
  // `height` and `width` have to be divisible by 8 (use 32 to be safe)
71
- // width: 512,
72
- // height: 288,
73
- width: 456, // 512,
74
- height: 256, // 288,
 
 
 
 
 
75
 
76
  turbo: true, // without much effect for videos as of now, as we only supports turbo (AnimateDiff Lightning)
77
  shouldRenewCache: true,
 
1
  import { NextResponse, NextRequest } from "next/server"
2
  import queryString from "query-string"
3
+ import { createSecretKey } from "crypto"
4
+ import { jwtVerify } from "jose"
5
 
6
  import { newRender, getRender } from "../../providers/videochain/renderWithVideoChain"
7
  import { generateSeed } from "@/lib/utils/generateSeed"
8
  import { sleep } from "@/lib/utils/sleep"
9
  import { getNegativePrompt, getPositivePrompt } from "../../utils/imagePrompts"
10
  import { getContentType } from "@/lib/data/getContentType"
11
+ import { whoAmI, WhoAmIUser } from "@huggingface/hub"
12
+ import { getValidNumber } from "@/lib/utils/getValidNumber"
13
+
14
+ const secretKey = createSecretKey(`${process.env.API_SECRET_JWT_KEY || ""}`, 'utf-8');
15
 
16
  export async function GET(req: NextRequest) {
17
 
18
  const qs = queryString.parseUrl(req.url || "")
19
  const query = (qs || {}).query
20
 
21
+ /*
22
+ TODO Julian: check the validity of the JWT token
23
+ let token = ""
24
+ try {
25
+ token = decodeURIComponent(query?.t?.toString() || "").trim()
26
+
27
+ // verify token
28
+ const { payload, protectedHeader } = await jwtVerify(token, secretKey, {
29
+ issuer: `${process.env.API_SECRET_JWT_ISSUER || ""}`, // issuer
30
+ audience: `${process.env.API_SECRET_JWT_AUDIENCE || ""}`, // audience
31
+ });
32
+ // log values to console
33
+ console.log(payload);
34
+ console.log(protectedHeader);
35
+ } catch (err) {
36
+ // token verification failed
37
+ console.log("Token is invalid");
38
+ return NextResponse.json({ error: `access denied ${err}` }, { status: 400 });
39
+ }
40
+ */
41
 
42
  let prompt = ""
43
  try {
 
48
  return NextResponse.json({ error: 'no prompt provided' }, { status: 400 });
49
  }
50
 
51
+ let width = 512
52
+ try {
53
+ const rawString = decodeURIComponent(query?.w?.toString() || "").trim()
54
+ width = getValidNumber(rawString, 256, 8192, 512)
55
+ } catch (err) {}
56
+
57
+ let height = 288
58
+ try {
59
+ const rawString = decodeURIComponent(query?.h?.toString() || "").trim()
60
+ height = getValidNumber(rawString, 256, 8192, 288)
61
+ } catch (err) {}
62
+
63
+
64
  let format = "binary"
65
  try {
66
  const f = decodeURIComponent(query?.f?.toString() || "").trim()
 
79
  // ATTENTION: changing those will slow things to 5-6s of loading time (compared to 3-4s)
80
  // and with no real visible change
81
 
82
+ // ATTENTION! if you change those values,
83
+ // please make sure that the backend API can support them,
84
+ // and also make sure to update the Zustand store values in the frontend:
85
+ // videoModelFPS: number
86
+ // videoModelNumOfFrames: number
87
+ // videoModelDurationInSec: number
88
+ //
89
+ // note: internally, the model can only do 16 frames at 10 FPS
90
+ // (1.6 second of video)
91
+ // but I have added a FFmpeg interpolation step, which adds some
92
+ // overhead (2-3 secs) but at least can help smooth things out, or make
93
+ // them artificially longer
94
+
95
+ // those settings are pretty good, takes about 2.9,, 3.1 seconds to compute
96
+ // represents 3 secs of 16fps
97
+
98
+
99
+ // with those parameters, we can generate a 2.584s long video at 24 FPS
100
+ // note that there is a overhead due to smoothing,
101
+ // on the A100 it takes betwen 5.3 and 7 seconds to compute
102
+ // although it will appear a bit "slo-mo"
103
+ // since the original is a 1.6s long video at 10 FPS
104
+ nbFrames: 80,
105
+ nbFPS: 24,
106
+
107
+
108
+ // nbFrames: 48,
109
+ // nbFPS: 24,
110
 
111
+ // it generated about:
112
+ // 24 frames
113
+ // 2.56s run time
114
 
115
  // possibles values are 1, 2, 4, and 8
116
+ // but with 2 steps the video "flashes" and it creates monstruosity
117
+ // like fishes with 2 tails etc
118
+ // and with 8 steps I don't see much improvements with 8 to be honest
119
  nbSteps: 4,
120
 
121
  // this corresponds roughly to 16:9
122
  // which is the aspect ratio video used by AiTube
123
 
124
+ // unfortunately, this is too compute intensive,
125
+ // and it creates monsters like two-headed fishes
126
+ // (although this artifact could probably be fixed with more steps,
127
+ // but we cannot afford those)
128
  // width: 1024,
129
  // height: 576,
130
 
 
140
  //
141
  // that's not the only constraint: you also need to respect this:
142
  // `height` and `width` have to be divisible by 8 (use 32 to be safe)
143
+ width,
144
+ height,
145
+
146
+ // we save about 500ms if we go below,
147
+ // but there we will be some deformed artifacts as the model
148
+ // doesn't perform well below 512px
149
+ // it also makes things more "flashy"
150
+ // width: 456, // 512,
151
+ // height: 256, // 288,
152
 
153
  turbo: true, // without much effect for videos as of now, as we only supports turbo (AnimateDiff Lightning)
154
  shouldRenewCache: true,
src/app/dream/{page.tsx → spoiler.tsx} RENAMED
@@ -1,18 +1,20 @@
1
-
2
-
3
  import { LatentQueryProps } from "@/types/general"
4
 
5
  import { Main } from "../main"
6
- import { searchResultToMediaInfo } from "../api/generators/search/searchResultToMediaInfo"
7
- import { LatentSearchResult } from "../api/generators/search/types"
8
- import { serializeClap } from "@/lib/clap/serializeClap"
9
- import { getMockClap } from "@/lib/clap/getMockClap"
10
  import { clapToDataUri } from "@/lib/clap/clapToDataUri"
11
  import { getNewMediaInfo } from "../api/generators/search/getNewMediaInfo"
 
12
 
13
- export default async function DreamPage({ searchParams: {
14
- l: latentContent,
15
- } }: LatentQueryProps) {
 
 
 
 
 
 
16
 
17
  // const latentSearchResult = JSON.parse(atob(`${latentContent}`)) as LatentSearchResult
18
 
@@ -24,10 +26,13 @@ export default async function DreamPage({ searchParams: {
24
  const latentMedia = getNewMediaInfo()
25
 
26
  latentMedia.clapUrl = await clapToDataUri(
27
- getMockClap({showDisclaimer: true })
 
 
 
28
  )
29
 
30
  return (
31
- <Main latentMedia={latentMedia} />
32
- )
33
  }
 
 
 
1
  import { LatentQueryProps } from "@/types/general"
2
 
3
  import { Main } from "../main"
4
+ import { generateClapFromSimpleStory } from "@/lib/clap/generateClapFromSimpleStory"
 
 
 
5
  import { clapToDataUri } from "@/lib/clap/clapToDataUri"
6
  import { getNewMediaInfo } from "../api/generators/search/getNewMediaInfo"
7
+ import { getToken } from "../api/auth/getToken"
8
 
9
+ // https://jmswrnr.com/blog/protecting-next-js-api-routes-query-parameters
10
+
11
+ export default async function DreamPage({
12
+ searchParams: {
13
+ l: latentContent,
14
+ },
15
+ ...rest
16
+ }: LatentQueryProps) {
17
+ const jwtToken = await getToken({ user: "anonymous" })
18
 
19
  // const latentSearchResult = JSON.parse(atob(`${latentContent}`)) as LatentSearchResult
20
 
 
26
  const latentMedia = getNewMediaInfo()
27
 
28
  latentMedia.clapUrl = await clapToDataUri(
29
+ generateClapFromSimpleStory({
30
+ showIntroPoweredByEngine: false,
31
+ showIntroDisclaimerAboutAI: false
32
+ })
33
  )
34
 
35
  return (
36
+ <Main latentMedia={latentMedia} jwtToken={jwtToken} />
37
+ )
38
  }
src/app/main.tsx CHANGED
@@ -29,6 +29,8 @@ import { PublicLatentMediaView } from "./views/public-latent-media-view"
29
  // one benefit of doing this is that we will able to add some animations/transitions
30
  // more easily
31
  export function Main({
 
 
32
  // view,
33
  publicMedia,
34
  publicMedias,
@@ -42,25 +44,31 @@ export function Main({
42
  publicTrack,
43
  channel,
44
  }: {
45
- // server side params
46
- // view?: InterfaceView
47
- publicMedia?: MediaInfo
48
- publicMedias?: MediaInfo[]
49
-
50
- latentMedia?: MediaInfo
51
- latentMedias?: MediaInfo[]
52
-
53
- publicChannelVideos?: MediaInfo[]
54
-
55
- publicTracks?: MediaInfo[]
56
- publicTrack?: MediaInfo
57
-
58
- channel?: ChannelInfo
 
 
 
 
 
59
  }) {
60
  // this could be also a parameter of main, where we pass this manually
61
  const pathname = usePathname()
62
  const router = useRouter()
63
 
 
64
  const setPublicMedia = useStore(s => s.setPublicMedia)
65
  const setView = useStore(s => s.setView)
66
  const setPathname = useStore(s => s.setPathname)
@@ -72,6 +80,12 @@ export function Main({
72
  const setPublicTracks = useStore(s => s.setPublicTracks)
73
  const setPublicTrack = useStore(s => s.setPublicTrack)
74
 
 
 
 
 
 
 
75
  useEffect(() => {
76
  if (!publicMedias?.length) { return }
77
  // note: it is important to ALWAYS set the current video to videoId
 
29
  // one benefit of doing this is that we will able to add some animations/transitions
30
  // more easily
31
  export function Main({
32
+ jwtToken,
33
+
34
  // view,
35
  publicMedia,
36
  publicMedias,
 
44
  publicTrack,
45
  channel,
46
  }: {
47
+ // token used to secure communications between the Next frontend and the Next API
48
+ // this doesn't necessarily mean the user has to be logged it:
49
+ // we can use this for anonymous visitors too.
50
+ jwtToken?: string
51
+
52
+ // server side params
53
+ // view?: InterfaceView
54
+ publicMedia?: MediaInfo
55
+ publicMedias?: MediaInfo[]
56
+
57
+ latentMedia?: MediaInfo
58
+ latentMedias?: MediaInfo[]
59
+
60
+ publicChannelVideos?: MediaInfo[]
61
+
62
+ publicTracks?: MediaInfo[]
63
+ publicTrack?: MediaInfo
64
+
65
+ channel?: ChannelInfo
66
  }) {
67
  // this could be also a parameter of main, where we pass this manually
68
  const pathname = usePathname()
69
  const router = useRouter()
70
 
71
+ const setJwtToken = useStore(s => s.setJwtToken)
72
  const setPublicMedia = useStore(s => s.setPublicMedia)
73
  const setView = useStore(s => s.setView)
74
  const setPathname = useStore(s => s.setPathname)
 
80
  const setPublicTracks = useStore(s => s.setPublicTracks)
81
  const setPublicTrack = useStore(s => s.setPublicTrack)
82
 
83
+ useEffect(() => {
84
+ if (typeof jwtToken !== "string" && !jwtToken) { return }
85
+ setJwtToken(jwtToken)
86
+ }, [jwtToken])
87
+
88
+
89
  useEffect(() => {
90
  if (!publicMedias?.length) { return }
91
  // note: it is important to ALWAYS set the current video to videoId
src/app/state/useStore.ts CHANGED
@@ -19,6 +19,9 @@ export const useStore = create<{
19
 
20
  setPathname: (pathname: string) => void
21
 
 
 
 
22
  searchQuery: string
23
  setSearchQuery: (searchQuery?: string) => void
24
 
@@ -131,6 +134,11 @@ export const useStore = create<{
131
  set({ view: routes[pathname] || "not_found" })
132
  },
133
 
 
 
 
 
 
134
  searchAutocompleteQuery: "",
135
  setSearchAutocompleteQuery: (searchAutocompleteQuery?: string) => {
136
  set({ searchAutocompleteQuery })
 
19
 
20
  setPathname: (pathname: string) => void
21
 
22
+ jwtToken: string
23
+ setJwtToken: (jwtToken: string) => void
24
+
25
  searchQuery: string
26
  setSearchQuery: (searchQuery?: string) => void
27
 
 
134
  set({ view: routes[pathname] || "not_found" })
135
  },
136
 
137
+ jwtToken: "",
138
+ setJwtToken: (jwtToken: string) => {
139
+ set({ jwtToken })
140
+ },
141
+
142
  searchAutocompleteQuery: "",
143
  setSearchAutocompleteQuery: (searchAutocompleteQuery?: string) => {
144
  set({ searchAutocompleteQuery })
src/components/interface/latent-engine/components/content-layer/index.tsx CHANGED
@@ -26,7 +26,7 @@ export const ContentLayer = forwardRef(function ContentLayer({
26
  ref={ref}
27
  onClick={onClick}
28
  >
29
- <div className="h-full aspect-video opacity-60">
30
  {children}
31
  </div>
32
  </div>
 
26
  ref={ref}
27
  onClick={onClick}
28
  >
29
+ <div className="h-full aspect-video opacity-100">
30
  {children}
31
  </div>
32
  </div>
src/components/interface/latent-engine/components/{disclaimers/this-is-ai.tsx → intros/ai-content-disclaimer.tsx} RENAMED
@@ -4,12 +4,11 @@ import React from "react"
4
  import { cn } from "@/lib/utils/cn"
5
 
6
  import { arimoBold, arimoNormal } from "@/lib/fonts"
7
- import { ClapStreamType } from "@/lib/clap/types"
8
 
9
- export function ThisIsAI({
10
- streamType,
11
  }: {
12
- streamType?: ClapStreamType
13
  } = {}) {
14
 
15
  return (
@@ -59,7 +58,7 @@ export function ThisIsAI({
59
  */
60
  } content
61
  </div> {
62
- streamType !== "static"
63
  ? "will be"
64
  : "has been"
65
  } <div className={cn(`
 
4
  import { cn } from "@/lib/utils/cn"
5
 
6
  import { arimoBold, arimoNormal } from "@/lib/fonts"
 
7
 
8
+ export function AIContentDisclaimer({
9
+ isInteractive = false,
10
  }: {
11
+ isInteractive?: boolean
12
  } = {}) {
13
 
14
  return (
 
58
  */
59
  } content
60
  </div> {
61
+ isInteractive
62
  ? "will be"
63
  : "has been"
64
  } <div className={cn(`
src/components/interface/latent-engine/components/intros/latent-engine.png ADDED
src/components/interface/latent-engine/components/intros/powered-by.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react"
2
+ import Image from 'next/image'
3
+
4
+ import { cn } from "@/lib/utils/cn"
5
+
6
+ import latentEngineLogo from "./latent-engine.png"
7
+
8
+ export function PoweredBy() {
9
+
10
+ return (
11
+ <div className={cn(`
12
+ flex flex-col flex-1
13
+ items-center justify-center
14
+ w-full h-full
15
+
16
+ bg-black
17
+ `,
18
+ )}>
19
+
20
+ <div className={cn(`
21
+ flex flex-col items-center justify-center
22
+ `)}>
23
+ <Image
24
+ src={latentEngineLogo}
25
+ alt="Latent Engine logo"
26
+ className="w-[80%]"
27
+ />
28
+ </div>
29
+ </div>
30
+ )
31
+ }
src/components/interface/latent-engine/core/engine.tsx CHANGED
@@ -1,16 +1,20 @@
1
  "use client"
2
 
3
  import React, { MouseEventHandler, useEffect, useRef, useState } from "react"
 
4
 
5
  import { cn } from "@/lib/utils/cn"
 
 
 
6
 
7
- import { useLatentEngine } from "../store/useLatentEngine"
8
  import { PlayPauseButton } from "../components/play-pause-button"
9
- import { StreamTag } from "../../stream-tag"
10
  import { ContentLayer } from "../components/content-layer"
11
- import { MediaInfo } from "@/types/general"
12
- import { getMockClap } from "@/lib/clap/getMockClap"
13
- import { serializeClap } from "@/lib/clap/serializeClap"
14
 
15
  function LatentEngine({
16
  media,
@@ -22,19 +26,35 @@ function LatentEngine({
22
  height?: number
23
  className?: string
24
  }) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  const setContainerDimension = useLatentEngine(s => s.setContainerDimension)
26
  const isLoaded = useLatentEngine(s => s.isLoaded)
27
  const imagine = useLatentEngine(s => s.imagine)
28
  const open = useLatentEngine(s => s.open)
29
 
30
- const setImageElement = useLatentEngine(s => s.setImageElement)
31
- const setVideoElement = useLatentEngine(s => s.setVideoElement)
32
- const setSegmentationElement = useLatentEngine(s => s.setSegmentationElement)
33
-
34
- const simulationVideoPlaybackFPS = useLatentEngine(s => s.simulationVideoPlaybackFPS)
35
- const simulationRenderingTimeFPS = useLatentEngine(s => s.simulationRenderingTimeFPS)
36
 
37
- const streamType = useLatentEngine(s => s.streamType)
38
  const isStatic = useLatentEngine(s => s.isStatic)
39
  const isLive = useLatentEngine(s => s.isLive)
40
  const isInteractive = useLatentEngine(s => s.isInteractive)
@@ -42,11 +62,8 @@ function LatentEngine({
42
  const isPlaying = useLatentEngine(s => s.isPlaying)
43
  const togglePlayPause = useLatentEngine(s => s.togglePlayPause)
44
 
45
- const videoLayer = useLatentEngine(s => s.videoLayer)
46
- const segmentationLayer = useLatentEngine(s => s.segmentationLayer)
47
- const interfaceLayer = useLatentEngine(s => s.interfaceLayer)
48
- const videoElement = useLatentEngine(s => s.videoElement)
49
- const imageElement = useLatentEngine(s => s.imageElement)
50
 
51
  const onClickOnSegmentationLayer = useLatentEngine(s => s.onClickOnSegmentationLayer)
52
 
@@ -70,7 +87,7 @@ function LatentEngine({
70
  // there is a bug, we can't unpack the .clap when it's from a data-uri :/
71
 
72
  // open(mediaUrl)
73
- const mockClap = getMockClap()
74
  const mockArchive = await serializeClap(mockClap)
75
  // for some reason conversion to data uri doesn't work
76
  // const mockDataUri = await blobToDataUri(mockArchive, "application/x-gzip")
@@ -91,7 +108,7 @@ function LatentEngine({
91
  setOverlayVisible(!isPlayingRef.current)
92
  }
93
  clearTimeout(overlayTimerRef.current)
94
- }, 1000)
95
  }
96
 
97
  /*
@@ -109,32 +126,6 @@ function LatentEngine({
109
  }, [isPlaying])
110
  */
111
 
112
- useEffect(() => {
113
- if (!videoLayerRef.current) { return }
114
-
115
- // note how in both cases we are pulling from the videoLayerRef
116
- // that's because one day everything will be a video, but for now we
117
- // "fake it until we make it"
118
- const videoElements = Array.from(
119
- videoLayerRef.current.querySelectorAll('.latent-video')
120
- ) as HTMLVideoElement[]
121
- setVideoElement(videoElements.at(0))
122
-
123
- // images are used for simpler or static experiences
124
- const imageElements = Array.from(
125
- videoLayerRef.current.querySelectorAll('.latent-image')
126
- ) as HTMLImageElement[]
127
- setImageElement(imageElements.at(0))
128
-
129
-
130
- if (!segmentationLayerRef.current) { return }
131
-
132
- const segmentationElements = Array.from(
133
- segmentationLayerRef.current.querySelectorAll('.segmentation-canvas')
134
- ) as HTMLCanvasElement[]
135
- setSegmentationElement(segmentationElements.at(0))
136
-
137
- })
138
 
139
  useEffect(() => {
140
  setContainerDimension({ width: width || 256, height: height || 256 })
@@ -161,9 +152,26 @@ function LatentEngine({
161
  height={height}
162
  ref={videoLayerRef}
163
  onClick={onClickOnSegmentationLayer}
164
- >{videoLayer}</ContentLayer>
165
-
166
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  <ContentLayer
168
  className="pointer-events-none"
169
  width={width}
@@ -174,14 +182,20 @@ function LatentEngine({
174
  style={{ width, height }}
175
  ></canvas></ContentLayer>
176
 
 
177
  {/*
178
  <ContentLayer
179
  className="pointer-events-auto"
180
  width={width}
181
  height={height}
182
- >{interfaceLayer}</ContentLayer>
183
- */}
184
-
 
 
 
 
 
185
  {/* content overlay, with the gradient, buttons etc */}
186
  <div className={cn(`
187
  absolute
@@ -247,8 +261,8 @@ function LatentEngine({
247
  isToggledOn={isPlaying}
248
  onClick={togglePlayPause}
249
  />
250
- <StreamTag
251
- streamType={streamType}
252
  size="md"
253
  className=""
254
  />
@@ -266,8 +280,8 @@ function LatentEngine({
266
  TODO: put a fullscreen button (and mode) here
267
 
268
  */}
269
- <div className="mono text-xs text-center">playback: {Math.round(simulationVideoPlaybackFPS * 100) / 100} FPS</div>
270
- <div className="mono text-xs text-center">rendering: {Math.round(simulationRenderingTimeFPS * 100) / 100} FPS</div>
271
  </div>
272
  </div>
273
  </div>
 
1
  "use client"
2
 
3
  import React, { MouseEventHandler, useEffect, useRef, useState } from "react"
4
+ import { useLocalStorage } from "usehooks-ts"
5
 
6
  import { cn } from "@/lib/utils/cn"
7
+ import { MediaInfo } from "@/types/general"
8
+ import { serializeClap } from "@/lib/clap/serializeClap"
9
+ import { generateClapFromSimpleStory } from "@/lib/clap/generateClapFromSimpleStory"
10
 
11
+ import { useLatentEngine } from "./useLatentEngine"
12
  import { PlayPauseButton } from "../components/play-pause-button"
13
+ import { StaticOrInteractiveTag } from "../../static-or-interactive-tag"
14
  import { ContentLayer } from "../components/content-layer"
15
+ import { localStorageKeys } from "@/app/state/localStorageKeys"
16
+ import { defaultSettings } from "@/app/state/defaultSettings"
17
+ import { useStore } from "@/app/state/useStore"
18
 
19
  function LatentEngine({
20
  media,
 
26
  height?: number
27
  className?: string
28
  }) {
29
+ // used to prevent people from opening multiple sessions at the same time
30
+ // note: this should also be enforced with the Hugging Face ID
31
+ const [multiTabsLock, setMultiTabsLock] = useLocalStorage<number>(
32
+ "AI_TUBE_ENGINE_MULTI_TABS_LOCK",
33
+ Date.now()
34
+ )
35
+
36
+ const [huggingfaceApiKey, setHuggingfaceApiKey] = useLocalStorage<string>(
37
+ localStorageKeys.huggingfaceApiKey,
38
+ defaultSettings.huggingfaceApiKey
39
+ )
40
+
41
+ // note here how we transfer the info from one store to another
42
+ const jwtToken = useStore(s => s.jwtToken)
43
+ const setJwtToken = useLatentEngine(s => s.setJwtToken)
44
+ useEffect(() => {
45
+ setJwtToken(jwtToken)
46
+ }, [jwtToken])
47
+
48
+
49
  const setContainerDimension = useLatentEngine(s => s.setContainerDimension)
50
  const isLoaded = useLatentEngine(s => s.isLoaded)
51
  const imagine = useLatentEngine(s => s.imagine)
52
  const open = useLatentEngine(s => s.open)
53
 
54
+ const videoSimulationVideoPlaybackFPS = useLatentEngine(s => s.videoSimulationVideoPlaybackFPS)
55
+ const videoSimulationRenderingTimeFPS = useLatentEngine(s => s.videoSimulationRenderingTimeFPS)
 
 
 
 
56
 
57
+ const isLoop = useLatentEngine(s => s.isLoop)
58
  const isStatic = useLatentEngine(s => s.isStatic)
59
  const isLive = useLatentEngine(s => s.isLive)
60
  const isInteractive = useLatentEngine(s => s.isInteractive)
 
62
  const isPlaying = useLatentEngine(s => s.isPlaying)
63
  const togglePlayPause = useLatentEngine(s => s.togglePlayPause)
64
 
65
+ const videoLayers = useLatentEngine(s => s.videoLayers)
66
+ const interfaceLayers = useLatentEngine(s => s.interfaceLayers)
 
 
 
67
 
68
  const onClickOnSegmentationLayer = useLatentEngine(s => s.onClickOnSegmentationLayer)
69
 
 
87
  // there is a bug, we can't unpack the .clap when it's from a data-uri :/
88
 
89
  // open(mediaUrl)
90
+ const mockClap = generateClapFromSimpleStory()
91
  const mockArchive = await serializeClap(mockClap)
92
  // for some reason conversion to data uri doesn't work
93
  // const mockDataUri = await blobToDataUri(mockArchive, "application/x-gzip")
 
108
  setOverlayVisible(!isPlayingRef.current)
109
  }
110
  clearTimeout(overlayTimerRef.current)
111
+ }, 3000)
112
  }
113
 
114
  /*
 
126
  }, [isPlaying])
127
  */
128
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
  useEffect(() => {
131
  setContainerDimension({ width: width || 256, height: height || 256 })
 
152
  height={height}
153
  ref={videoLayerRef}
154
  onClick={onClickOnSegmentationLayer}
155
+ >{videoLayers.map(({ id }) => (
156
+ <video
157
+ key={id}
158
+ id={id}
159
+ style={{ width, height }}
160
+ className={cn(
161
+ `video-buffer`,
162
+ `video-buffer-${id}`,
163
+ )}
164
+ data-segment-id="0"
165
+ data-segment-start-at="0"
166
+ data-z-index-depth="0"
167
+ playsInline={true}
168
+ muted={true}
169
+ autoPlay={false}
170
+ loop={true}
171
+ src="/blanks/blank_1sec_512x288.webm"
172
+ />))}
173
+ </ContentLayer>
174
+
175
  <ContentLayer
176
  className="pointer-events-none"
177
  width={width}
 
182
  style={{ width, height }}
183
  ></canvas></ContentLayer>
184
 
185
+
186
  {/*
187
  <ContentLayer
188
  className="pointer-events-auto"
189
  width={width}
190
  height={height}
191
+ >{interfaceLayers.map(({ id, element }) => (
192
+ <div
193
+ key={id}
194
+ id={id}
195
+ style={{ width, height }}
196
+ className={`interface-layer-${id}`}>{element}</div>))}</ContentLayer>
197
+ */}
198
+
199
  {/* content overlay, with the gradient, buttons etc */}
200
  <div className={cn(`
201
  absolute
 
261
  isToggledOn={isPlaying}
262
  onClick={togglePlayPause}
263
  />
264
+ <StaticOrInteractiveTag
265
+ isInteractive={isInteractive}
266
  size="md"
267
  className=""
268
  />
 
280
  TODO: put a fullscreen button (and mode) here
281
 
282
  */}
283
+ <div className="mono text-xs text-center">playback: {Math.round(videoSimulationVideoPlaybackFPS * 100) / 100} FPS</div>
284
+ <div className="mono text-xs text-center">rendering: {Math.round(videoSimulationRenderingTimeFPS * 100) / 100} FPS</div>
285
  </div>
286
  </div>
287
  </div>
src/components/interface/latent-engine/core/{fetchLatentClap.ts → generators/fetchLatentClap.ts} RENAMED
File without changes
src/components/interface/latent-engine/core/{fetchLatentSearchResults.ts → generators/fetchLatentSearchResults.ts} RENAMED
File without changes
src/components/interface/latent-engine/core/prompts/getCharacterPrompt.ts ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ClapModel } from "@/lib/clap/types"
2
+
3
+ export function getCharacterPrompt(model: ClapModel): string {
4
+
5
+ let characterPrompt = ""
6
+ if (model.description) {
7
+ characterPrompt = [
8
+ // the label (character name) can help making the prompt more unique
9
+ // this might backfires however, if the name is
10
+ // something like "SUN", "SILVER" etc
11
+ // I'm not sure stable diffusion really needs this,
12
+ // so let's skip it for now (might still be useful for locations, though)
13
+ // we also want to avoid triggering "famous people" (BARBOSSA etc)
14
+ // model.label,
15
+
16
+ model.description
17
+ ].join(", ")
18
+ } else {
19
+ characterPrompt = [
20
+ model.gender !== "object" ? model.gender : "",
21
+ model.age ? `aged ${model.age}yo` : '',
22
+ model.label ? `named ${model.label}` : '',
23
+ ].map(i => i.trim()).filter(i => i).join(", ")
24
+ }
25
+ return characterPrompt
26
+ }
src/components/interface/latent-engine/core/prompts/getVideoPrompt.ts ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { ClapModel, ClapSegment } from "@/lib/clap/types"
2
+
3
+ import { deduplicatePrompt } from "../../utils/prompting/deduplicatePrompt"
4
+
5
+ import { getCharacterPrompt } from "./getCharacterPrompt"
6
+
7
+ /**
8
+ * Construct a video prompt from a list of active segments
9
+ *
10
+ * @param segments
11
+ * @returns
12
+ */
13
+ export function getVideoPrompt(
14
+ segments: ClapSegment[],
15
+ modelsById: Record<string, ClapModel>,
16
+ extraPositivePrompt: string[]
17
+ ): string {
18
+
19
+ // console.log("modelsById:", modelsById)
20
+
21
+ // to construct the video we need to collect all the segments describing it
22
+ // we ignore unrelated categories (music, dialogue) or non-prompt items (eg. an audio sample)
23
+ const tmp = segments
24
+ .filter(({ category, outputType }) => {
25
+ if (outputType === "audio") {
26
+ return false
27
+ }
28
+
29
+ if (category === "music" ||
30
+ category === "sound") {
31
+ return false
32
+ }
33
+
34
+ if (category === "event" ||
35
+ category === "interface" ||
36
+ category === "phenomenon"
37
+ ) {
38
+ return false
39
+ }
40
+
41
+ if (category === "splat" ||
42
+ category === "mesh" ||
43
+ category === "depth"
44
+ ) {
45
+ return false
46
+ }
47
+
48
+ if (category === "storyboard" ||
49
+ category === "video") {
50
+ return false
51
+ }
52
+
53
+ if (category === "transition") {
54
+ return false
55
+ }
56
+
57
+ return true
58
+ })
59
+
60
+ tmp.sort((a, b) => b.label.localeCompare(a.label))
61
+
62
+ let videoPrompt = tmp.map(segment => {
63
+ const model: ClapModel | undefined = modelsById[segment?.modelId || ""] || undefined
64
+
65
+ if (segment.category === "dialogue") {
66
+
67
+ // if we can't find the model, then we are unable
68
+ // to make any assumption about the gender, age or appearance
69
+ if (!model) {
70
+ console.log("ERROR: this is a dialogue, but couldn't find the model!")
71
+ return `portrait of a person speaking, blurry background, bokeh`
72
+ }
73
+
74
+ const characterTrigger = model?.triggerName || ""
75
+ const characterLabel = model?.label || ""
76
+ const characterDescription = model?.description || ""
77
+ const dialogueLine = segment?.prompt || ""
78
+
79
+ const characterPrompt = getCharacterPrompt(model)
80
+
81
+ // in the context of a video, we some something additional:
82
+ // we create a "bokeh" style
83
+ return `portrait of a person speaking, blurry background, bokeh, ${characterPrompt}`
84
+
85
+ } else if (segment.category === "location") {
86
+
87
+ // if we can't find the location's model, we default to returning the prompt
88
+ if (!model) {
89
+ console.log("ERROR: this is a location, but couldn't find the model!")
90
+ return segment.prompt
91
+ }
92
+
93
+ return model.description
94
+ } else {
95
+ return segment.prompt
96
+ }
97
+ }).filter(x => x)
98
+
99
+ videoPrompt = videoPrompt.concat([
100
+ ...extraPositivePrompt
101
+ ])
102
+
103
+ return deduplicatePrompt(videoPrompt.join(", "))
104
+ }
src/components/interface/latent-engine/core/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ClapProject, ClapSegment, ClapStreamType } from "@/lib/clap/types"
2
  import { InteractiveSegmenterResult } from "@mediapipe/tasks-vision"
3
  import { MouseEventHandler, ReactNode } from "react"
4
 
@@ -24,18 +24,31 @@ export type LayerCategory =
24
  | "video"
25
  | "splat"
26
 
27
- export type LatentComponentResolver = (segment: ClapSegment, clap: ClapProject) => Promise<JSX.Element>
 
 
 
 
 
28
 
29
  export type LatentEngineStore = {
 
 
 
 
 
30
  width: number
31
  height: number
32
 
33
  clap: ClapProject
34
  debug: boolean
35
 
36
- streamType: ClapStreamType
 
 
37
 
38
  // just some aliases for convenience
 
39
  isStatic: boolean
40
  isLive: boolean
41
  isInteractive: boolean
@@ -51,46 +64,56 @@ export type LatentEngineStore = {
51
  isPlaying: boolean
52
  isPaused: boolean
53
 
54
- simulationPromise?: Promise<void>
55
- simulationPending: boolean // used as a "lock"
56
- simulationStartedAt: number
57
- simulationEndedAt: number
58
- simulationDurationInMs: number
59
- simulationVideoPlaybackFPS: number
60
- simulationRenderingTimeFPS: number
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
  renderingIntervalId: NodeJS.Timeout | string | number | undefined
63
  renderingIntervalDelayInMs: number
 
 
 
 
 
 
 
 
 
 
 
64
 
65
  positionInMs: number
66
  durationInMs: number
67
 
68
- videoLayerElement?: HTMLDivElement
69
- imageElement?: HTMLImageElement
70
- videoElement?: HTMLVideoElement
71
- segmentationElement?: HTMLCanvasElement
72
 
73
- videoLayer: ReactNode
74
- videoBuffer: "A" | "B"
75
- videoBufferA: ReactNode
76
- videoBufferB: ReactNode
77
 
78
- segmentationLayer: ReactNode
79
-
80
- interfaceLayer: ReactNode
81
- interfaceBuffer: "A" | "B"
82
- interfaceBufferA: ReactNode
83
- interfaceBufferB: ReactNode
84
 
85
  setContainerDimension: ({ width, height }: { width: number; height: number }) => void
86
  imagine: (prompt: string) => Promise<void>
87
  open: (src?: string | ClapProject | Blob) => Promise<void>
88
 
89
- setVideoLayerElement: (videoLayerElement?: HTMLDivElement) => void
90
- setImageElement: (imageElement?: HTMLImageElement) => void
91
- setVideoElement: (videoElement?: HTMLVideoElement) => void
92
- setSegmentationElement: (segmentationElement?: HTMLCanvasElement) => void
93
-
94
  processClickOnSegment: (data: InteractiveSegmenterResult) => void
95
  onClickOnSegmentationLayer: MouseEventHandler<HTMLDivElement>
96
 
@@ -98,8 +121,14 @@ export type LatentEngineStore = {
98
  play: () => boolean
99
  pause: () => boolean
100
 
101
- // a slow rendering function (async - might call a third party LLM)
102
- runSimulationLoop: () => Promise<void>
 
 
 
 
 
 
103
 
104
  // a fast rendering function; whose sole role is to filter the component
105
  // list to put into the buffer the one that should be displayed
 
1
+ import { ClapProject, ClapSegment } from "@/lib/clap/types"
2
  import { InteractiveSegmenterResult } from "@mediapipe/tasks-vision"
3
  import { MouseEventHandler, ReactNode } from "react"
4
 
 
24
  | "video"
25
  | "splat"
26
 
27
+ export type LatentComponentResolver = (segment: ClapSegment, clap: ClapProject) => Promise<LayerElement>
28
+
29
+ export type LayerElement = {
30
+ id: string;
31
+ element: JSX.Element;
32
+ }
33
 
34
  export type LatentEngineStore = {
35
+ // the token use to communicate with the NextJS backend
36
+ // note that this isn't the Hugging Face token,
37
+ // it is something more anynomous
38
+ jwtToken: string
39
+
40
  width: number
41
  height: number
42
 
43
  clap: ClapProject
44
  debug: boolean
45
 
46
+ // whether the engine is headless or not
47
+ // (pure chatbot apps won't need the live UI for instance)
48
+ headless: boolean
49
 
50
  // just some aliases for convenience
51
+ isLoop: boolean
52
  isStatic: boolean
53
  isLive: boolean
54
  isInteractive: boolean
 
64
  isPlaying: boolean
65
  isPaused: boolean
66
 
67
+ videoSimulationPromise?: Promise<void>
68
+ videoSimulationPending: boolean // used as a "lock"
69
+ videoSimulationStartedAt: number
70
+ videoSimulationEndedAt: number
71
+ videoSimulationDurationInMs: number
72
+ videoSimulationVideoPlaybackFPS: number
73
+ videoSimulationRenderingTimeFPS: number
74
+
75
+ interfaceSimulationPromise?: Promise<void>
76
+ interfaceSimulationPending: boolean // used as a "lock"
77
+ interfaceSimulationStartedAt: number
78
+ interfaceSimulationEndedAt: number
79
+ interfaceSimulationDurationInMs: number
80
+
81
+ entitySimulationPromise?: Promise<void>
82
+ entitySimulationPending: boolean // used as a "lock"
83
+ entitySimulationStartedAt: number
84
+ entitySimulationEndedAt: number
85
+ entitySimulationDurationInMs: number
86
 
87
  renderingIntervalId: NodeJS.Timeout | string | number | undefined
88
  renderingIntervalDelayInMs: number
89
+ renderingLastRenderAt: number
90
+
91
+ // for our calculations to be correct
92
+ // those need to match the actual output from the API
93
+ // don't trust the parameters you send to the API,
94
+ // instead check the *actual* values with VLC!!
95
+ videoModelFPS: number
96
+ videoModelNumOfFrames: number
97
+ videoModelDurationInSec: number
98
+
99
+ playbackSpeed: number
100
 
101
  positionInMs: number
102
  durationInMs: number
103
 
104
+ // this is the "buffer size"
105
+ videoLayers: LayerElement[]
106
+ videoElements: HTMLVideoElement[]
 
107
 
108
+ interfaceLayers: LayerElement[]
 
 
 
109
 
110
+ setJwtToken: (jwtToken: string) => void
 
 
 
 
 
111
 
112
  setContainerDimension: ({ width, height }: { width: number; height: number }) => void
113
  imagine: (prompt: string) => Promise<void>
114
  open: (src?: string | ClapProject | Blob) => Promise<void>
115
 
116
+ setVideoElements: (videoElements?: HTMLVideoElement[]) => void
 
 
 
 
117
  processClickOnSegment: (data: InteractiveSegmenterResult) => void
118
  onClickOnSegmentationLayer: MouseEventHandler<HTMLDivElement>
119
 
 
121
  play: () => boolean
122
  pause: () => boolean
123
 
124
+ // a slow simulation function (async - might call a third party LLM)
125
+ runVideoSimulationLoop: () => Promise<void>
126
+
127
+ // a slow simulation function (async - might call a third party LLM)
128
+ runInterfaceSimulationLoop: () => Promise<void>
129
+
130
+ // a slow simulation function (async - might call a third party LLM)
131
+ runEntitySimulationLoop: () => Promise<void>
132
 
133
  // a fast rendering function; whose sole role is to filter the component
134
  // list to put into the buffer the one that should be displayed
src/components/interface/latent-engine/{store → core}/useLatentEngine.ts RENAMED
@@ -1,29 +1,43 @@
1
 
2
  import { create } from "zustand"
3
 
4
- import { ClapProject } from "@/lib/clap/types"
5
  import { newClap } from "@/lib/clap/newClap"
6
  import { sleep } from "@/lib/utils/sleep"
7
  // import { getSegmentationCanvas } from "@/lib/on-device-ai/getSegmentationCanvas"
8
 
9
- import { LatentEngineStore } from "../core/types"
10
  import { resolveSegments } from "../resolvers/resolveSegments"
11
- import { fetchLatentClap } from "../core/fetchLatentClap"
12
  import { dataUriToBlob } from "@/app/api/utils/dataUriToBlob"
13
  import { parseClap } from "@/lib/clap/parseClap"
14
  import { InteractiveSegmenterResult, MPMask } from "@mediapipe/tasks-vision"
15
  import { segmentFrame } from "@/lib/on-device-ai/segmentFrameOnClick"
16
- import { drawSegmentation } from "../core/drawSegmentation"
17
  import { filterImage } from "@/lib/on-device-ai/filterImage"
 
 
 
 
 
 
 
 
 
 
18
 
19
  export const useLatentEngine = create<LatentEngineStore>((set, get) => ({
20
- width: 1024,
21
- height: 576,
 
 
22
 
23
  clap: newClap(),
24
  debug: true,
25
 
26
- streamType: "static",
 
 
27
  isStatic: false,
28
  isLive: false,
29
  isInteractive: false,
@@ -37,36 +51,73 @@ export const useLatentEngine = create<LatentEngineStore>((set, get) => ({
37
  hasDisclaimer: true,
38
  hasPresentedDisclaimer: false,
39
 
40
- simulationPromise: undefined,
41
- simulationPending: false,
42
- simulationStartedAt: performance.now(),
43
- simulationEndedAt: performance.now(),
44
- simulationDurationInMs: 0,
45
- simulationVideoPlaybackFPS: 0,
46
- simulationRenderingTimeFPS: 0,
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
  renderingIntervalId: undefined,
49
- renderingIntervalDelayInMs: 2000, // 2 sec
50
-
51
- positionInMs: 0,
52
- durationInMs: 0,
53
 
54
- videoLayerElement: undefined,
55
- imageElement: undefined,
56
- videoElement: undefined,
57
- segmentationElement: undefined,
58
-
59
- videoLayer: undefined,
60
- videoBuffer: "A",
61
- videoBufferA: null,
62
- videoBufferB: undefined,
63
 
64
- segmentationLayer: undefined,
65
 
66
- interfaceLayer: undefined,
67
- interfaceBuffer: "A",
68
- interfaceBufferA: undefined,
69
- interfaceBufferB: undefined,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
  setContainerDimension: ({ width, height }: { width: number; height: number }) => {
72
  set({
@@ -117,27 +168,24 @@ export const useLatentEngine = create<LatentEngineStore>((set, get) => ({
117
  }
118
 
119
  if (!clap) { return }
120
-
121
  set({
122
  clap,
123
  isLoading: false,
124
  isLoaded: true,
125
- streamType: clap.meta.streamType,
126
- isStatic: clap.meta.streamType !== "interactive" && clap.meta.streamType !== "live",
127
- isLive: clap.meta.streamType === "live",
128
- isInteractive: clap.meta.streamType === "interactive",
129
  })
130
  },
131
 
132
- setVideoLayerElement: (videoLayerElement?: HTMLDivElement) => { set({ videoLayerElement }) },
133
- setImageElement: (imageElement?: HTMLImageElement) => { set({ imageElement }) },
134
- setVideoElement: (videoElement?: HTMLVideoElement) => { set({ videoElement }) },
135
- setSegmentationElement: (segmentationElement?: HTMLCanvasElement) => { set({ segmentationElement }) },
136
 
137
  processClickOnSegment: (result: InteractiveSegmenterResult) => {
138
  console.log(`processClickOnSegment: user clicked on something:`, result)
139
 
140
- const { videoElement, imageElement, segmentationElement, debug } = get()
141
 
142
  if (!result?.categoryMask) {
143
  if (debug) {
@@ -151,10 +199,20 @@ export const useLatentEngine = create<LatentEngineStore>((set, get) => ({
151
  console.log(`processClickOnSegment: callling drawSegmentation`)
152
  }
153
 
 
 
 
 
 
 
 
 
 
 
154
  const canvasMask: HTMLCanvasElement = drawSegmentation({
155
  mask: result.categoryMask,
156
  canvas: segmentationElement,
157
- backgroundImage: imageElement,
158
  fillStyle: "rgba(255, 255, 255, 1.0)"
159
  })
160
  // TODO: read the canvas te determine on what the user clicked
@@ -174,12 +232,15 @@ export const useLatentEngine = create<LatentEngineStore>((set, get) => ({
174
  },
175
  onClickOnSegmentationLayer: (event) => {
176
 
177
- const { videoElement, imageElement, segmentationLayer, segmentationElement, debug } = get()
178
  if (debug) {
179
  console.log("onClickOnSegmentationLayer")
180
  }
181
- // TODO use the videoElement if this is is video!
182
- if (!videoElement) { return }
 
 
 
183
 
184
  const box = event.currentTarget.getBoundingClientRect()
185
 
@@ -188,27 +249,36 @@ export const useLatentEngine = create<LatentEngineStore>((set, get) => ({
188
 
189
  const x = px / box.width
190
  const y = py / box.height
191
- console.log(`onClickOnSegmentationLayer: user clicked on `, { x, y, px, py, box, videoElement })
192
 
193
  const fn = async () => {
194
- const results: InteractiveSegmenterResult = await segmentFrame(videoElement, x, y)
 
195
  get().processClickOnSegment(results)
196
  }
197
  fn()
198
  },
199
 
200
  togglePlayPause: (): boolean => {
201
- const { isLoaded, isPlaying, renderingIntervalId, videoElement } = get()
202
  if (!isLoaded) { return false }
203
 
204
  const newValue = !isPlaying
205
 
206
  clearInterval(renderingIntervalId)
207
 
 
 
 
 
 
 
 
208
  if (newValue) {
209
- if (videoElement) {
210
  try {
211
- videoElement.play()
 
212
  } catch (err) {
213
  console.error(`togglePlayPause: failed to start the video (${err})`)
214
  }
@@ -218,9 +288,10 @@ export const useLatentEngine = create<LatentEngineStore>((set, get) => ({
218
  renderingIntervalId: setTimeout(() => { get().runRenderingLoop() }, 0)
219
  })
220
  } else {
221
- if (videoElement) {
222
  try {
223
- videoElement.pause()
 
224
  } catch (err) {
225
  console.error(`togglePlayPause: failed to pause the video (${err})`)
226
  }
@@ -260,129 +331,254 @@ export const useLatentEngine = create<LatentEngineStore>((set, get) => ({
260
  },
261
 
262
  // a slow rendering function (async - might call a third party LLM)
263
- runSimulationLoop: async () => {
264
  const {
265
  isLoaded,
266
  isPlaying,
267
  clap,
268
- segmentationLayer,
269
- imageElement,
270
- videoElement,
271
- height,
272
- width,
 
 
273
  } = get()
274
 
275
  if (!isLoaded || !isPlaying) {
276
-
277
- set({
278
- simulationPending: false,
279
- })
280
-
281
  return
282
  }
283
 
284
  set({
285
- simulationPending: true,
286
- simulationStartedAt: performance.now(),
287
  })
288
 
289
- try {
290
 
291
- /*
292
- // console.log("doing stuff")
293
- let timestamp = performance.now()
294
-
295
- if (imageElement) {
296
- // console.log("we have an image element:", imageElement)
297
- const segmentationLayer = await getSegmentationCanvas({
298
- frame: imageElement,
299
- timestamp,
300
- width,
301
- height,
302
- })
303
- set({ segmentationLayer })
 
 
 
 
 
 
 
 
 
304
  }
305
- */
306
-
307
- // await sleep(500)
308
 
309
- // note: since we are asynchronous, we need to regularly check if
310
- // the user asked to pause the system or no
311
- if (get().isPlaying) {
312
- // console.log(`runSimulationLoop: rendering video content layer..`)
313
- // we only grab the first one
 
 
 
 
 
 
 
 
314
 
 
 
 
 
315
 
 
316
 
317
- const videoLayer = (await resolveSegments(clap, "video", 1)).at(0)
 
 
 
318
 
319
- if (get().isPlaying) {
 
 
 
320
 
321
- set({
322
- videoLayer
323
- })
324
 
325
- const { videoElement, imageElement, segmentationElement } = get()
326
 
327
- if (videoElement) {
328
- // yes, it is a very a dirty trick
329
- // yes, it will look back
330
- videoElement.defaultPlaybackRate = 0.5
331
- }
332
 
333
- const canvas = drawSegmentation({
334
- // no mask means this will effectively clear the canvas
335
- canvas: segmentationElement,
336
- backgroundImage: imageElement,
337
- })
338
-
339
 
340
- // console.log(`runSimulationLoop: rendered video content layer`)
341
- }
342
- }
343
 
344
- } catch (err) {
345
- console.error(`runSimulationLoop failed to render video layer ${err}`)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  }
347
 
 
 
 
 
 
348
  try {
349
  if (get().isPlaying) {
350
  // console.log(`runSimulationLoop: rendering UI layer..`)
351
 
352
- // note: for now we only display one element, to avoid handing a list of html elements
353
- const interfaceLayer = (await resolveSegments(clap, "interface", 1)).at(0)
 
 
 
354
  if (get().isPlaying) {
355
  set({
356
- interfaceLayer
357
  })
358
 
359
  // console.log(`runSimulationLoop: rendered UI layer`)
360
  }
361
  }
362
  } catch (err) {
363
- console.error(`runSimulationLoop failed to render UI layer ${err}`)
364
  }
365
 
366
- const simulationEndedAt = performance.now()
367
- const simulationDurationInMs = simulationEndedAt - get().simulationStartedAt
368
- const simulationDurationInSec =simulationDurationInMs / 1000
369
-
370
- // I've counted the frames manually, and we indeed have, in term of pure video playback,
371
- // 10 fps divided by 2 (the 0.5 playback factor)
372
- const videoFPS = 10
373
- const videoDurationInSec = 1
374
- const videoPlaybackSpeed = 0.5
375
- const simulationVideoPlaybackFPS = videoDurationInSec * videoFPS * videoPlaybackSpeed
376
- const simulationRenderingTimeFPS = (videoDurationInSec * videoFPS) / simulationDurationInSec
377
  set({
378
- simulationPending: false,
379
- simulationEndedAt,
380
- simulationDurationInMs,
381
- simulationVideoPlaybackFPS,
382
- simulationRenderingTimeFPS,
383
  })
384
  },
385
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
  // a fast sync rendering function; whose sole role is to filter the component
387
  // list to put into the buffer the one that should be displayed
388
  runRenderingLoop: () => {
@@ -391,32 +587,52 @@ export const useLatentEngine = create<LatentEngineStore>((set, get) => ({
391
  isPlaying,
392
  renderingIntervalId,
393
  renderingIntervalDelayInMs,
394
- simulationPromise,
395
- simulationPending,
396
- runSimulationLoop,
397
- imageElement,
398
- videoElement,
 
 
 
399
  } = get()
400
- if (!isLoaded) { return }
401
- if (!isPlaying) { return }
402
- try {
403
- // console.log(`runRenderingLoop: starting..`)
404
-
405
- // TODO: some operations with
406
- // console.log(`runRenderingLoop: ended`)
407
- } catch (err) {
408
- console.error(`runRenderingLoop failed ${err}`)
409
- }
 
 
 
410
  clearInterval(renderingIntervalId)
 
411
  set({
412
  isPlaying: true,
413
- simulationPromise: simulationPending ? simulationPromise : runSimulationLoop(),
 
414
 
 
415
  // TODO: use requestAnimationFrame somehow
416
  // https://developers.google.com/mediapipe/solutions/vision/image_segmenter/web_js
417
  renderingIntervalId: setTimeout(() => { get().runRenderingLoop() }, renderingIntervalDelayInMs)
418
  })
419
-
 
 
 
 
 
 
 
 
 
 
 
420
  },
421
 
422
  jumpTo: (positionInMs: number) => {
 
1
 
2
  import { create } from "zustand"
3
 
4
+ import { ClapModel, ClapProject } from "@/lib/clap/types"
5
  import { newClap } from "@/lib/clap/newClap"
6
  import { sleep } from "@/lib/utils/sleep"
7
  // import { getSegmentationCanvas } from "@/lib/on-device-ai/getSegmentationCanvas"
8
 
9
+ import { LatentEngineStore } from "./types"
10
  import { resolveSegments } from "../resolvers/resolveSegments"
11
+ import { fetchLatentClap } from "./generators/fetchLatentClap"
12
  import { dataUriToBlob } from "@/app/api/utils/dataUriToBlob"
13
  import { parseClap } from "@/lib/clap/parseClap"
14
  import { InteractiveSegmenterResult, MPMask } from "@mediapipe/tasks-vision"
15
  import { segmentFrame } from "@/lib/on-device-ai/segmentFrameOnClick"
16
+ import { drawSegmentation } from "../utils/canvas/drawSegmentation"
17
  import { filterImage } from "@/lib/on-device-ai/filterImage"
18
+ import { getZIndexDepth } from "../utils/data/getZIndexDepth"
19
+ import { getSegmentStartAt } from "../utils/data/getSegmentStartAt"
20
+ import { getSegmentId } from "../utils/data/getSegmentId"
21
+ import { getElementsSortedByStartAt } from "../utils/data/getElementsSortedByStartAt"
22
+ import { getSegmentEndAt } from "../utils/data/getSegmentEndAt"
23
+ import { getVideoPrompt } from "./prompts/getVideoPrompt"
24
+ import { setZIndexDepthId } from "../utils/data/setZIndexDepth"
25
+ import { setSegmentStartAt } from "../utils/data/setSegmentStartAt"
26
+ import { setSegmentEndAt } from "../utils/data/setSegmentEndAt"
27
+ import { setSegmentId } from "../utils/data/setSegmentId"
28
 
29
  export const useLatentEngine = create<LatentEngineStore>((set, get) => ({
30
+ jwtToken: "",
31
+
32
+ width: 512,
33
+ height: 288,
34
 
35
  clap: newClap(),
36
  debug: true,
37
 
38
+ headless: false, // false by default
39
+
40
+ isLoop: false,
41
  isStatic: false,
42
  isLive: false,
43
  isInteractive: false,
 
51
  hasDisclaimer: true,
52
  hasPresentedDisclaimer: false,
53
 
54
+ videoSimulationPromise: undefined,
55
+ videoSimulationPending: false,
56
+ videoSimulationStartedAt: performance.now(),
57
+ videoSimulationEndedAt: performance.now(),
58
+ videoSimulationDurationInMs: 0,
59
+ videoSimulationVideoPlaybackFPS: 0,
60
+ videoSimulationRenderingTimeFPS: 0,
61
+
62
+ interfaceSimulationPromise: undefined,
63
+ interfaceSimulationPending: false,
64
+ interfaceSimulationStartedAt: performance.now(),
65
+ interfaceSimulationEndedAt: performance.now(),
66
+ interfaceSimulationDurationInMs: 0,
67
+
68
+ entitySimulationPromise: undefined,
69
+ entitySimulationPending: false,
70
+ entitySimulationStartedAt: performance.now(),
71
+ entitySimulationEndedAt: performance.now(),
72
+ entitySimulationDurationInMs: 0,
73
 
74
  renderingIntervalId: undefined,
75
+ renderingIntervalDelayInMs: 150, // 0.2s
76
+ renderingLastRenderAt: performance.now(),
 
 
77
 
78
+ // for our calculations to be correct
79
+ // those need to match the actual output from the API
80
+ // don't trust the parameters you send to the API,
81
+ // instead check the *actual* values with VLC!!
82
+ videoModelFPS: 24,
83
+ videoModelNumOfFrames: 60, // 80,
84
+ videoModelDurationInSec: 2.584,
 
 
85
 
86
+ playbackSpeed: 1,
87
 
88
+ positionInMs: 0,
89
+ durationInMs: 0,
90
+
91
+ // this is the "buffer size"
92
+ videoLayers: [
93
+ {
94
+ id: "video-buffer-0",
95
+ element: null as unknown as JSX.Element,
96
+ },
97
+ {
98
+ id: "video-buffer-1",
99
+ element: null as unknown as JSX.Element,
100
+ },
101
+ /*
102
+ {
103
+ id: "video-buffer-2",
104
+ element: null as unknown as JSX.Element,
105
+ },
106
+ {
107
+ id: "video-buffer-3",
108
+ element: null as unknown as JSX.Element,
109
+ },
110
+ */
111
+ ],
112
+ videoElements: [],
113
+
114
+ interfaceLayers: [],
115
+
116
+ setJwtToken: (jwtToken: string) => {
117
+ set({
118
+ jwtToken
119
+ })
120
+ },
121
 
122
  setContainerDimension: ({ width, height }: { width: number; height: number }) => {
123
  set({
 
168
  }
169
 
170
  if (!clap) { return }
171
+
172
  set({
173
  clap,
174
  isLoading: false,
175
  isLoaded: true,
176
+ isLoop: clap.meta.isLoop,
177
+ isStatic: !clap.meta.isInteractive,
178
+ isLive: false,
179
+ isInteractive: clap.meta.isInteractive,
180
  })
181
  },
182
 
183
+ setVideoElements: (videoElements: HTMLVideoElement[] = []) => { set({ videoElements }) },
 
 
 
184
 
185
  processClickOnSegment: (result: InteractiveSegmenterResult) => {
186
  console.log(`processClickOnSegment: user clicked on something:`, result)
187
 
188
+ const { videoElements, debug } = get()
189
 
190
  if (!result?.categoryMask) {
191
  if (debug) {
 
199
  console.log(`processClickOnSegment: callling drawSegmentation`)
200
  }
201
 
202
+ const firstVisibleVideo = videoElements.find(element =>
203
+ getZIndexDepth(element) > 0
204
+ )
205
+
206
+ const segmentationElements = Array.from(
207
+ document.querySelectorAll('.segmentation-canvas')
208
+ ) as HTMLCanvasElement[]
209
+
210
+ const segmentationElement = segmentationElements.at(0)
211
+
212
  const canvasMask: HTMLCanvasElement = drawSegmentation({
213
  mask: result.categoryMask,
214
  canvas: segmentationElement,
215
+ backgroundImage: firstVisibleVideo,
216
  fillStyle: "rgba(255, 255, 255, 1.0)"
217
  })
218
  // TODO: read the canvas te determine on what the user clicked
 
232
  },
233
  onClickOnSegmentationLayer: (event) => {
234
 
235
+ const { videoElements, debug } = get()
236
  if (debug) {
237
  console.log("onClickOnSegmentationLayer")
238
  }
239
+
240
+ const firstVisibleVideo = videoElements.find(element =>
241
+ getZIndexDepth(element) > 0
242
+ )
243
+ if (!firstVisibleVideo) { return }
244
 
245
  const box = event.currentTarget.getBoundingClientRect()
246
 
 
249
 
250
  const x = px / box.width
251
  const y = py / box.height
252
+ console.log(`onClickOnSegmentationLayer: user clicked on `, { x, y, px, py, box, videoElements })
253
 
254
  const fn = async () => {
255
+ // todo julian: this should use the visible element instead
256
+ const results: InteractiveSegmenterResult = await segmentFrame(firstVisibleVideo, x, y)
257
  get().processClickOnSegment(results)
258
  }
259
  fn()
260
  },
261
 
262
  togglePlayPause: (): boolean => {
263
+ const { isLoaded, isPlaying, playbackSpeed, renderingIntervalId, videoElements } = get()
264
  if (!isLoaded) { return false }
265
 
266
  const newValue = !isPlaying
267
 
268
  clearInterval(renderingIntervalId)
269
 
270
+ const firstVisibleVideo = videoElements.find(element =>
271
+ getZIndexDepth(element) > 0
272
+ )
273
+
274
+ // Note Julian: we could also let the background scheduler
275
+ // (runRenderingLoop) do its work of advancing the cursor here
276
+
277
  if (newValue) {
278
+ if (firstVisibleVideo) {
279
  try {
280
+ firstVisibleVideo.playbackRate = playbackSpeed
281
+ firstVisibleVideo.play()
282
  } catch (err) {
283
  console.error(`togglePlayPause: failed to start the video (${err})`)
284
  }
 
288
  renderingIntervalId: setTimeout(() => { get().runRenderingLoop() }, 0)
289
  })
290
  } else {
291
+ if (firstVisibleVideo) {
292
  try {
293
+ firstVisibleVideo.playbackRate = playbackSpeed
294
+ firstVisibleVideo.pause()
295
  } catch (err) {
296
  console.error(`togglePlayPause: failed to pause the video (${err})`)
297
  }
 
331
  },
332
 
333
  // a slow rendering function (async - might call a third party LLM)
334
+ runVideoSimulationLoop: async () => {
335
  const {
336
  isLoaded,
337
  isPlaying,
338
  clap,
339
+ playbackSpeed,
340
+ positionInMs,
341
+ videoModelFPS,
342
+ videoModelNumOfFrames,
343
+ videoModelDurationInSec,
344
+ videoElements,
345
+ jwtToken,
346
  } = get()
347
 
348
  if (!isLoaded || !isPlaying) {
349
+ set({ videoSimulationPending: false })
 
 
 
 
350
  return
351
  }
352
 
353
  set({
354
+ videoSimulationPending: true,
355
+ videoSimulationStartedAt: performance.now(),
356
  })
357
 
358
+ const videosSortedByStartAt = getElementsSortedByStartAt(videoElements)
359
 
360
+ // videos whose timestamp is behind the current cursor
361
+ let toRecycle: HTMLVideoElement[] = []
362
+ let toPlay: HTMLVideoElement[] = []
363
+ let toPreload: HTMLVideoElement[] = []
364
+
365
+ for (let i = 0; i < videosSortedByStartAt.length; i++) {
366
+ const video = videosSortedByStartAt[i]
367
+
368
+ const segmentStartAt = getSegmentStartAt(video)
369
+ const segmentEndAt = getSegmentEndAt(video)
370
+
371
+ // this segment has been spent, it should be discared
372
+ if (segmentEndAt < positionInMs) {
373
+ toRecycle.push(video)
374
+ } else if (segmentStartAt < positionInMs) {
375
+ toPlay.push(video)
376
+ video.play()
377
+ setZIndexDepthId(video, 10)
378
+ } else {
379
+ toPreload.push(video)
380
+ video.pause()
381
+ setZIndexDepthId(video, 0)
382
  }
383
+ }
 
 
384
 
385
+ const videoDurationInMs = videoModelDurationInSec * 1000
386
+
387
+ // TODO julian: this is an approximation
388
+ // to grab the max number of segments
389
+ const maxBufferDurationInMs = positionInMs + (videoDurationInMs * 4)
390
+ console.log(`DEBUG: `, {
391
+ positionInMs,
392
+ videoModelDurationInSec,
393
+ videoDurationInMs,
394
+ "(videoDurationInMs * 4)": (videoDurationInMs * 4),
395
+ maxBufferDurationInMs,
396
+ segments: clap.segments
397
+ })
398
 
399
+ const prefilterSegmentsForPerformanceReasons = clap.segments.filter(s =>
400
+ s.startTimeInMs >= positionInMs &&
401
+ s.startTimeInMs < maxBufferDurationInMs
402
+ )
403
 
404
+ console.log(`prefilterSegmentsForPerformanceReasons: `, prefilterSegmentsForPerformanceReasons)
405
 
406
+ // this tells us how much time is left
407
+ let remainingTimeInMs = Math.max(0, clap.meta.durationInMs - positionInMs)
408
+ // to avoid interruptions, we should jump to the beginning of the project
409
+ // as soo as we are start playing back the "last" video segment
410
 
411
+ // now, we need to recycle spent videos,
412
+ // by discarding their content and replacing it with fresh one
413
+ //
414
+ // yes: I know the code is complex and not intuitive - sorry about that
415
 
416
+ // TODO Julian: use the Clap project to fill in those
417
+ const modelsById: Record<string, ClapModel> = {}
418
+ const extraPositivePrompt: string[] = []
419
 
420
+ let bufferAheadOfCurrentPositionInMs = positionInMs
421
 
422
+ for (let i = 0; i < toRecycle.length; i++) {
423
+ console.log(`got a spent video to recycle`)
424
+
425
+ // we select the segments in the current shot
 
426
 
427
+ const shotSegmentsToPreload = prefilterSegmentsForPerformanceReasons.filter(s =>
428
+ s.startTimeInMs >= bufferAheadOfCurrentPositionInMs &&
429
+ s.startTimeInMs < (bufferAheadOfCurrentPositionInMs + videoDurationInMs)
430
+ )
431
+
432
+ bufferAheadOfCurrentPositionInMs += videoDurationInMs
433
 
434
+ const prompt = getVideoPrompt(shotSegmentsToPreload, modelsById, extraPositivePrompt)
 
 
435
 
436
+ console.log(`video prompt: ${prompt}`)
437
+ // could also be the camera
438
+ // after all, we don't necessarily have a shot,
439
+ // this could also be a gaussian splat
440
+ const shotData = shotSegmentsToPreload.find(s => s.category === "video")
441
+
442
+ console.log(`shotData:`, shotData)
443
+
444
+ if (!prompt || !shotData) { continue }
445
+
446
+ const recycled = toRecycle[i]
447
+
448
+ recycled.pause()
449
+
450
+ setSegmentId(recycled, shotData.id)
451
+ setSegmentStartAt(recycled, shotData.startTimeInMs)
452
+ setSegmentEndAt(recycled, shotData.endTimeInMs)
453
+ setZIndexDepthId(recycled, 0)
454
+
455
+ // this is the best compromise for now in term of speed
456
+ const width = 512
457
+ const height = 288
458
+
459
+ // this is our magic trick: we let the browser do the token-secured,
460
+ // asynchronous and parallel video generation call for us
461
+ //
462
+ // one issue with this approach is that it hopes the video
463
+ // will be downloaded in time, but it's not an exact science
464
+ //
465
+ // first, generation time varies between 4sec and 7sec,
466
+ // then some people will get 300ms latency due to their ISP,
467
+ // and finally the video itself is a 150~200 Kb payload)
468
+ recycled.src = `/api/resolvers/video?t=${
469
+
470
+ // to prevent funny people from using this as a free, open-bar video API
471
+ // we have this system of token with a 24h expiration date
472
+ // we might even make it tighter in the future
473
+ jwtToken
474
+ }&w=${
475
+ width
476
+
477
+ }&h=${
478
+ height
479
+ }&p=${
480
+ // let's re-use the best ideas from the Latent Browser:
481
+ // a text uri equals a latent resource
482
+ encodeURIComponent(prompt)
483
+ }`
484
+
485
+ toPreload.push(recycled)
486
+ }
487
+
488
+ const videoSimulationEndedAt = performance.now()
489
+ const videoSimulationDurationInMs = videoSimulationEndedAt - get().videoSimulationStartedAt
490
+ const videoSimulationDurationInSec = videoSimulationDurationInMs / 1000
491
+
492
+ const videoSimulationVideoPlaybackFPS = videoModelFPS * playbackSpeed
493
+ const videoSimulationRenderingTimeFPS = videoModelNumOfFrames / videoSimulationDurationInSec
494
+ set({
495
+ videoSimulationPending: false,
496
+ videoSimulationEndedAt,
497
+ videoSimulationDurationInMs,
498
+ videoSimulationVideoPlaybackFPS,
499
+ videoSimulationRenderingTimeFPS,
500
+ })
501
+ },
502
+
503
+
504
+ // a slow rendering function (async - might call a third party LLM)
505
+ runInterfaceSimulationLoop: async () => {
506
+ const {
507
+ isLoaded,
508
+ isPlaying,
509
+ clap,
510
+ } = get()
511
+
512
+ if (!isLoaded || !isPlaying) {
513
+ set({ interfaceSimulationPending: false })
514
+ return
515
  }
516
 
517
+ set({
518
+ interfaceSimulationPending: true,
519
+ interfaceSimulationStartedAt: performance.now(),
520
+ })
521
+
522
  try {
523
  if (get().isPlaying) {
524
  // console.log(`runSimulationLoop: rendering UI layer..`)
525
 
526
+ // note: for now we only display one panel at a time,
527
+ // later we can try to see if we should handle more
528
+ // for nice gradient transition,
529
+ const interfaceLayers = await resolveSegments(clap, "interface", 1)
530
+
531
  if (get().isPlaying) {
532
  set({
533
+ interfaceLayers
534
  })
535
 
536
  // console.log(`runSimulationLoop: rendered UI layer`)
537
  }
538
  }
539
  } catch (err) {
540
+ console.error(`runInterfaceSimulationLoop failed to render UI layer ${err}`)
541
  }
542
 
543
+ const interfaceSimulationEndedAt = performance.now()
544
+ const interfaceSimulationDurationInMs = interfaceSimulationEndedAt - get().interfaceSimulationStartedAt
545
+
 
 
 
 
 
 
 
 
546
  set({
547
+ interfaceSimulationPending: false,
548
+ interfaceSimulationEndedAt,
549
+ interfaceSimulationDurationInMs,
 
 
550
  })
551
  },
552
 
553
+
554
+ // a slow rendering function (async - might call a third party LLM)
555
+ runEntitySimulationLoop: async () => {
556
+ const {
557
+ isLoaded,
558
+ isPlaying,
559
+ clap,
560
+ } = get()
561
+
562
+
563
+ if (!isLoaded || !isPlaying) {
564
+ set({ entitySimulationPending: false })
565
+ return
566
+ }
567
+
568
+ set({
569
+ entitySimulationPending: true,
570
+ entitySimulationStartedAt: performance.now(),
571
+ })
572
+
573
+ const entitySimulationEndedAt = performance.now()
574
+ const entitySimulationDurationInMs = entitySimulationEndedAt - get().entitySimulationStartedAt
575
+
576
+ set({
577
+ entitySimulationPending: false,
578
+ entitySimulationEndedAt,
579
+ entitySimulationDurationInMs,
580
+ })
581
+ },
582
  // a fast sync rendering function; whose sole role is to filter the component
583
  // list to put into the buffer the one that should be displayed
584
  runRenderingLoop: () => {
 
587
  isPlaying,
588
  renderingIntervalId,
589
  renderingIntervalDelayInMs,
590
+ renderingLastRenderAt,
591
+ positionInMs,
592
+ videoSimulationPending,
593
+ runVideoSimulationLoop,
594
+ interfaceSimulationPending,
595
+ runInterfaceSimulationLoop,
596
+ entitySimulationPending,
597
+ runEntitySimulationLoop,
598
  } = get()
599
+ if (!isLoaded || !isPlaying) { return }
600
+
601
+ // TODO julian: don't do this here, this is inneficient
602
+ const videoElements = Array.from(
603
+ document.querySelectorAll('.video-buffer')
604
+ ) as HTMLVideoElement[]
605
+
606
+ const newRenderingLastRenderAt = performance.now()
607
+ const elapsedInMs = newRenderingLastRenderAt - renderingLastRenderAt
608
+
609
+ // let's move inside the Clap file timeline
610
+ const newPositionInMs = positionInMs + elapsedInMs
611
+
612
  clearInterval(renderingIntervalId)
613
+
614
  set({
615
  isPlaying: true,
616
+ renderingLastRenderAt: newRenderingLastRenderAt,
617
+ positionInMs: newPositionInMs,
618
 
619
+ videoElements: videoElements,
620
  // TODO: use requestAnimationFrame somehow
621
  // https://developers.google.com/mediapipe/solutions/vision/image_segmenter/web_js
622
  renderingIntervalId: setTimeout(() => { get().runRenderingLoop() }, renderingIntervalDelayInMs)
623
  })
624
+
625
+ // note that having this second set() also helps us to make sure previously values are properly stored
626
+ // in the state when the simulation loop runs
627
+ if (!videoSimulationPending) {
628
+ set({ videoSimulationPromise: runVideoSimulationLoop() }) // <-- note: this is a fire-and-forget operation!
629
+ }
630
+ if (!interfaceSimulationPending) {
631
+ set({ interfaceSimulationPromise: runInterfaceSimulationLoop() }) // <-- note: this is a fire-and-forget operation!
632
+ }
633
+ if (!entitySimulationPending) {
634
+ set({ entitySimulationPromise: runEntitySimulationLoop() }) // <-- note: this is a fire-and-forget operation!
635
+ }
636
  },
637
 
638
  jumpTo: (positionInMs: number) => {
src/components/interface/latent-engine/core/video-buffer.tsx ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useRef, useState } from "react"
2
+ import { v4 as uuidv4 } from "uuid"
3
+
4
+ import { cn } from "@/lib/utils/cn"
5
+
6
+ import { LayerElement } from "./types"
7
+
8
+ export function VideoBuffer({
9
+ bufferSize = 4,
10
+ className = "",
11
+ width = 512,
12
+ height = 256,
13
+ }: {
14
+ bufferSize?: number
15
+ className?: string
16
+ width?: number
17
+ height?: number
18
+ }) {
19
+ const state = useRef<{
20
+ isInitialized: boolean
21
+ }>({
22
+ isInitialized: false,
23
+ })
24
+
25
+ const [layers, setLayers] = useState<LayerElement[]>([])
26
+
27
+ // this initialize the VideoBuffer and keeps the layers in sync with the bufferSize
28
+ useEffect(() => {
29
+ if (layers?.length !== bufferSize) {
30
+ const newLayers: LayerElement[] = []
31
+ for (let i = 0; i < bufferSize; i++) {
32
+ newLayers.push({
33
+ id: uuidv4(),
34
+ element: <></>
35
+ })
36
+ }
37
+ setLayers(newLayers)
38
+ }
39
+ }, [bufferSize, layers?.length])
40
+
41
+ return (
42
+ <div
43
+ className={cn(className)}
44
+ style={{
45
+ width,
46
+ height
47
+ }}>
48
+ {layers.map(({ id, element }) => (
49
+ <div
50
+ key={id}
51
+ id={id}
52
+ style={{ width, height }}
53
+ // className={`video-buffer-layer video-buffer-layer-${id}`}
54
+ >{element}</div>))}
55
+ </div>
56
+ )
57
+ }
src/components/interface/latent-engine/resolvers/generic/index.tsx CHANGED
@@ -1,11 +1,14 @@
1
  "use client"
2
 
 
 
3
  import { ClapProject, ClapSegment } from "@/lib/clap/types"
4
 
5
- export async function resolve(segment: ClapSegment, clap: ClapProject): Promise<JSX.Element> {
6
- return (
7
- <div
8
- className="w-full h-full"
9
- />
10
- )
 
11
  }
 
1
  "use client"
2
 
3
+ import { v4 as uuidv4 } from "uuid"
4
+
5
  import { ClapProject, ClapSegment } from "@/lib/clap/types"
6
 
7
+ import { LayerElement } from "../../core/types"
8
+
9
+ export async function resolve(segment: ClapSegment, clap: ClapProject): Promise<LayerElement> {
10
+ return {
11
+ id: uuidv4(),
12
+ element: <div className="w-full h-full" />
13
+ }
14
  }
src/components/interface/latent-engine/resolvers/image/generateImage.ts CHANGED
@@ -1,5 +1,24 @@
1
- export async function generateImage(prompt: string): Promise<string> {
2
- const requestUri = `/api/resolvers/image?p=${encodeURIComponent(prompt)}`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  const res = await fetch(requestUri)
4
  const blob = await res.blob()
5
  const url = URL.createObjectURL(blob)
 
1
+ export async function generateImage({
2
+ prompt,
3
+ width,
4
+ height,
5
+ token,
6
+ }: {
7
+ prompt: string
8
+ width: number
9
+ height: number
10
+ token: string
11
+ }): Promise<string> {
12
+ const requestUri = `/api/resolvers/image?t=${
13
+ token
14
+ }&w=${
15
+ width
16
+ }&h=${
17
+ height
18
+
19
+ }&p=${
20
+ encodeURIComponent(prompt)
21
+ }`
22
  const res = await fetch(requestUri)
23
  const blob = await res.blob()
24
  const url = URL.createObjectURL(blob)
src/components/interface/latent-engine/resolvers/image/index.tsx CHANGED
@@ -2,8 +2,10 @@
2
 
3
  import { ClapProject, ClapSegment } from "@/lib/clap/types"
4
  import { generateImage } from "./generateImage"
 
 
5
 
6
- export async function resolve(segment: ClapSegment, clap: ClapProject): Promise<JSX.Element> {
7
 
8
  const { prompt } = segment
9
 
@@ -11,21 +13,30 @@ export async function resolve(segment: ClapSegment, clap: ClapProject): Promise<
11
  try {
12
  // console.log(`resolveImage: generating video for: ${prompt}`)
13
 
14
- assetUrl = await generateImage(prompt)
 
 
 
 
 
15
 
16
  // console.log(`resolveImage: generated ${assetUrl}`)
17
 
18
  } catch (err) {
19
  console.error(`resolveImage failed (${err})`)
20
- return <></>
 
 
 
21
  }
22
 
23
  // note: the latent-image class is not used for styling, but to grab the component
24
  // from JS when we need to segment etc
25
- return (
26
- <img
 
27
  className="latent-image object-cover h-full"
28
  src={assetUrl}
29
  />
30
- )
31
  }
 
2
 
3
  import { ClapProject, ClapSegment } from "@/lib/clap/types"
4
  import { generateImage } from "./generateImage"
5
+ import { LayerElement } from "../../core/types"
6
+ import { useStore } from "@/app/state/useStore"
7
 
8
+ export async function resolve(segment: ClapSegment, clap: ClapProject): Promise<LayerElement> {
9
 
10
  const { prompt } = segment
11
 
 
13
  try {
14
  // console.log(`resolveImage: generating video for: ${prompt}`)
15
 
16
+ assetUrl = await generateImage({
17
+ prompt,
18
+ width: clap.meta.width,
19
+ height: clap.meta.height,
20
+ token: useStore.getState().jwtToken,
21
+ })
22
 
23
  // console.log(`resolveImage: generated ${assetUrl}`)
24
 
25
  } catch (err) {
26
  console.error(`resolveImage failed (${err})`)
27
+ return {
28
+ id: segment.id,
29
+ element: <></>
30
+ }
31
  }
32
 
33
  // note: the latent-image class is not used for styling, but to grab the component
34
  // from JS when we need to segment etc
35
+ return {
36
+ id: segment.id,
37
+ element: <img
38
  className="latent-image object-cover h-full"
39
  src={assetUrl}
40
  />
41
+ }
42
  }
src/components/interface/latent-engine/resolvers/interface/index.tsx CHANGED
@@ -4,19 +4,31 @@ import RunCSS, { extendRunCSS } from "runcss"
4
 
5
  import { ClapProject, ClapSegment } from "@/lib/clap/types"
6
  import { generateHtml } from "./generateHtml"
7
- import { ThisIsAI } from "../../components/disclaimers/this-is-ai"
 
 
8
 
9
  let state = {
10
  runCSS: RunCSS({}),
11
  isWatching: false,
12
  }
13
 
14
- export async function resolve(segment: ClapSegment, clap: ClapProject): Promise<JSX.Element> {
15
 
16
  const { prompt } = segment
17
 
18
- if (prompt.toLowerCase() === "<builtin:disclaimer>") {
19
- return <ThisIsAI streamType={clap.meta.streamType} />
 
 
 
 
 
 
 
 
 
 
20
  }
21
 
22
  let dangerousHtml = ""
@@ -48,10 +60,11 @@ export async function resolve(segment: ClapSegment, clap: ClapProject): Promise<
48
  // startWatching(targetNode)
49
  }
50
 
51
- return (
52
- <div
 
53
  className="w-full h-full"
54
  dangerouslySetInnerHTML={{ __html: dangerousHtml }}
55
  />
56
- )
57
  }
 
4
 
5
  import { ClapProject, ClapSegment } from "@/lib/clap/types"
6
  import { generateHtml } from "./generateHtml"
7
+ import { AIContentDisclaimer } from "../../components/intros/ai-content-disclaimer"
8
+ import { LayerElement } from "../../core/types"
9
+ import { PoweredBy } from "../../components/intros/powered-by"
10
 
11
  let state = {
12
  runCSS: RunCSS({}),
13
  isWatching: false,
14
  }
15
 
16
+ export async function resolve(segment: ClapSegment, clap: ClapProject): Promise<LayerElement> {
17
 
18
  const { prompt } = segment
19
 
20
+ if (prompt.toLowerCase() === "<builtin:powered_by_engine>") {
21
+ return {
22
+ id: segment.id,
23
+ element: <PoweredBy />
24
+ }
25
+ }
26
+
27
+ if (prompt.toLowerCase() === "<builtin:disclaimer_about_ai>") {
28
+ return {
29
+ id: segment.id,
30
+ element: <AIContentDisclaimer isInteractive={clap.meta.isInteractive} />
31
+ }
32
  }
33
 
34
  let dangerousHtml = ""
 
60
  // startWatching(targetNode)
61
  }
62
 
63
+ return {
64
+ id: segment.id,
65
+ element: <div
66
  className="w-full h-full"
67
  dangerouslySetInnerHTML={{ __html: dangerousHtml }}
68
  />
69
+ }
70
  }
src/components/interface/latent-engine/resolvers/resolveSegment.ts CHANGED
@@ -1,13 +1,13 @@
1
  import { ClapProject, ClapSegment } from "@/lib/clap/types"
2
 
3
- import { LatentComponentResolver } from "../core/types"
4
 
5
  import { resolve as genericResolver } from "./generic"
6
  import { resolve as interfaceResolver } from "./interface"
7
  import { resolve as videoResolver } from "./video"
8
  import { resolve as imageResolver } from "./image"
9
 
10
- export async function resolveSegment(segment: ClapSegment, clap: ClapProject): Promise<JSX.Element> {
11
  let latentComponentResolver: LatentComponentResolver = genericResolver
12
 
13
  if (segment.category === "interface") {
 
1
  import { ClapProject, ClapSegment } from "@/lib/clap/types"
2
 
3
+ import { LatentComponentResolver, LayerElement } from "../core/types"
4
 
5
  import { resolve as genericResolver } from "./generic"
6
  import { resolve as interfaceResolver } from "./interface"
7
  import { resolve as videoResolver } from "./video"
8
  import { resolve as imageResolver } from "./image"
9
 
10
+ export async function resolveSegment(segment: ClapSegment, clap: ClapProject): Promise<LayerElement> {
11
  let latentComponentResolver: LatentComponentResolver = genericResolver
12
 
13
  if (segment.category === "interface") {
src/components/interface/latent-engine/resolvers/resolveSegments.ts CHANGED
@@ -1,17 +1,17 @@
1
  import { ClapProject, ClapSegmentCategory } from "@/lib/clap/types"
2
 
3
  import { resolveSegment } from "./resolveSegment"
 
4
 
5
  export async function resolveSegments(
6
  clap: ClapProject,
7
  segmentCategory: ClapSegmentCategory,
8
  nbMax?: number
9
- ) : Promise<JSX.Element[]> {
10
- const elements: JSX.Element[] = await Promise.all(
11
  clap.segments
12
  .filter(s => s.category === segmentCategory)
13
  .slice(0, nbMax)
14
  .map(s => resolveSegment(s, clap))
15
  )
16
- return elements
17
  }
 
1
  import { ClapProject, ClapSegmentCategory } from "@/lib/clap/types"
2
 
3
  import { resolveSegment } from "./resolveSegment"
4
+ import { LayerElement } from "../core/types"
5
 
6
  export async function resolveSegments(
7
  clap: ClapProject,
8
  segmentCategory: ClapSegmentCategory,
9
  nbMax?: number
10
+ ) : Promise<LayerElement[]> {
11
+ return Promise.all(
12
  clap.segments
13
  .filter(s => s.category === segmentCategory)
14
  .slice(0, nbMax)
15
  .map(s => resolveSegment(s, clap))
16
  )
 
17
  }
src/components/interface/latent-engine/resolvers/video/THIS FOLDER CONTENT IS DEPRECATED ADDED
File without changes
src/components/interface/latent-engine/resolvers/video/basic-video.tsx ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React, { useEffect, useRef } from 'react';
3
+
4
+ export function BasicVideo({
5
+ className = "",
6
+ src,
7
+ playbackSpeed = 1.0,
8
+ playsInline = true,
9
+ muted = true,
10
+ autoPlay = true,
11
+ loop = true,
12
+ }: {
13
+ className?: string
14
+ src?: string
15
+ playbackSpeed?: number
16
+ playsInline?: boolean
17
+ muted?: boolean
18
+ autoPlay?: boolean
19
+ loop?: boolean
20
+ }) {
21
+ const videoRef = useRef<HTMLVideoElement>(null)
22
+
23
+ // Setup and handle changing playback rate and video source
24
+ useEffect(() => {
25
+ if (videoRef.current) {
26
+ videoRef.current.playbackRate = playbackSpeed;
27
+ }
28
+ }, [videoRef.current, playbackSpeed]);
29
+
30
+
31
+ // Handle UI case for empty playlists
32
+ if (!src || typeof src !== "string") {
33
+ return <></>
34
+ }
35
+
36
+ return (
37
+ <video
38
+ ref={videoRef}
39
+ className={className}
40
+ playsInline={playsInline}
41
+ muted={muted}
42
+ autoPlay={autoPlay}
43
+ loop={loop}
44
+ src={src}
45
+ />
46
+ );
47
+ };
src/components/interface/latent-engine/resolvers/video/generateVideo.ts CHANGED
@@ -1,8 +1,25 @@
1
- export async function generateVideo(prompt: string): Promise<string> {
2
- const requestUri = `/api/resolvers/video?p=${encodeURIComponent(prompt)}`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  const res = await fetch(requestUri)
4
  const blob = await res.blob()
5
  const url = URL.createObjectURL(blob)
6
  return url
7
-
8
  }
 
1
+ export async function generateVideo({
2
+ prompt,
3
+ width,
4
+ height,
5
+ token,
6
+ }: {
7
+ prompt: string
8
+ width: number
9
+ height: number
10
+ token: string
11
+ }): Promise<string> {
12
+ const requestUri = `/api/resolvers/video?t=${
13
+ token
14
+ }&w=${
15
+ width
16
+ }&h=${
17
+ height
18
+ }&p=${
19
+ encodeURIComponent(prompt)
20
+ }`
21
  const res = await fetch(requestUri)
22
  const blob = await res.blob()
23
  const url = URL.createObjectURL(blob)
24
  return url
 
25
  }
src/components/interface/latent-engine/resolvers/video/index.tsx CHANGED
@@ -1,40 +1,47 @@
1
  "use client"
2
 
3
  import { ClapProject, ClapSegment } from "@/lib/clap/types"
 
 
 
4
  import { generateVideo } from "./generateVideo"
 
 
5
 
6
- export async function resolve(segment: ClapSegment, clap: ClapProject): Promise<JSX.Element> {
7
 
8
  const { prompt } = segment
9
 
10
- let assetUrl = ""
11
- try {
12
- // console.log(`resolveVideo: generating video for: ${prompt}`)
13
-
14
- assetUrl = await generateVideo(prompt)
15
 
 
 
 
 
 
 
 
16
  // console.log(`resolveVideo: generated ${assetUrl}`)
17
 
18
  } catch (err) {
19
- console.error(`resolveVideo failed (${err})`)
20
- return <></>
 
 
 
21
  }
22
 
23
  // note: the latent-video class is not used for styling, but to grab the component
24
  // from JS when we need to segment etc
25
- return (
26
- <video
27
- loop
28
  className="latent-video object-cover h-full"
 
29
  playsInline
30
-
31
- // muted needs to be enabled for iOS to properly autoplay
32
  muted
33
  autoPlay
34
-
35
- // we hide the controls
36
- // controls
37
- src={assetUrl}>
38
- </video>
39
- )
40
  }
 
1
  "use client"
2
 
3
  import { ClapProject, ClapSegment } from "@/lib/clap/types"
4
+
5
+ import { LayerElement } from "../../core/types"
6
+
7
  import { generateVideo } from "./generateVideo"
8
+ import { BasicVideo } from "./basic-video"
9
+ import { useStore } from "@/app/state/useStore"
10
 
11
+ export async function resolve(segment: ClapSegment, clap: ClapProject): Promise<LayerElement> {
12
 
13
  const { prompt } = segment
14
 
15
+ let src: string = ""
 
 
 
 
16
 
17
+ try {
18
+ src = await generateVideo({
19
+ prompt,
20
+ width: clap.meta.width,
21
+ height: clap.meta.height,
22
+ token: useStore.getState().jwtToken,
23
+ })
24
  // console.log(`resolveVideo: generated ${assetUrl}`)
25
 
26
  } catch (err) {
27
+ console.error(`resolveVideo failed: ${err}`)
28
+ return {
29
+ id: segment.id,
30
+ element: <></>
31
+ }
32
  }
33
 
34
  // note: the latent-video class is not used for styling, but to grab the component
35
  // from JS when we need to segment etc
36
+ return {
37
+ id: segment.id,
38
+ element: <BasicVideo
39
  className="latent-video object-cover h-full"
40
+ src={src}
41
  playsInline
 
 
42
  muted
43
  autoPlay
44
+ loop
45
+ />
46
+ }
 
 
 
47
  }
src/components/interface/latent-engine/resolvers/video/index_legacy.tsx ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { ClapProject, ClapSegment } from "@/lib/clap/types"
4
+ import { PromiseResponseType, waitPromisesUntil } from "@/lib/utils/waitPromisesUntil"
5
+ import { generateVideo } from "./generateVideo"
6
+ import { BasicVideo } from "./basic-video"
7
+ import { useStore } from "@/app/state/useStore"
8
+
9
+ export async function resolve(segment: ClapSegment, clap: ClapProject): Promise<JSX.Element> {
10
+
11
+ const { prompt } = segment
12
+
13
+
14
+ // this is were the magic happen, and we create our buffer of N videos
15
+ // we need to adopt a conservative approach, ideally no more than 3 videos in parallel per user
16
+ const numberOfParallelRequests = 3
17
+
18
+ // the playback speed is the second trick:
19
+ // it allows us to get more "value" per video (a run time of 2 sec instead of 1)
20
+ // at the expense of a more sluggish appearance (10 fps -> 5 fps, if you pick a speed of 0.5)
21
+ const playbackSpeed = 0.5
22
+
23
+ // running multiple requests in parallel increase the risk that a server is down
24
+ // we also cannot afford to wait for all videos for too long as this increase latency,
25
+ // so we should keep a tight execution window
26
+
27
+ // with current resolution and step settings,
28
+ // we achieve 3463.125 rendering time on a A10 Large
29
+ const maxRenderingTimeForAllVideos = 5000
30
+
31
+ // this is how long we wait after we received our first video
32
+ // this represents the variability between rendering time
33
+ //
34
+ // this parameters helps us "squeeze" the timeout,
35
+ // if the hardware settings changed for instance
36
+ // this is a way to say "how, this is faster than I expected, other videos should be fast too then"
37
+ //
38
+ // I've noticed it is between 0.5s and 1s with current settings
39
+ // so let's a a slightly larger value
40
+ const maxWaitTimeAfterFirstVideo = 1500
41
+
42
+ let playlist: string[] = []
43
+
44
+ try {
45
+ // console.log(`resolveVideo: generating video for: ${prompt}`)
46
+ const promises: Array<Promise<string>> = []
47
+
48
+ for (let i = 0; i < numberOfParallelRequests; i++) {
49
+ // TODO use the Clap segments instead to bufferize the next scenes,
50
+ // otherwise we just clone the current segment, which is not very interesting
51
+ promises.push(generateVideo({
52
+ prompt,
53
+ width: clap.meta.width,
54
+ height: clap.meta.height,
55
+ token: useStore.getState().jwtToken,
56
+ }))
57
+ }
58
+
59
+ const results = await waitPromisesUntil(promises, maxWaitTimeAfterFirstVideo, maxRenderingTimeForAllVideos)
60
+
61
+ playlist = results
62
+ .filter(result => result?.status === PromiseResponseType.Resolved && typeof result?.value === "string")
63
+ .map(result => result?.value || "")
64
+
65
+
66
+ // console.log(`resolveVideo: generated ${assetUrl}`)
67
+
68
+ } catch (err) {
69
+ console.error(`resolveVideo failed: ${err}`)
70
+ return <></>
71
+ }
72
+
73
+ // note: the latent-video class is not used for styling, but to grab the component
74
+ // from JS when we need to segment etc
75
+ return (
76
+ <BasicVideo
77
+ className="latent-video object-cover h-full"
78
+ playbackSpeed={playbackSpeed}
79
+ src={playlist[0]}
80
+ playsInline
81
+ muted
82
+ autoPlay
83
+ loop
84
+ />
85
+ )
86
+ }
src/components/interface/latent-engine/resolvers/video/index_notSoGood.tsx ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { ClapProject, ClapSegment } from "@/lib/clap/types"
4
+ import { PromiseResponseType, waitPromisesUntil } from "@/lib/utils/waitPromisesUntil"
5
+ import { VideoLoop } from "./video-loop"
6
+ import { generateVideo } from "./generateVideo"
7
+ import { useStore } from "@/app/state/useStore"
8
+
9
+ export async function resolve(segment: ClapSegment, clap: ClapProject): Promise<JSX.Element> {
10
+
11
+ const { prompt } = segment
12
+
13
+
14
+ // this is were the magic happen, and we create our buffer of N videos
15
+ // we need to adopt a conservative approach, ideally no more than 3 videos in parallel per user
16
+ const numberOfParallelRequests = 3
17
+
18
+ // the playback speed is the second trick:
19
+ // it allows us to get more "value" per video (a run time of 2 sec instead of 1)
20
+ // at the expense of a more sluggish appearance (10 fps -> 5 fps, if you pick a speed of 0.5)
21
+ const playbackSpeed = 0.5
22
+
23
+ // running multiple requests in parallel increase the risk that a server is down
24
+ // we also cannot afford to wait for all videos for too long as this increase latency,
25
+ // so we should keep a tight execution window
26
+
27
+ // with current resolution and step settings,
28
+ // we achieve 3463.125 rendering time on a A10 Large
29
+ const maxRenderingTimeForAllVideos = 5000
30
+
31
+ // this is how long we wait after we received our first video
32
+ // this represents the variability between rendering time
33
+ //
34
+ // this parameters helps us "squeeze" the timeout,
35
+ // if the hardware settings changed for instance
36
+ // this is a way to say "how, this is faster than I expected, other videos should be fast too then"
37
+ //
38
+ // I've noticed it is between 0.5s and 1s with current settings
39
+ // so let's a a slightly larger value
40
+ const maxWaitTimeAfterFirstVideo = 1500
41
+
42
+ let playlist: string[] = []
43
+
44
+ try {
45
+ // console.log(`resolveVideo: generating video for: ${prompt}`)
46
+ const promises: Array<Promise<string>> = []
47
+
48
+ for (let i = 0; i < numberOfParallelRequests; i++) {
49
+ // TODO use the Clap segments instead to bufferize the next scenes,
50
+ // otherwise we just clone the current segment, which is not very interesting
51
+ promises.push(generateVideo({
52
+ prompt,
53
+ width: clap.meta.width,
54
+ height: clap.meta.height,
55
+ token: useStore.getState().jwtToken,
56
+ }))
57
+ }
58
+
59
+ const results = await waitPromisesUntil(promises, maxWaitTimeAfterFirstVideo, maxRenderingTimeForAllVideos)
60
+
61
+ playlist = results
62
+ .filter(result => result?.status === PromiseResponseType.Resolved && typeof result?.value === "string")
63
+ .map(result => result?.value || "")
64
+
65
+
66
+ // console.log(`resolveVideo: generated ${assetUrl}`)
67
+
68
+ } catch (err) {
69
+ console.error(`resolveVideo failed: ${err}`)
70
+ return <></>
71
+ }
72
+
73
+ // note: the latent-video class is not used for styling, but to grab the component
74
+ // from JS when we need to segment etc
75
+ return (
76
+ <VideoLoop
77
+ className="latent-video object-cover h-full"
78
+ playbackSpeed={playbackSpeed}
79
+ playlist={playlist}
80
+ />
81
+ )
82
+ }
src/components/interface/latent-engine/resolvers/video/video-loop.tsx ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React, { useState, useEffect, useRef } from 'react';
3
+
4
+ interface VideoLoopProps {
5
+ className?: string;
6
+ playlist?: string[];
7
+ playbackSpeed?: number;
8
+ }
9
+
10
+ export const VideoLoop: React.FC<VideoLoopProps> = ({
11
+ className = "",
12
+ playlist = [],
13
+ playbackSpeed = 1.0
14
+ }) => {
15
+ const [currentVideoIndex, setCurrentVideoIndex] = useState(0);
16
+ const videoRef = useRef<HTMLVideoElement>(null);
17
+
18
+ const handleVideoEnd = () => {
19
+ // Loop only if there is more than one video
20
+ if (playlist.length > 1) {
21
+ setCurrentVideoIndex(prevIndex => (prevIndex + 1) % playlist.length);
22
+ }
23
+ };
24
+
25
+ // Setup and handle changing playback rate and video source
26
+ useEffect(() => {
27
+ if (videoRef.current) {
28
+ videoRef.current.playbackRate = playbackSpeed;
29
+ videoRef.current.src = playlist[currentVideoIndex] || ''; // Resort to empty string if undefined
30
+ videoRef.current.load();
31
+ if (videoRef.current.src) {
32
+ videoRef.current.play().catch(error => {
33
+ console.error('Video play failed', error);
34
+ });
35
+ } else {
36
+ console.log("VideoLoop: cannot start (no video)")
37
+ }
38
+ }
39
+ }, [playbackSpeed, currentVideoIndex, playlist]);
40
+
41
+ // Handle native video controls interaction
42
+ useEffect(() => {
43
+ const videoElement = videoRef.current;
44
+ if (!videoElement || playlist.length === 0) return;
45
+
46
+ const handlePlay = () => {
47
+ if (videoElement.paused && !videoElement.ended) {
48
+ if (videoRef.current?.src) {
49
+ videoElement.play().catch((error) => {
50
+ console.error('Error playing the video', error);
51
+ });
52
+ } else {
53
+ console.log("VideoLoop: cannot start (no video)")
54
+ }
55
+ }
56
+ };
57
+
58
+ videoElement.addEventListener('play', handlePlay);
59
+ videoElement.addEventListener('ended', handleVideoEnd);
60
+
61
+ return () => {
62
+ videoElement.removeEventListener('play', handlePlay);
63
+ videoElement.removeEventListener('ended', handleVideoEnd);
64
+ };
65
+ }, [playlist]);
66
+
67
+ // Handle UI case for empty playlists
68
+ if (playlist.length === 0 || !playlist[currentVideoIndex]) {
69
+ return <></>
70
+ }
71
+
72
+ return (
73
+ <video
74
+ ref={videoRef}
75
+ loop={false}
76
+ className={className}
77
+ playsInline
78
+ muted
79
+ autoPlay
80
+ src={playlist[currentVideoIndex]}
81
+ />
82
+ );
83
+ };
src/components/interface/latent-engine/{core → utils/canvas}/drawSegmentation.ts RENAMED
@@ -3,7 +3,7 @@ import { MPMask } from "@mediapipe/tasks-vision";
3
  interface DrawSegmentationOptions {
4
  mask?: MPMask;
5
  canvas?: HTMLCanvasElement;
6
- backgroundImage?: HTMLImageElement;
7
  fillStyle?: string;
8
  }
9
 
 
3
  interface DrawSegmentationOptions {
4
  mask?: MPMask;
5
  canvas?: HTMLCanvasElement;
6
+ backgroundImage?: HTMLVideoElement | HTMLImageElement;
7
  fillStyle?: string;
8
  }
9
 
src/components/interface/latent-engine/utils/data/getElementsSortedByStartAt.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getSegmentStartAt } from "./getSegmentStartAt"
2
+
3
+ export function getElementsSortedByStartAt<T extends HTMLElement>(elements: T[], createCopy = true): T[] {
4
+
5
+ const array = createCopy ? [...elements]: elements
6
+
7
+ // this sort from the smallest (oldest) to biggest (youngest)
8
+ return array.sort((a, b) => {
9
+ const aSegmentStartAt = getSegmentStartAt(a)
10
+ const bSegmentStartAt = getSegmentStartAt(b)
11
+ return aSegmentStartAt - bSegmentStartAt
12
+ })
13
+ }
src/components/interface/latent-engine/utils/data/getSegmentEndAt.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export function getSegmentEndAt(element: HTMLElement, defaultValue = 0): number {
2
+ return Number(element.getAttribute('data-segment-end-at') || defaultValue)
3
+ }
src/components/interface/latent-engine/utils/data/getSegmentId.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export function getSegmentId(element: HTMLElement, defaultValue = ""): string {
2
+ return element.getAttribute('data-segment-id') || defaultValue
3
+ }
src/components/interface/latent-engine/utils/data/getSegmentStartAt.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export function getSegmentStartAt(element: HTMLElement, defaultValue = 0): number {
2
+ return Number(element.getAttribute('data-segment-start-at') || defaultValue)
3
+ }
src/components/interface/latent-engine/utils/data/getZIndexDepth.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export function getZIndexDepth(element: HTMLElement, defaultValue = 0) {
2
+ return Number(element.getAttribute('data-z-index-depth') || 0)
3
+ }
src/components/interface/latent-engine/utils/data/setSegmentEndAt.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export function setSegmentEndAt(element: HTMLElement, value = 0): void {
2
+ return element.setAttribute('data-segment-end-at', `${value || "0"}`)
3
+ }
src/components/interface/latent-engine/utils/data/setSegmentId.ts ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ export function setSegmentId(element: HTMLElement, value = ""): void {
2
+ return element.setAttribute('data-segment-id', value)
3
+ }