Spaces:
Running
Running
Commit
•
6215321
1
Parent(s):
8101ed0
Modifying AiTube to support Stories Factory use cases
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .env +4 -0
- package-lock.json +67 -58
- package.json +3 -2
- public/blanks/blank_1sec_1024x576.webm +0 -0
- public/blanks/blank_1sec_512x288.webm +0 -0
- public/logos/latent-engine/latent-engine.png +0 -0
- public/logos/latent-engine/latent-engine.xcf +0 -0
- src/app/api/auth/getToken.ts +20 -0
- src/app/api/generate/story/route.ts +28 -0
- src/app/api/generate/storyboards/route.ts +84 -0
- src/app/api/generate/video/route.ts +19 -0
- src/app/api/generators/clap/generateClap.ts +2 -1
- src/app/api/resolvers/image/route.ts +44 -5
- src/app/api/resolvers/video/route.ts +86 -9
- src/app/dream/{page.tsx → spoiler.tsx} +17 -12
- src/app/main.tsx +28 -14
- src/app/state/useStore.ts +8 -0
- src/components/interface/latent-engine/components/content-layer/index.tsx +1 -1
- src/components/interface/latent-engine/components/{disclaimers/this-is-ai.tsx → intros/ai-content-disclaimer.tsx} +4 -5
- src/components/interface/latent-engine/components/intros/latent-engine.png +0 -0
- src/components/interface/latent-engine/components/intros/powered-by.tsx +31 -0
- src/components/interface/latent-engine/core/engine.tsx +69 -55
- src/components/interface/latent-engine/core/{fetchLatentClap.ts → generators/fetchLatentClap.ts} +0 -0
- src/components/interface/latent-engine/core/{fetchLatentSearchResults.ts → generators/fetchLatentSearchResults.ts} +0 -0
- src/components/interface/latent-engine/core/prompts/getCharacterPrompt.ts +26 -0
- src/components/interface/latent-engine/core/prompts/getVideoPrompt.ts +104 -0
- src/components/interface/latent-engine/core/types.ts +60 -31
- src/components/interface/latent-engine/{store → core}/useLatentEngine.ts +363 -147
- src/components/interface/latent-engine/core/video-buffer.tsx +57 -0
- src/components/interface/latent-engine/resolvers/generic/index.tsx +9 -6
- src/components/interface/latent-engine/resolvers/image/generateImage.ts +21 -2
- src/components/interface/latent-engine/resolvers/image/index.tsx +17 -6
- src/components/interface/latent-engine/resolvers/interface/index.tsx +20 -7
- src/components/interface/latent-engine/resolvers/resolveSegment.ts +2 -2
- src/components/interface/latent-engine/resolvers/resolveSegments.ts +3 -3
- src/components/interface/latent-engine/resolvers/video/THIS FOLDER CONTENT IS DEPRECATED +0 -0
- src/components/interface/latent-engine/resolvers/video/basic-video.tsx +47 -0
- src/components/interface/latent-engine/resolvers/video/generateVideo.ts +20 -3
- src/components/interface/latent-engine/resolvers/video/index.tsx +26 -19
- src/components/interface/latent-engine/resolvers/video/index_legacy.tsx +86 -0
- src/components/interface/latent-engine/resolvers/video/index_notSoGood.tsx +82 -0
- src/components/interface/latent-engine/resolvers/video/video-loop.tsx +83 -0
- src/components/interface/latent-engine/{core → utils/canvas}/drawSegmentation.ts +1 -1
- src/components/interface/latent-engine/utils/data/getElementsSortedByStartAt.ts +13 -0
- src/components/interface/latent-engine/utils/data/getSegmentEndAt.ts +3 -0
- src/components/interface/latent-engine/utils/data/getSegmentId.ts +3 -0
- src/components/interface/latent-engine/utils/data/getSegmentStartAt.ts +3 -0
- src/components/interface/latent-engine/utils/data/getZIndexDepth.ts +3 -0
- src/components/interface/latent-engine/utils/data/setSegmentEndAt.ts +3 -0
- 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.
|
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.
|
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.
|
1496 |
-
"resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.13-rc.
|
1497 |
-
"integrity": "sha512-
|
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.
|
1681 |
-
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.7.
|
1682 |
-
"integrity": "sha512-
|
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.
|
1694 |
-
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/equirectangular-video-adapter/-/equirectangular-video-adapter-5.7.
|
1695 |
-
"integrity": "sha512-
|
1696 |
"peerDependencies": {
|
1697 |
-
"@photo-sphere-viewer/core": "5.7.
|
1698 |
}
|
1699 |
},
|
1700 |
"node_modules/@photo-sphere-viewer/gyroscope-plugin": {
|
1701 |
-
"version": "5.7.
|
1702 |
-
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/gyroscope-plugin/-/gyroscope-plugin-5.7.
|
1703 |
-
"integrity": "sha512-
|
1704 |
"peerDependencies": {
|
1705 |
-
"@photo-sphere-viewer/core": "5.7.
|
1706 |
}
|
1707 |
},
|
1708 |
"node_modules/@photo-sphere-viewer/markers-plugin": {
|
1709 |
-
"version": "5.7.
|
1710 |
-
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/markers-plugin/-/markers-plugin-5.7.
|
1711 |
-
"integrity": "sha512-
|
1712 |
"peerDependencies": {
|
1713 |
-
"@photo-sphere-viewer/core": "5.7.
|
1714 |
}
|
1715 |
},
|
1716 |
"node_modules/@photo-sphere-viewer/overlays-plugin": {
|
1717 |
-
"version": "5.7.
|
1718 |
-
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/overlays-plugin/-/overlays-plugin-5.7.
|
1719 |
-
"integrity": "sha512-
|
1720 |
"peerDependencies": {
|
1721 |
-
"@photo-sphere-viewer/core": "5.7.
|
1722 |
}
|
1723 |
},
|
1724 |
"node_modules/@photo-sphere-viewer/resolution-plugin": {
|
1725 |
-
"version": "5.7.
|
1726 |
-
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/resolution-plugin/-/resolution-plugin-5.7.
|
1727 |
-
"integrity": "sha512-
|
1728 |
"peerDependencies": {
|
1729 |
-
"@photo-sphere-viewer/core": "5.7.
|
1730 |
-
"@photo-sphere-viewer/settings-plugin": "5.7.
|
1731 |
}
|
1732 |
},
|
1733 |
"node_modules/@photo-sphere-viewer/settings-plugin": {
|
1734 |
-
"version": "5.7.
|
1735 |
-
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/settings-plugin/-/settings-plugin-5.7.
|
1736 |
-
"integrity": "sha512-
|
1737 |
"peerDependencies": {
|
1738 |
-
"@photo-sphere-viewer/core": "5.7.
|
1739 |
}
|
1740 |
},
|
1741 |
"node_modules/@photo-sphere-viewer/stereo-plugin": {
|
1742 |
-
"version": "5.7.
|
1743 |
-
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/stereo-plugin/-/stereo-plugin-5.7.
|
1744 |
-
"integrity": "sha512-
|
1745 |
"peerDependencies": {
|
1746 |
-
"@photo-sphere-viewer/core": "5.7.
|
1747 |
-
"@photo-sphere-viewer/gyroscope-plugin": "5.7.
|
1748 |
}
|
1749 |
},
|
1750 |
"node_modules/@photo-sphere-viewer/video-plugin": {
|
1751 |
-
"version": "5.7.
|
1752 |
-
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.7.
|
1753 |
-
"integrity": "sha512-
|
1754 |
"peerDependencies": {
|
1755 |
-
"@photo-sphere-viewer/core": "5.7.
|
1756 |
}
|
1757 |
},
|
1758 |
"node_modules/@photo-sphere-viewer/visible-range-plugin": {
|
1759 |
-
"version": "5.7.
|
1760 |
-
"resolved": "https://registry.npmjs.org/@photo-sphere-viewer/visible-range-plugin/-/visible-range-plugin-5.7.
|
1761 |
-
"integrity": "sha512-
|
1762 |
"peerDependencies": {
|
1763 |
-
"@photo-sphere-viewer/core": "5.7.
|
1764 |
}
|
1765 |
},
|
1766 |
"node_modules/@pkgjs/parseargs": {
|
@@ -3698,9 +3699,9 @@
|
|
3698 |
}
|
3699 |
},
|
3700 |
"node_modules/caniuse-lite": {
|
3701 |
-
"version": "1.0.
|
3702 |
-
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.
|
3703 |
-
"integrity": "sha512-
|
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.
|
4276 |
-
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.
|
4277 |
-
"integrity": "sha512-
|
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.
|
6610 |
-
"resolved": "https://registry.npmjs.org/openai/-/openai-4.38.
|
6611 |
-
"integrity": "sha512-
|
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.
|
8221 |
-
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.
|
8222 |
-
"integrity": "sha512-
|
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.
|
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.
|
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 |
-
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
40 |
-
height
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
44 |
|
45 |
-
|
|
|
|
|
46 |
|
47 |
// possibles values are 1, 2, 4, and 8
|
48 |
-
// but
|
49 |
-
//
|
|
|
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,
|
|
|
|
|
|
|
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 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
|
|
|
|
|
|
|
|
|
|
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 {
|
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 |
-
|
14 |
-
|
15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
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 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
|
|
|
|
|
|
|
|
|
|
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-
|
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
|
10 |
-
|
11 |
}: {
|
12 |
-
|
13 |
} = {}) {
|
14 |
|
15 |
return (
|
@@ -59,7 +58,7 @@ export function ThisIsAI({
|
|
59 |
*/
|
60 |
} content
|
61 |
</div> {
|
62 |
-
|
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 "
|
8 |
import { PlayPauseButton } from "../components/play-pause-button"
|
9 |
-
import {
|
10 |
import { ContentLayer } from "../components/content-layer"
|
11 |
-
import {
|
12 |
-
import {
|
13 |
-
import {
|
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
|
31 |
-
const
|
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
|
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
|
46 |
-
const
|
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 =
|
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 |
-
},
|
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 |
-
>{
|
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 |
-
|
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 |
-
<
|
251 |
-
|
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(
|
270 |
-
<div className="mono text-xs text-center">rendering: {Math.round(
|
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
|
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<
|
|
|
|
|
|
|
|
|
|
|
28 |
|
29 |
export type LatentEngineStore = {
|
|
|
|
|
|
|
|
|
|
|
30 |
width: number
|
31 |
height: number
|
32 |
|
33 |
clap: ClapProject
|
34 |
debug: boolean
|
35 |
|
36 |
-
|
|
|
|
|
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 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
61 |
|
62 |
renderingIntervalId: NodeJS.Timeout | string | number | undefined
|
63 |
renderingIntervalDelayInMs: number
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
64 |
|
65 |
positionInMs: number
|
66 |
durationInMs: number
|
67 |
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
segmentationElement?: HTMLCanvasElement
|
72 |
|
73 |
-
|
74 |
-
videoBuffer: "A" | "B"
|
75 |
-
videoBufferA: ReactNode
|
76 |
-
videoBufferB: ReactNode
|
77 |
|
78 |
-
|
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 |
-
|
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
|
102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
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 "
|
10 |
import { resolveSegments } from "../resolvers/resolveSegments"
|
11 |
-
import { fetchLatentClap } from "
|
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 "../
|
17 |
import { filterImage } from "@/lib/on-device-ai/filterImage"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
|
19 |
export const useLatentEngine = create<LatentEngineStore>((set, get) => ({
|
20 |
-
|
21 |
-
|
|
|
|
|
22 |
|
23 |
clap: newClap(),
|
24 |
debug: true,
|
25 |
|
26 |
-
|
|
|
|
|
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 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
47 |
|
48 |
renderingIntervalId: undefined,
|
49 |
-
renderingIntervalDelayInMs:
|
50 |
-
|
51 |
-
positionInMs: 0,
|
52 |
-
durationInMs: 0,
|
53 |
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
videoBufferA: null,
|
62 |
-
videoBufferB: undefined,
|
63 |
|
64 |
-
|
65 |
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
126 |
-
isStatic: clap.meta.
|
127 |
-
isLive:
|
128 |
-
isInteractive: clap.meta.
|
129 |
})
|
130 |
},
|
131 |
|
132 |
-
|
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 {
|
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:
|
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 {
|
178 |
if (debug) {
|
179 |
console.log("onClickOnSegmentationLayer")
|
180 |
}
|
181 |
-
|
182 |
-
|
|
|
|
|
|
|
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,
|
192 |
|
193 |
const fn = async () => {
|
194 |
-
|
|
|
195 |
get().processClickOnSegment(results)
|
196 |
}
|
197 |
fn()
|
198 |
},
|
199 |
|
200 |
togglePlayPause: (): boolean => {
|
201 |
-
const { isLoaded, isPlaying, renderingIntervalId,
|
202 |
if (!isLoaded) { return false }
|
203 |
|
204 |
const newValue = !isPlaying
|
205 |
|
206 |
clearInterval(renderingIntervalId)
|
207 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
208 |
if (newValue) {
|
209 |
-
if (
|
210 |
try {
|
211 |
-
|
|
|
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 (
|
222 |
try {
|
223 |
-
|
|
|
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 |
-
|
264 |
const {
|
265 |
isLoaded,
|
266 |
isPlaying,
|
267 |
clap,
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
272 |
-
|
|
|
|
|
273 |
} = get()
|
274 |
|
275 |
if (!isLoaded || !isPlaying) {
|
276 |
-
|
277 |
-
set({
|
278 |
-
simulationPending: false,
|
279 |
-
})
|
280 |
-
|
281 |
return
|
282 |
}
|
283 |
|
284 |
set({
|
285 |
-
|
286 |
-
|
287 |
})
|
288 |
|
289 |
-
|
290 |
|
291 |
-
|
292 |
-
|
293 |
-
|
294 |
-
|
295 |
-
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
304 |
}
|
305 |
-
|
306 |
-
|
307 |
-
// await sleep(500)
|
308 |
|
309 |
-
|
310 |
-
|
311 |
-
|
312 |
-
|
313 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
314 |
|
|
|
|
|
|
|
|
|
315 |
|
|
|
316 |
|
317 |
-
|
|
|
|
|
|
|
318 |
|
319 |
-
|
|
|
|
|
|
|
320 |
|
321 |
-
|
322 |
-
|
323 |
-
|
324 |
|
325 |
-
|
326 |
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
}
|
332 |
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
|
340 |
-
|
341 |
-
}
|
342 |
-
}
|
343 |
|
344 |
-
|
345 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
346 |
}
|
347 |
|
|
|
|
|
|
|
|
|
|
|
348 |
try {
|
349 |
if (get().isPlaying) {
|
350 |
// console.log(`runSimulationLoop: rendering UI layer..`)
|
351 |
|
352 |
-
// note: for now we only display one
|
353 |
-
|
|
|
|
|
|
|
354 |
if (get().isPlaying) {
|
355 |
set({
|
356 |
-
|
357 |
})
|
358 |
|
359 |
// console.log(`runSimulationLoop: rendered UI layer`)
|
360 |
}
|
361 |
}
|
362 |
} catch (err) {
|
363 |
-
console.error(`
|
364 |
}
|
365 |
|
366 |
-
const
|
367 |
-
const
|
368 |
-
|
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 |
-
|
379 |
-
|
380 |
-
|
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 |
-
|
395 |
-
|
396 |
-
|
397 |
-
|
398 |
-
|
|
|
|
|
|
|
399 |
} = get()
|
400 |
-
if (!isLoaded) { return }
|
401 |
-
|
402 |
-
|
403 |
-
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
|
408 |
-
|
409 |
-
|
|
|
|
|
|
|
410 |
clearInterval(renderingIntervalId)
|
|
|
411 |
set({
|
412 |
isPlaying: true,
|
413 |
-
|
|
|
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 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
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(
|
2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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<
|
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(
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
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 {
|
|
|
|
|
8 |
|
9 |
let state = {
|
10 |
runCSS: RunCSS({}),
|
11 |
isWatching: false,
|
12 |
}
|
13 |
|
14 |
-
export async function resolve(segment: ClapSegment, clap: ClapProject): Promise<
|
15 |
|
16 |
const { prompt } = segment
|
17 |
|
18 |
-
if (prompt.toLowerCase() === "<builtin:
|
19 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
20 |
}
|
21 |
|
22 |
let dangerousHtml = ""
|
@@ -48,10 +60,11 @@ export async function resolve(segment: ClapSegment, clap: ClapProject): Promise<
|
|
48 |
// startWatching(targetNode)
|
49 |
}
|
50 |
|
51 |
-
|
52 |
-
|
|
|
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<
|
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<
|
10 |
-
|
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(
|
2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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<
|
7 |
|
8 |
const { prompt } = segment
|
9 |
|
10 |
-
let
|
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
|
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 |
-
|
27 |
-
|
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 |
-
|
36 |
-
|
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 |
+
}
|