diff --git a/.gitattributes b/.gitattributes index dec20884f4c4ed2cab0dd8fd5e8572827d36a98e..fd90cc2a6c15d3f1297034096f3e139e881f99a1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ *.clap filter=lfs diff=lfs merge=lfs -text +*.xcf filter=lfs diff=lfs merge=lfs -text diff --git a/package-lock.json b/package-lock.json index fe17f8229a337b5daa7a059f8715ae4ca31322d7..3cb80c278b5341e60a7694ef7bc41a27650abf6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,9 @@ "name": "@jbilcke-hf/clapper", "version": "0.0.3", "dependencies": { - "@aitube/clap": "0.0.24", - "@aitube/engine": "0.0.15", - "@aitube/timeline": "0.0.16", + "@aitube/clap": "0.0.25", + "@aitube/engine": "0.0.16", + "@aitube/timeline": "0.0.19", "@fal-ai/serverless-client": "^0.10.3", "@huggingface/hub": "^0.15.1", "@huggingface/inference": "^2.7.0", @@ -18,6 +18,7 @@ "@langchain/core": "^0.2.6", "@langchain/groq": "^0.0.12", "@langchain/openai": "^0.1.1", + "@monaco-editor/react": "^4.6.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", @@ -44,6 +45,7 @@ "@react-three/fiber": "^8.16.6", "@react-three/uikit": "^0.3.4", "@react-three/uikit-lucide": "^0.3.4", + "@tailwindcss/container-queries": "^0.1.1", "@types/node": "20.12.7", "@types/react": "18.3.0", "@types/react-dom": "18.3.0", @@ -57,6 +59,8 @@ "eslint": "8.57.0", "eslint-config-next": "14.1.0", "lucide-react": "^0.334.0", + "mlt-xml": "^2.0.2", + "monaco-editor": "^0.49.0", "next": "^14.2.3", "next-themes": "^0.2.1", "postcss": "8.4.38", @@ -64,6 +68,8 @@ "query-string": "^9.0.0", "react": "^18.3.1", "react-device-frameset": "^1.3.4", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.3.1", "react-drag-drop-files": "^2.3.10", "react-hook-consent": "^3.5.3", @@ -82,14 +88,16 @@ "use-file-picker": "^2.1.2", "usehooks-ts": "^2.14.0", "uuid": "^9.0.1", + "web-audio-beat-detector": "^8.2.10", "yaml": "^2.4.2", - "zustand": "^4.5.2" + "zustand": "^4.5.2", + "zx": "^8.1.2" } }, "node_modules/@aitube/clap": { - "version": "0.0.24", - "resolved": "https://registry.npmjs.org/@aitube/clap/-/clap-0.0.24.tgz", - "integrity": "sha512-iMmxyPmVh1LWNUlWKK2rRnCSZ/CLmBeb1ADNr5pj0dFI51NNezipYXjZv9dBOb5y3FKBkL+jbEcy7kQFgKHhpw==", + "version": "0.0.25", + "resolved": "https://registry.npmjs.org/@aitube/clap/-/clap-0.0.25.tgz", + "integrity": "sha512-d6LhvTavzYllVKPuE3YneS3dGh7RCol4fuFq90s4kHElNhha0dPuqscbVT/JWtsF/C2TUVAT393mM+a6BgLFQA==", "dependencies": { "pure-uuid": "^1.8.1" }, @@ -98,29 +106,29 @@ } }, "node_modules/@aitube/engine": { - "version": "0.0.15", - "resolved": "https://registry.npmjs.org/@aitube/engine/-/engine-0.0.15.tgz", - "integrity": "sha512-SxlTGOLHNPh93PY/hCXuNgCUWV7Uky9Q4mBKS4uA7+gReeYBT//x/sAWqIvV8MEj2dkx1Br3iTse1/rcVay49w==", + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/@aitube/engine/-/engine-0.0.16.tgz", + "integrity": "sha512-vu6PlL3y6DKeeHRfy2N48nQF4Gbebket3PXT7Toc23CfxupuAbZKPr09VT9bTE9v0EpuhCeSW6lPuqWeBtSQew==", "peerDependencies": { - "@aitube/clap": "0.0.24" + "@aitube/clap": "0.0.25" } }, "node_modules/@aitube/timeline": { - "version": "0.0.16", - "resolved": "https://registry.npmjs.org/@aitube/timeline/-/timeline-0.0.16.tgz", - "integrity": "sha512-Jva0nt7LC9XPwFR9yNfK/MnK7Qj/wWtR3nwkP1Xs5zH7U6I4Wm6D9cm4RpLs/nZ5nDZyPgJRuw0mX8Vq9CUQgQ==", + "version": "0.0.19", + "resolved": "https://registry.npmjs.org/@aitube/timeline/-/timeline-0.0.19.tgz", + "integrity": "sha512-CpgpVg875+KIERzrhM6zlejm3AbQ5shdFwSTYqLFtCtcH905EpK1WLbzmbuDWt/s9QYJEIRJOcE9iT78FfCJyw==", "dependencies": { "date-fns": "^3.6.0", "react-virtualized-auto-sizer": "^1.0.24" }, "peerDependencies": { - "@aitube/clap": "0.0.24", + "@aitube/clap": "0.0.25", "@radix-ui/react-slider": "^1.1.2", "@react-spring/three": "^9.7.3", "@react-spring/types": "^9.7.3", "@react-three/drei": "^9.105.4", "@react-three/fiber": "^8.16.2", - "clsx": "^2.0.0", + "clsx": "^2.1.1", "react": "*", "react-dom": "*", "tailwind-merge": "^2.3.0", @@ -1392,6 +1400,30 @@ "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.8.tgz", "integrity": "sha512-Rp7ll8BHrKB3wXaRFKhrltwZl1CiXGdibPxuWXvqGnKTnv8fqa/nvftYNuSbf+pbJWKYCXdBtYTITdAUTGGh0Q==" }, + "node_modules/@monaco-editor/loader": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", + "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==", + "dependencies": { + "state-local": "^1.0.6" + }, + "peerDependencies": { + "monaco-editor": ">= 0.21.0 < 1" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz", + "integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==", + "dependencies": { + "@monaco-editor/loader": "^1.4.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@monogrid/gainmap-js": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.0.5.tgz", @@ -2735,6 +2767,21 @@ "@babel/runtime": "^7.13.10" } }, + "node_modules/@react-dnd/asap": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", + "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==" + }, + "node_modules/@react-dnd/invariant": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz", + "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==" + }, + "node_modules/@react-dnd/shallowequal": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", + "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==" + }, "node_modules/@react-spring/animated": { "version": "9.7.3", "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.3.tgz", @@ -3035,6 +3082,14 @@ "tslib": "^2.4.0" } }, + "node_modules/@tailwindcss/container-queries": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/container-queries/-/container-queries-0.1.1.tgz", + "integrity": "sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==", + "peerDependencies": { + "tailwindcss": ">=3.2.0" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -3065,11 +3120,30 @@ "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz", "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==" }, + "node_modules/@types/fs-extra": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", + "optional": true, + "dependencies": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, + "node_modules/@types/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "20.12.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", @@ -3623,6 +3697,18 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/automation-events": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/automation-events/-/automation-events-7.0.5.tgz", + "integrity": "sha512-Ni6vhZg0mKmVlew1kxWAzWL7QY1LYDdoYgp6yF9OHeskDrjyJp2SqoKoPQYeiMYjeIlbSpnxXm/JI55VcmX5Wg==", + "dependencies": { + "@babel/runtime": "^7.24.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.2.0" + } + }, "node_modules/autoprefixer": { "version": "10.4.17", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz", @@ -3783,9 +3869,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.23.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", + "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", "funding": [ { "type": "opencollective", @@ -3801,10 +3887,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", + "caniuse-lite": "^1.0.30001629", + "electron-to-chromium": "^1.4.796", "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "update-browserslist-db": "^1.0.16" }, "bin": { "browserslist": "cli.js" @@ -4673,6 +4759,16 @@ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, + "node_modules/dnd-core": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz", + "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", + "dependencies": { + "@react-dnd/asap": "^5.0.1", + "@react-dnd/invariant": "^4.0.1", + "redux": "^4.2.0" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -5459,6 +5555,18 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" }, + "node_modules/fast-unique-numbers": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/fast-unique-numbers/-/fast-unique-numbers-9.0.5.tgz", + "integrity": "sha512-wVA1YcLQn0eAo4P1DXu4UI3wx68Llwm2/iCtpoOGuTi3jm/13KFYjCt3Nlq2fseSuyaBz43RKimj5G68GDzjjg==", + "dependencies": { + "@babel/runtime": "^7.24.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.2.0" + } + }, "node_modules/fast-xml-parser": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.0.tgz", @@ -6990,6 +7098,16 @@ "num-sort": "^2.0.0" } }, + "node_modules/mlt-xml": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mlt-xml/-/mlt-xml-2.0.2.tgz", + "integrity": "sha512-UkbC0IaWS1LAIAm99/UukRq1htYTgBsalxUtBAVMUJn7LZl6EqpziU7Sv6dgoeo1UtXsUkWBAc37gwHOdPkoJA==" + }, + "node_modules/monaco-editor": { + "version": "0.49.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.49.0.tgz", + "integrity": "sha512-2I8/T3X/hLxB2oPHgqcNYUVdA/ZEFShT7IAujifIPMfKkNbLOqY8XCoyHCXrsdjb36dW9MwoTwBCFpXKMwNwaQ==" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -7967,6 +8085,43 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, + "node_modules/react-dnd": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", + "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", + "dependencies": { + "@react-dnd/invariant": "^4.0.1", + "@react-dnd/shallowequal": "^4.0.1", + "dnd-core": "^16.0.1", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2" + }, + "peerDependencies": { + "@types/hoist-non-react-statics": ">= 3.3.1", + "@types/node": ">= 12", + "@types/react": ">= 16", + "react": ">= 16.14" + }, + "peerDependenciesMeta": { + "@types/hoist-non-react-statics": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-dnd-html5-backend": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz", + "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==", + "dependencies": { + "dnd-core": "^16.0.1" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -8215,6 +8370,14 @@ "node": ">=8.10.0" } }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", @@ -8630,9 +8793,9 @@ } }, "node_modules/sonner": { - "version": "1.4.41", - "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.4.41.tgz", - "integrity": "sha512-uG511ggnnsw6gcn/X+YKkWPo5ep9il9wYi3QJxHsYe7yTZ4+cOd1wuodOUmOpFuXL+/RE3R04LczdNCDygTDgQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.5.0.tgz", + "integrity": "sha512-FBjhG/gnnbN6FY0jaNnqZOMmB73R+5IiyYAw8yBj7L54ER7HB3fOSE5OFiQiE2iXWxeXKvg6fIP4LtVppHEdJA==", "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" @@ -8657,6 +8820,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/standardized-audio-context": { + "version": "25.3.72", + "resolved": "https://registry.npmjs.org/standardized-audio-context/-/standardized-audio-context-25.3.72.tgz", + "integrity": "sha512-Dvwu2NuqafQqWxUWoo6G9ze/cvVNlFDpmIOA8XjuItrfR0h/REjgjoYCT3Y7nbkUJKGoz8SqqVzR7JATQV4XeQ==", + "dependencies": { + "@babel/runtime": "^7.24.5", + "automation-events": "^7.0.5", + "tslib": "^2.6.2" + } + }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" + }, "node_modules/stats-gl": { "version": "2.2.8", "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.2.8.tgz", @@ -9617,6 +9795,38 @@ "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" }, + "node_modules/web-audio-beat-detector": { + "version": "8.2.10", + "resolved": "https://registry.npmjs.org/web-audio-beat-detector/-/web-audio-beat-detector-8.2.10.tgz", + "integrity": "sha512-kVC0NLcuB/gPNr53TJR7zjasZpu3Uf+Ov5fBpHG0XsWaNVS9qZ+HY+J5pd1hD7BnaHH4CuYV7yaiRhjN41GaFQ==", + "dependencies": { + "@babel/runtime": "^7.24.5", + "tslib": "^2.6.2", + "web-audio-beat-detector-broker": "^4.1.9", + "web-audio-beat-detector-worker": "^5.2.52" + } + }, + "node_modules/web-audio-beat-detector-broker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/web-audio-beat-detector-broker/-/web-audio-beat-detector-broker-4.1.9.tgz", + "integrity": "sha512-4xT0qN8Bb09P8MiCvIvMzHs6fP/uMC+q9oO0VG3A0YjwWGhm97rrXO0vHAUSvE/bUzZyyESpJo4GmpGWqnK9iQ==", + "dependencies": { + "@babel/runtime": "^7.24.5", + "fast-unique-numbers": "^9.0.5", + "standardized-audio-context": "^25.3.72", + "tslib": "^2.6.2", + "web-audio-beat-detector-worker": "^5.2.52" + } + }, + "node_modules/web-audio-beat-detector-worker": { + "version": "5.2.52", + "resolved": "https://registry.npmjs.org/web-audio-beat-detector-worker/-/web-audio-beat-detector-worker-5.2.52.tgz", + "integrity": "sha512-+W8wHnho7ybSrllvDz5M/LpmjgapdG2Otnn66jYgycPJmj1U8RnLzyhFuonOpFMJc4437IVEEVKBAAXVTVmRzw==", + "dependencies": { + "@babel/runtime": "^7.24.5", + "tslib": "^2.6.2" + } + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -9859,9 +10069,9 @@ "peer": true }, "node_modules/yaml": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.3.tgz", - "integrity": "sha512-sntgmxj8o7DE7g/Qi60cqpLBA3HG3STcDA0kO+WfB05jEKhZMbY7umNm2rBpQvsmZ16/lPXCJGW2672dgOUkrg==", + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", + "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", "bin": { "yaml": "bin.mjs" }, @@ -9935,6 +10145,30 @@ "optional": true } } + }, + "node_modules/zx": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/zx/-/zx-8.1.2.tgz", + "integrity": "sha512-zkCiXKh8D/eo6r58OmJvO5mc2NthcSRvysb3fuS6VQlHPbEPBcxduRwM3m6ZfHj+7cLHcrahCnuO2TDAbW+6xw==", + "bin": { + "zx": "build/cli.js" + }, + "engines": { + "node": ">= 12.17.0" + }, + "optionalDependencies": { + "@types/fs-extra": "^11.0.4", + "@types/node": ">=20.12.12" + } + }, + "node_modules/zx/node_modules/@types/node": { + "version": "20.14.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", + "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", + "optional": true, + "dependencies": { + "undici-types": "~5.26.4" + } } } } diff --git a/package.json b/package.json index 4c8a31e1174f5799f6740adbfa421b6b4ae84957..001de97290f45100751d9e8e966900c9120917bd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "@jbilcke-hf/clapper", - "version": "0.0.3", + "name": "@aitube/clapper", + "version": "0.0.0", "private": true, "description": "🎬 Clapper", "scripts": { @@ -10,9 +10,9 @@ "lint": "next lint" }, "dependencies": { - "@aitube/clap": "0.0.24", - "@aitube/engine": "0.0.15", - "@aitube/timeline": "0.0.16", + "@aitube/clap": "0.0.25", + "@aitube/engine": "0.0.16", + "@aitube/timeline": "0.0.19", "@fal-ai/serverless-client": "^0.10.3", "@huggingface/hub": "^0.15.1", "@huggingface/inference": "^2.7.0", @@ -20,6 +20,7 @@ "@langchain/core": "^0.2.6", "@langchain/groq": "^0.0.12", "@langchain/openai": "^0.1.1", + "@monaco-editor/react": "^4.6.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", @@ -46,6 +47,7 @@ "@react-three/fiber": "^8.16.6", "@react-three/uikit": "^0.3.4", "@react-three/uikit-lucide": "^0.3.4", + "@tailwindcss/container-queries": "^0.1.1", "@types/node": "20.12.7", "@types/react": "18.3.0", "@types/react-dom": "18.3.0", @@ -59,6 +61,8 @@ "eslint": "8.57.0", "eslint-config-next": "14.1.0", "lucide-react": "^0.334.0", + "mlt-xml": "^2.0.2", + "monaco-editor": "^0.49.0", "next": "^14.2.3", "next-themes": "^0.2.1", "postcss": "8.4.38", @@ -66,6 +70,8 @@ "query-string": "^9.0.0", "react": "^18.3.1", "react-device-frameset": "^1.3.4", + "react-dnd": "^16.0.1", + "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.3.1", "react-drag-drop-files": "^2.3.10", "react-hook-consent": "^3.5.3", @@ -84,7 +90,9 @@ "use-file-picker": "^2.1.2", "usehooks-ts": "^2.14.0", "uuid": "^9.0.1", + "web-audio-beat-detector": "^8.2.10", "yaml": "^2.4.2", - "zustand": "^4.5.2" + "zustand": "^4.5.2", + "zx": "^8.1.2" } } diff --git a/public/bubble.jpg b/public/bubble.jpg deleted file mode 100644 index 22e44c049b61e7b56281e8a74504855959970617..0000000000000000000000000000000000000000 Binary files a/public/bubble.jpg and /dev/null differ diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index 060fa8ce26f545dd54e28b76401e5bc7a55b7c92..0000000000000000000000000000000000000000 Binary files a/public/favicon.ico and /dev/null differ diff --git a/public/favicon/favicon-114-precomposed.png b/public/favicon/favicon-114-precomposed.png deleted file mode 100644 index be8953b99cc353a6ea9047e83a8c28a627cff46c..0000000000000000000000000000000000000000 Binary files a/public/favicon/favicon-114-precomposed.png and /dev/null differ diff --git a/public/favicon/favicon-120-precomposed.png b/public/favicon/favicon-120-precomposed.png deleted file mode 100644 index 3aab950a1f0268f0642e87e8fec63ef8f064c4da..0000000000000000000000000000000000000000 Binary files a/public/favicon/favicon-120-precomposed.png and /dev/null differ diff --git a/public/favicon/favicon-144-precomposed.png b/public/favicon/favicon-144-precomposed.png deleted file mode 100644 index e29c5d95a6d22dd36aea448c8eda8df03e875815..0000000000000000000000000000000000000000 Binary files a/public/favicon/favicon-144-precomposed.png and /dev/null differ diff --git a/public/favicon/favicon-152-precomposed.png b/public/favicon/favicon-152-precomposed.png deleted file mode 100644 index 6201f9f8fe506e8562741ab990c33c7a39d714d9..0000000000000000000000000000000000000000 Binary files a/public/favicon/favicon-152-precomposed.png and /dev/null differ diff --git a/public/favicon/favicon-180-precomposed.png b/public/favicon/favicon-180-precomposed.png deleted file mode 100644 index 241a60c82c96c8665d4935632a1db121b8a99387..0000000000000000000000000000000000000000 Binary files a/public/favicon/favicon-180-precomposed.png and /dev/null differ diff --git a/public/favicon/favicon-192.png b/public/favicon/favicon-192.png deleted file mode 100644 index ecc6cbefdfc1232e92b40cc616cc34388b0360da..0000000000000000000000000000000000000000 Binary files a/public/favicon/favicon-192.png and /dev/null differ diff --git a/public/favicon/favicon-32.png b/public/favicon/favicon-32.png deleted file mode 100644 index 4076fa1b3ea9a28897aeb9dbb37bc8bfa4f1a624..0000000000000000000000000000000000000000 Binary files a/public/favicon/favicon-32.png and /dev/null differ diff --git a/public/favicon/favicon-36.png b/public/favicon/favicon-36.png deleted file mode 100644 index 4bb5a3262eff1c5ba29e3e06320b9d76a07565af..0000000000000000000000000000000000000000 Binary files a/public/favicon/favicon-36.png and /dev/null differ diff --git a/public/favicon/favicon-48.png b/public/favicon/favicon-48.png deleted file mode 100644 index 69d6328355ed22cf0dca75f558351e1f585af8bb..0000000000000000000000000000000000000000 Binary files a/public/favicon/favicon-48.png and /dev/null differ diff --git a/public/favicon/favicon-57.png b/public/favicon/favicon-57.png deleted file mode 100644 index 91ac87f90441ddbe723a45cf7e0b16eded515d21..0000000000000000000000000000000000000000 Binary files a/public/favicon/favicon-57.png and /dev/null differ diff --git a/public/favicon/favicon-60.png b/public/favicon/favicon-60.png deleted file mode 100644 index cf5ee0bea0c30fea6addb1134d8bfeb7ca888e0d..0000000000000000000000000000000000000000 Binary files a/public/favicon/favicon-60.png and /dev/null differ diff --git a/public/favicon/favicon-72-precomposed.png b/public/favicon/favicon-72-precomposed.png deleted file mode 100644 index 4e957de6a9137ec73ab51ba9317e8c13d1e45bf5..0000000000000000000000000000000000000000 Binary files a/public/favicon/favicon-72-precomposed.png and /dev/null differ diff --git a/public/favicon/favicon-72.png b/public/favicon/favicon-72.png deleted file mode 100644 index 4e957de6a9137ec73ab51ba9317e8c13d1e45bf5..0000000000000000000000000000000000000000 Binary files a/public/favicon/favicon-72.png and /dev/null differ diff --git a/public/favicon/favicon-76.png b/public/favicon/favicon-76.png deleted file mode 100644 index 7eb8efbd1087dc53784116053667c49073e08d3d..0000000000000000000000000000000000000000 Binary files a/public/favicon/favicon-76.png and /dev/null differ diff --git a/public/favicon/favicon-96.png b/public/favicon/favicon-96.png deleted file mode 100644 index 5098aed1cda10c42eabcacb721d17b62dc5a8f3a..0000000000000000000000000000000000000000 Binary files a/public/favicon/favicon-96.png and /dev/null differ diff --git a/public/favicon/favicon.ico b/public/favicon/favicon.ico deleted file mode 100644 index 060fa8ce26f545dd54e28b76401e5bc7a55b7c92..0000000000000000000000000000000000000000 Binary files a/public/favicon/favicon.ico and /dev/null differ diff --git a/public/favicon/manifest.json b/public/favicon/manifest.json deleted file mode 100644 index d0d92afbc1530e91966f13b737cea8885bf5a111..0000000000000000000000000000000000000000 --- a/public/favicon/manifest.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "pollo", - "icons": [ - { - "src": "\/favicon-36.png", - "sizes": "36x36", - "type": "image\/png", - "density": 0.75 - }, - { - "src": "\/favicon-48.png", - "sizes": "48x48", - "type": "image\/png", - "density": 1 - }, - { - "src": "\/favicon-72.png", - "sizes": "72x72", - "type": "image\/png", - "density": 1.5 - }, - { - "src": "\/favicon-96.png", - "sizes": "96x96", - "type": "image\/png", - "density": 2 - }, - { - "src": "\/favicon-144.png", - "sizes": "144x144", - "type": "image\/png", - "density": 3 - }, - { - "src": "\/favicon-192.png", - "sizes": "192x192", - "type": "image\/png", - "density": 4 - } - ] -} diff --git a/public/icon.png b/public/icon.png deleted file mode 100644 index ecc6cbefdfc1232e92b40cc616cc34388b0360da..0000000000000000000000000000000000000000 Binary files a/public/icon.png and /dev/null differ diff --git a/public/next.svg b/public/next.svg deleted file mode 100644 index 5174b28c565c285e3e312ec5178be64fbeca8398..0000000000000000000000000000000000000000 --- a/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/public/vercel.svg b/public/vercel.svg deleted file mode 100644 index d2f84222734f27b623d1c80dda3561b04d1284af..0000000000000000000000000000000000000000 --- a/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/app/CL.icns b/src/app/CL.icns new file mode 100644 index 0000000000000000000000000000000000000000..d2a105ebcf8f1ca66d13f224a76775b9c00705e6 Binary files /dev/null and b/src/app/CL.icns differ diff --git "a/public/favicon/Icon\r" "b/src/app/CL.iconset/Icon\r" similarity index 100% rename from "public/favicon/Icon\r" rename to "src/app/CL.iconset/Icon\r" diff --git a/src/app/CL.iconset/icon_128x128.png b/src/app/CL.iconset/icon_128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..275953e04bf87b389dcc3f0182fb4332b68f47b7 Binary files /dev/null and b/src/app/CL.iconset/icon_128x128.png differ diff --git a/src/app/CL.iconset/icon_128x128@2x.png b/src/app/CL.iconset/icon_128x128@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..cb5e7acba72d56ac74a67d6ed168bc6dc7b26c93 Binary files /dev/null and b/src/app/CL.iconset/icon_128x128@2x.png differ diff --git a/src/app/CL.iconset/icon_16x16.png b/src/app/CL.iconset/icon_16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..10de26314d2a2c359e10e84f9b146ba876bef770 Binary files /dev/null and b/src/app/CL.iconset/icon_16x16.png differ diff --git a/src/app/CL.iconset/icon_16x16@2x.png b/src/app/CL.iconset/icon_16x16@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..41bb0467b1d19e9a21822f1ba4bc6f12731b1a1a Binary files /dev/null and b/src/app/CL.iconset/icon_16x16@2x.png differ diff --git a/src/app/CL.iconset/icon_256x256.png b/src/app/CL.iconset/icon_256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..cb5e7acba72d56ac74a67d6ed168bc6dc7b26c93 Binary files /dev/null and b/src/app/CL.iconset/icon_256x256.png differ diff --git a/src/app/CL.iconset/icon_256x256@2x.png b/src/app/CL.iconset/icon_256x256@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0052bb75ba3c1aa8b8e68b14238e773da9168036 Binary files /dev/null and b/src/app/CL.iconset/icon_256x256@2x.png differ diff --git a/src/app/CL.iconset/icon_32x32.png b/src/app/CL.iconset/icon_32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..41bb0467b1d19e9a21822f1ba4bc6f12731b1a1a Binary files /dev/null and b/src/app/CL.iconset/icon_32x32.png differ diff --git a/src/app/CL.iconset/icon_32x32@2x.png b/src/app/CL.iconset/icon_32x32@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..553ad0dccf8351ae92054deca3d7cfc1ab139f98 Binary files /dev/null and b/src/app/CL.iconset/icon_32x32@2x.png differ diff --git a/src/app/CL.iconset/icon_512x512.png b/src/app/CL.iconset/icon_512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..0052bb75ba3c1aa8b8e68b14238e773da9168036 Binary files /dev/null and b/src/app/CL.iconset/icon_512x512.png differ diff --git a/src/app/CL.iconset/icon_512x512@2x.png b/src/app/CL.iconset/icon_512x512@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a5625746479e503f448f5c7653c151de1355490d Binary files /dev/null and b/src/app/CL.iconset/icon_512x512@2x.png differ diff --git a/src/app/cute_1024x1024x32.png b/src/app/cute_1024x1024x32.png new file mode 100644 index 0000000000000000000000000000000000000000..9827a22f4305ffcf1aa5d1bacf078cdae84606e5 Binary files /dev/null and b/src/app/cute_1024x1024x32.png differ diff --git a/src/app/cute_favicon.ico b/src/app/cute_favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..adbe24ee0d0beaedcfac18bd3afa50ef1c50a41f Binary files /dev/null and b/src/app/cute_favicon.ico differ diff --git a/src/app/cute_icon.png b/src/app/cute_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b44e99882253a6090f7e967eee48e3f02795d2b9 Binary files /dev/null and b/src/app/cute_icon.png differ diff --git a/src/app/favicon.ico b/src/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..48b85e4a6244694290dd487e74749670e0b9a0b4 Binary files /dev/null and b/src/app/favicon.ico differ diff --git a/src/app/fonts.ts b/src/app/fonts.ts deleted file mode 100644 index 92144b6142c3906eef56c2c9a6e978378b2cda53..0000000000000000000000000000000000000000 --- a/src/app/fonts.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Inter } from 'next/font/google' - -export const inter = Inter({ subsets: ['latin'] }) diff --git a/src/app/fonts/alarm-clock/alarm-clock.woff2 b/src/app/fonts/alarm-clock/alarm-clock.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..f5154ebff3d4eba854551ab8ee7192c3174139be Binary files /dev/null and b/src/app/fonts/alarm-clock/alarm-clock.woff2 differ diff --git a/src/app/fonts/index.ts b/src/app/fonts/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..0bbee27992b1b3f1cf77d4186fd0f2507ea0fd98 --- /dev/null +++ b/src/app/fonts/index.ts @@ -0,0 +1,23 @@ +import localFont from "next/font/local" +import { Inter } from 'next/font/google' + +export const inter = Inter({ subsets: ['latin'] }) + +// note: this one is an "italic" digital display font +export const clock = localFont({ + src: "./alarm-clock/alarm-clock.woff2", + variable: "--font-clock" +}) + +export const fonts = { + clock, + inter +} + +export const fontList = Object.keys(fonts) +export type FontName = keyof typeof fonts +export const classNames = Object.values(fonts).map(font => font.className) +export const className = classNames.join(" ") +export type FontClass = + | "font-clock" + | "font-inter" \ No newline at end of file diff --git a/src/app/icon.png b/src/app/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a3cb26850b7ba056cd0ce055a601e4f8f6b6d58 Binary files /dev/null and b/src/app/icon.png differ diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 73101508452a1f1f0a5915d54bbfda682c4898c9..c75aeb4c20a2c78c9ef65240c9e50b6d97cc0fcd 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -8,8 +8,8 @@ import type { Metadata } from 'next' import { inter } from './fonts' export const metadata: Metadata = { - title: '🎬 Clapper', - description: '🎬 Clapper', + title: 'Clapper', + description: 'Clapper - The free and open-source AI Video Editor', } export default function RootLayout({ diff --git a/src/app/logo-desaturated.png b/src/app/logo-desaturated.png new file mode 100644 index 0000000000000000000000000000000000000000..856c61051596024ee4dc356e2c7a936ad2c3db17 Binary files /dev/null and b/src/app/logo-desaturated.png differ diff --git a/src/app/logo-no-bg.png b/src/app/logo-no-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..774f5786b855503028181e3abed36e9ad63663d3 Binary files /dev/null and b/src/app/logo-no-bg.png differ diff --git a/src/app/logo-v2.png b/src/app/logo-v2.png new file mode 100644 index 0000000000000000000000000000000000000000..dc07b1fbddad40e18c3399b15f918f3c4e131221 Binary files /dev/null and b/src/app/logo-v2.png differ diff --git a/src/app/logo-v2.xcf b/src/app/logo-v2.xcf new file mode 100644 index 0000000000000000000000000000000000000000..6d9d4fc2b01be6c1f90d805e237e4477270f7932 --- /dev/null +++ b/src/app/logo-v2.xcf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c05b74fade5a3fc56783d47586c70a32986751b9e60ffed56cd52da7d64a895c +size 2025250 diff --git a/src/app/main.tsx b/src/app/main.tsx index fea62b345ac7acd8d5e99fd6abed3b2d43601208..681e5787d112e07245e8bef91ea4b1aaa1acaa5a 100644 --- a/src/app/main.tsx +++ b/src/app/main.tsx @@ -1,71 +1,108 @@ "use client" -import React from "react" +import React, { useRef } from "react" import { ReflexContainer, ReflexSplitter, ReflexElement -} from 'react-reflex' -import { useTimelineState } from "@aitube/timeline" +} from "react-reflex" +import { DndProvider, useDrop } from "react-dnd" +import { HTML5Backend, NativeTypes } from "react-dnd-html5-backend" +import { useTimeline } from "@aitube/timeline" import { Toaster } from "@/components/ui/sonner" import { cn } from "@/lib/utils" import { TooltipProvider } from "@/components/ui/tooltip" -import { RenderClap } from "@/components/core/render-clap" +import { Monitor } from "@/components/monitor" import { SettingsDialog } from "@/components/settings" import { LoadingDialog } from "@/components/dialogs/loader/LoadingDialog" import { useUI } from "@/controllers/ui" import { TopBar } from "@/components/toolbars/top-bar" import { Timeline } from "@/components/core/timeline" +import { useIO } from "@/controllers/io/useIO" -export function Main() { +type DroppableThing = { files: File[] } - const isEmpty = useTimelineState(s => s.isEmpty) +function MainContent() { + const ref = useRef(null) + const isEmpty = useTimeline(s => s.isEmpty) const showTimeline = useUI((s) => s.showTimeline) + const showChat = useUI((s) => s.showChat) + + const openFiles = useIO(s => s.openFiles) + + const [{ isOver, canDrop }, connectFileDrop] = useDrop({ + accept: [ + NativeTypes.FILE, + ], + drop: (item: DroppableThing): void => { + console.log("DROP", item) + openFiles(item.files) + }, + collect: (monitor) => ({ + isOver: monitor.isOver(), + canDrop: monitor.canDrop(), + }), + }) + + connectFileDrop(ref) return ( - -
- +
+
+ )}> - + + {/* showChat && */} + {/* showChat && */}
- - - - -
+ + + + +
+ ); +} + +export function Main() { + return ( + + + + ); } diff --git a/src/components/assistant/ChatView.FINISH_ME b/src/components/assistant/ChatView.FINISH_ME index f7fb217960165c4d66e16b33548e0a2372a806a7..8688e70788977b99429642ffad7106aecc5b18f5 100644 --- a/src/components/assistant/ChatView.FINISH_ME +++ b/src/components/assistant/ChatView.FINISH_ME @@ -6,8 +6,8 @@ import { ClapOutputType, ClapProject, ClapScene, ClapSegment, ClapSegmentCategor import { Input } from "@/components/ui/input" import { queryAssistant } from "@/app/api/assistant/providers/openai/askAssistant" -import { useSettings } from "@/controllers/settings" -import { DEFAULT_DURATION_IN_MS_PER_STEP, findFreeTrack, useTimelineState } from "@aitube/timeline" +import { useSettingsa } from "@/controllers/settings" +import { DEFAULT_DURATION_IN_MS_PER_STEP, findFreeTrack, useTimeline } from "@aitube/timeline" import { useAssistant } from "@/controllers/assistant/useAssistant" export function ChatView() { @@ -63,10 +63,10 @@ export function ChatView() { } - const clap: ClapProject = useTimelineState.getState().clap + const clap: ClapProject = useTimeline.getState().clap console.log(`TODO @julian: restore the concept of "addSegment()", "updateSegment()", "active segment" and "cursor position" inside @aitube-timeline`) - // const { addSegment, activeSegments, cursorInSteps, } = useTimelineState.getState() + // const { addSegment, activeSegments, cursorInSteps, } = useTimeline.getState() const activeSegments: ClapSegment[] = [] const cursorInSteps = 0 diff --git a/src/components/core/ScriptEditor/index.FOR_LATER b/src/components/core/ScriptEditor/index.FOR_LATER new file mode 100644 index 0000000000000000000000000000000000000000..326b3d62866f2bdc35127f06de4489419bddae67 --- /dev/null +++ b/src/components/core/ScriptEditor/index.FOR_LATER @@ -0,0 +1,191 @@ +import { useEffect, useState } from "react" +import MonacoEditor from "monaco-editor" +import Editor from "@monaco-editor/react" + +const beforeMount = ({ editor }: { editor: typeof MonacoEditor.editor }) => { + // Define a custom theme with the provided color palette + editor.defineTheme('customTheme', { + base: 'vs-dark', // Base theme (you can change to vs for a lighter theme if preferred) + inherit: true, // Inherit the default rules + rules: [ + // You can define token-specific styles here if needed + ], + colors: { + 'editor.background': '#111827', // Editor background color (given) + 'editorCursor.foreground': '#e5e7eb', // Cursor color + 'editor.lineHighlightBackground': '#374151', // Highlighted line color + 'editorLineNumber.foreground': '#6b7280', // Line Numbers color + 'editor.selectionBackground': '#2c333c', // Selection color + 'editor.foreground': '#d1d5db', // Main text color + 'editorIndentGuide.background': '#4b5563', // Indent guides color + 'editorIndentGuide.activeBackground': '#6b7280', // Active indent guides color + 'editorWhitespace.foreground': '#3b4049', // Whitespace symbols color + // Add more color overrides if needed here + }, + }) + + // Apply the custom theme immediately after defining it + editor.setTheme('customTheme') +} + +export function ScriptEditor() { + const [editor, setEditor] = useState() + const script = useApp(state => state.script) + const rewriteCurrentScript = useApp(state => state.rewriteCurrentScript) + const isPlaying = useApp(state => state.isPlaying) + const setCursorAt = useApp((state) => state.setCursorAt) + const [scriptContent, setScriptContent] = useState("") + + const activeSegments = useApp(state => state.activeSegments) + const stepsToPreviews = useApp(state => state.stepsToPreviews) + + const screenplayScroll = useInterface(state => state.screenplayScroll) + const setScreenplayScroll = useInterface(state => state.setScreenplayScroll) + + const timelineScroll = useInterface(state => state.timelineScroll) + + const leftmostVisibleScene = stepsToPreviews[timelineScroll.scrollLeftInSteps]?.scene + + // console.log("linesToPreview:", linesToPreviews) + + useEffect(() => { + if (editor && leftmostVisibleScene) { + // console.log("ScriptEditor: timelineScrollLeftInStep changed to scene " + leftmostVisibleScene.line) + + // in Monaco editor the line index doesn't start at 0 but 1 + // however, it appears that we are already good, and the 1 is just here as a backup + const lineNumber = (leftmostVisibleScene.startAtLine) || 1 + editor.revealLineInCenter(lineNumber) + } + }, [editor, leftmostVisibleScene]) + + + const activeScene = activeSegments + .find(s => s.category === "video")?.scene + + const activeSceneLineNumber = (activeScene?.startAtLine || 0) + + useEffect(() => { + const fn = async () => { + // support both text and Blob + let content = "" + if (typeof script.content !== "string") { + content = await script.content.text() + } else { + content = script.content + } + editor?.setValue(content) + setScriptContent(content) + } + fn() + + }, [editor, script.content]) + + useEffect(() => { + if (editor && activeSceneLineNumber) { + // console.log("useEffect:", activeSceneLineNumber) + + // in Monaco editor the line index doesn't start at 0 but 1 + // however, it appears that we are already good, and the 1 is just here as a backup + const lineNumber = activeSceneLineNumber || 1 + const column = 1 + + // editor.revealRangeInCenter + + // IMPORTANT: we only alter the position if we are currently NOT focused + // that way we don't annoy the user while they are clicking/typing + if (!editor.hasTextFocus()) { + + // scroll to a specific line + // this will be used whenever the horizontal timeline scrolls to somewhere + // (to avoid infinite loops, we should do this only if the timeline scroll event + // is originating from the timeline and not the screenplay reader) + editor.revealLineInCenter(lineNumber) + + // console.log(`editor.setPosition({ lineNumber: ${lineNumber}, column: ${column} })`) + editor.setPosition({ lineNumber, column }) + } + } + }, [editor, activeSceneLineNumber]) + + const onMount = (editor: MonacoEditor.editor.IStandaloneCodeEditor) => { + const model = editor.getModel() + if (!model) { return } + + setEditor(editor) + + editor.onMouseDown((e) => { + const currentPosition = editor.getPosition() + + const line = currentPosition?.lineNumber + if (typeof line !== "number") { return } + + // so... due to how monaco callbacks work, we cannot use the hook context + // to get the linesToPreview.. but that's okay! + const linesToPreviews = useApp.getState().linesToPreviews + + const startTimeInSteps = linesToPreviews[line]?.startTimeInSteps + if (typeof startTimeInSteps !== "number") { return } + + setCursorAt(startTimeInSteps * DEFAULT_DURATION_IN_MS_PER_STEP) + }) + + editor.onDidScrollChange(({ scrollTop, scrollLeft, scrollWidth, scrollHeight }: MonacoEditor.IScrollEvent) => { + /*if (scrollHeight !== screenplayScroll.scrollHeight && + scrollLeft !== screenplayScroll.scrollLeft && + scrollTop !== screenplayScroll.scrollTop && + scrollWidth !== screenplayScroll.scrollWidth) { + */ + // console.log(`ScriptEditor:onDidScrollChange(${JSON.stringify({ scrollTop, scrollLeft, scrollWidth, scrollHeight }, null, 2)})`) + setScreenplayScroll({ + shouldRerender: false, + scrollTop, + scrollLeft, + scrollWidth, + scrollHeight + }) + //} + + // TODO we need to grab the leftmost segment + // now the problem is that this might be a bit costly to do + }) + + // as an optimization we can use this later, for surgical edits, + // to perform real time updates of the timeline + + model.onDidChangeContent((modelContentChangedEvent: MonacoEditor.editor.IModelContentChangedEvent) => { + // console.log("onDidChangeContent:") + for (const change of modelContentChangedEvent.changes) { + // console.log(" - change:", change) + } + }) + } + + const onChange = (plainText?: string) => { + if (!plainText) { return } + + if (plainText === scriptContent) { return } + + console.log("generic onChange:") + // this function is currently *very* expensive + // the only optimization right now is that we debounce it + rewriteCurrentScript(plainText) + } + + return ( +
+ +
+ ) +} diff --git a/src/components/core/render-clap/index.tsx b/src/components/core/render-clap/index.tsx deleted file mode 100644 index e07b61192514acbef57970313719532fb50bd899..0000000000000000000000000000000000000000 --- a/src/components/core/render-clap/index.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { ClapSegment } from "@aitube/clap" -import { useTimelineState } from "@aitube/timeline" - -import { StaticVideo } from "../static-video" - -// TODO: put this in a separate component -export function RenderClap() { - const finalVideo: ClapSegment | undefined = useTimelineState(s => s.finalVideo) - - console.log(`finalVideo:`, finalVideo) - const assetUrl: string = finalVideo?.assetUrl || "" - if (!assetUrl) { - return null - } - - return ( -
- -
- ) -} \ No newline at end of file diff --git a/src/components/core/timeline/index.tsx b/src/components/core/timeline/index.tsx index 79de7619236a0a8ac0ea28dc24558274f56c2fa1..705703d71e0c21ef04d2618f7419426eb23c106f 100644 --- a/src/components/core/timeline/index.tsx +++ b/src/components/core/timeline/index.tsx @@ -1,12 +1,11 @@ import { useEffect, useTransition } from "react" import { ClapSegment } from "@aitube/clap" -import { ClapTimeline, useTimelineState, SegmentRenderer } from "@aitube/timeline" -import { cn } from "@/lib/utils" +import { ClapTimeline, useTimeline, SegmentRenderer } from "@aitube/timeline" +import { cn } from "@/lib/utils" import { useSettings } from "@/controllers/settings" import { RenderRequest } from "@/types" -import { getVideoPrompt } from "@aitube/engine" -import { getRenderRequestPrompts } from "@/lib/utils/getRenderRequestPrompts" +import { useMonitor } from "@/controllers/monitor/useMonitor" const segmentRenderer: SegmentRenderer = async ({ segment, @@ -49,25 +48,37 @@ const segmentRenderer: SegmentRenderer = async ({ export function Timeline() { const [_isPending, startTransition] = useTransition() - const isReady = useTimelineState(s => s.isReady) + const isReady = useTimeline(s => s.isReady) const imageRenderingStrategy = useSettings(s => s.imageRenderingStrategy) - const setImageRenderingStrategy = useTimelineState(s => s.setImageRenderingStrategy) + const setImageRenderingStrategy = useTimeline(s => s.setImageRenderingStrategy) useEffect(() => { if (isReady) setImageRenderingStrategy(imageRenderingStrategy) }, [isReady, setImageRenderingStrategy, imageRenderingStrategy]) const videoRenderingStrategy = useSettings(s => s.videoRenderingStrategy) - const setVideoRenderingStrategy = useTimelineState(s => s.setVideoRenderingStrategy) + const setVideoRenderingStrategy = useTimeline(s => s.setVideoRenderingStrategy) useEffect(() => { if (isReady) setVideoRenderingStrategy(videoRenderingStrategy) }, [isReady, setVideoRenderingStrategy, videoRenderingStrategy]) const getSettings = useSettings(s => s.getSettings) - const setSegmentRenderer = useTimelineState(s => s.setSegmentRenderer) + const setSegmentRenderer = useTimeline(s => s.setSegmentRenderer) + + const jumpAt = useMonitor(s => s.jumpAt) + const checkIfPlaying = useMonitor(s => s.checkIfPlaying) + const togglePlayback = useMonitor(s => s.togglePlayback) + + const setJumpAt = useTimeline(s => s.setJumpAt) + const setIsPlaying = useTimeline(s => s.setIsPlaying) + const setTogglePlayback = useTimeline(s => s.setTogglePlayback) + // this is important: we connect the monitor to the timeline useEffect(() => { setSegmentRenderer(segmentRenderer) + setJumpAt(jumpAt) + setIsPlaying(checkIfPlaying) + setTogglePlayback(togglePlayback) }, [isReady]) return ( diff --git a/src/components/core/waveform/types.ts b/src/components/core/waveform/types.ts new file mode 100644 index 0000000000000000000000000000000000000000..0451df3f9b45ca7fecce6398e00cdcfd2fc6f9b2 --- /dev/null +++ b/src/components/core/waveform/types.ts @@ -0,0 +1,5 @@ + +export enum WaveformRenderingMode { + MONO = "MONO", + STERO = "STEREO" +} diff --git a/src/components/core/waveform/useWaveform.ts b/src/components/core/waveform/useWaveform.ts new file mode 100644 index 0000000000000000000000000000000000000000..f43de189129b600a418502a12c7697bb66fa029b --- /dev/null +++ b/src/components/core/waveform/useWaveform.ts @@ -0,0 +1,117 @@ +"use client" + +import { useLayoutEffect, useState } from "react" + +import { useDebounce } from "@/lib/hooks" + +import { WaveformRenderingMode } from "./types" + +// global cache definition +const cache: Record = {} + +/** + * This component renders a waveform for the given audio buffer + * + * @param { string } id - Unique id + * @param { AudioBuffer } audioBuffer - Audio buffer for the waveform + * @param { number } width - The width of the canvas + * @param { number } height - The height of the canvas + * @param { number } zoom - The zoom level of the waveform + * @param { string } color - The color of the waveform + * @param { WaveformRenderingMode } mode - The rendering mode (mono or stereo) + * @param { Function } onDone - Callback function to call when done + * @param { number } gain - Additional gain to increase the visibility + */ +export function useWaveform({ + id, + audioBuffer, + width = 500, + height = 100, + zoom = 1, + color = "#000000", + mode = WaveformRenderingMode.MONO, + onDone, + gain = 1.2, +}: { + id: string + audioBuffer: AudioBuffer + width: number + height: number + zoom?: number + color?: string + mode?: WaveformRenderingMode + onDone?: () => void + gain?: number +}): { + canvas: HTMLCanvasElement + canvasRenderingContext: CanvasRenderingContext2D + width: number + height: number + error: boolean +} { + const [canvas, setCanvas] = useState(document.createElement('canvas')) + const [canvasRenderingContext, setCanvasRenderingContext] = useState(canvas.getContext('2d') as CanvasRenderingContext2D) + + const error = !canvas || !canvasRenderingContext || !isFinite(width) || isNaN(width) || !isFinite(height) || isNaN(height) + + const cacheKey = `${id}:${width}:${height}:${zoom}:${color}:${gain}` + + const debounceSize = useDebounce(cacheKey, 300) + + const redraw = () => { + const cachedImage = cache[cacheKey] + + console.log(`redraw() for cache key "${cacheKey}"`) + if (canvasRenderingContext) { + if (cachedImage) { + console.log("redraw() we have a cached image:", cachedImage) + // If we have cached data + canvasRenderingContext.putImageData(cachedImage, 0, 0) + } else { + console.log("redraw() no cached image") + const middle = mode === WaveformRenderingMode.MONO ? height : height / 2 + const channelData = audioBuffer.getChannelData(0) + const step = Math.ceil(channelData.length / (width * zoom)) + canvasRenderingContext.fillStyle = color + + for (let i = 0; i < width; i += 1) { + let min = 1.0 + let max = -1.0 + + for (let j = 0; j < step; j += 1) { + const datum = channelData[(i * step) + j] + + if (datum < min) { + min = datum + } else if (datum > max) { + max = datum + } + + canvasRenderingContext.fillRect(i, (1 + min * gain) * middle, 1, Math.max(1, (max - min * gain) * middle)) + } + } + + // cache the drawn data + const imgData = canvasRenderingContext.getImageData(0, 0, width * zoom, height) + cache[cacheKey] = imgData + } + + onDone?.() + } else { + console.error("redraw(): no canvasRenderingContext") + } + } + + useLayoutEffect(() => { + if (error) { return } + redraw() + }, [id, debounceSize, zoom, color, onDone, gain, error]) + + return { + canvas, + canvasRenderingContext, + width: width * zoom, + height: height, + error, + } +} \ No newline at end of file diff --git a/src/components/dialogs/loader/LoadingDialog.tsx b/src/components/dialogs/loader/LoadingDialog.tsx index 62c4db596122d30fffc516ef9039111432cff4b4..bc9f8f01e01cf5b7b696296a762f7ea0b83cfced 100644 --- a/src/components/dialogs/loader/LoadingDialog.tsx +++ b/src/components/dialogs/loader/LoadingDialog.tsx @@ -2,7 +2,7 @@ import { Dialog, DialogContent } from "@/components/ui/dialog" import { Progress } from "@/components/ui/progress" import { cn } from "@/lib/utils" -import { useTasks } from "../../core/tasks/useTasks" +import { useTasks } from "../../tasks/useTasks" // a loading dialog that cannot be closed once it's loading export function LoadingDialog({ className = "" }: { className?: string }) { diff --git a/src/components/monitor/Counter/index.tsx b/src/components/monitor/Counter/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..695185fbd3167f1cdc0c1fce1f954d0eb4e91a6e --- /dev/null +++ b/src/components/monitor/Counter/index.tsx @@ -0,0 +1,61 @@ +import { clock } from "@/app/fonts" +import { cn } from "@/lib/utils" + +import { splitElapsedTime } from "../utils/splitElapsedTime" +import { zeroPad } from "../utils/zeroPad" +import { Separator } from "../Separator" + +export function Counter({ + valueInMs, + color = "text-gray-300", +}: { + valueInMs: number + color?: string +}) { + + const { hours, minutes, seconds, milliseconds } = splitElapsedTime(valueInMs) + + return ( +
+
+ { + zeroPad(hours || 0) + }{ + zeroPad(minutes || 0) + }{ + zeroPad(seconds || 0) + }{ + zeroPad(Math.min(999, milliseconds || 0)).slice(0, 2) + } +
+
+ ) +} \ No newline at end of file diff --git a/src/components/monitor/PlayerControls/index.tsx b/src/components/monitor/PlayerControls/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..de29cdd0eff76567ad950c1c2f1399fc91670570 --- /dev/null +++ b/src/components/monitor/PlayerControls/index.tsx @@ -0,0 +1,84 @@ +import { + TbPlayerPlay, + TbPlayerPlayFilled, + TbPlayerPause, + TbPlayerPauseFilled, + TbPlayerSkipBack, + TbPlayerSkipBackFilled, + TbPlayerTrackNext, + TbPlayerTrackNextFilled, + TbPlayerTrackPrev, + TbPlayerTrackPrevFilled +} from "react-icons/tb" +import { useTimeline } from "@aitube/timeline" + +import { cn } from "@/lib/utils" + +import { Counter } from "../Counter" +import { IconSwitch } from "../icons/icon-switch" +import { useMonitor } from "@/controllers/monitor/useMonitor" + +export function PlayerControls({ + className +}: { + className?: string +}) { + const isPlaying = useMonitor((s) => s.isPlaying) + const togglePlayback = useMonitor((s) => s.togglePlayback) + const jumpAt = useMonitor((s) => s.jumpAt) + + const cursorTimestampAtInMs = useTimeline((s) => s.cursorTimestampAtInMs) + const totalDurationInMs = useTimeline(s => s.totalDurationInMs) + + const handleAccelerate = () => { + } + + return ( +
+ +
+ + + +
+ +
+ ) +} \ No newline at end of file diff --git a/src/components/monitor/README.md b/src/components/monitor/README.md new file mode 100644 index 0000000000000000000000000000000000000000..ad44069007c7e4571993f1cdc71cfcc1eada0768 --- /dev/null +++ b/src/components/monitor/README.md @@ -0,0 +1,7 @@ +We are going to need to support multiple rendering modes + +The easiest is the case where we already have a video to show, +let's call it the "full" mode, which we can render using + +Then we have the "dynamic" mode, where we recompose a video from separate assets. +This mode can be rendered using \ No newline at end of file diff --git a/src/components/monitor/Separator/index.tsx b/src/components/monitor/Separator/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..107a42131b2a4df481a37f59fbb840dac25807c4 --- /dev/null +++ b/src/components/monitor/Separator/index.tsx @@ -0,0 +1,15 @@ + +import { cn } from "@/lib/utils" + +export function Separator({ className }: { className?: string }) { + return ( +
:
+ ) +} \ No newline at end of file diff --git a/src/components/core/static-video/index.tsx b/src/components/monitor/StaticPlayer/index.tsx similarity index 70% rename from src/components/core/static-video/index.tsx rename to src/components/monitor/StaticPlayer/index.tsx index 087df09081dba9ff538af418191b4d638b2e4f45..11835eeffaf6bb09d6f89a586f30f91d1efe6962 100644 --- a/src/components/core/static-video/index.tsx +++ b/src/components/monitor/StaticPlayer/index.tsx @@ -2,10 +2,11 @@ import React, { ReactNode, useEffect, useRef } from "react" -import { useTimelineState } from "@aitube/timeline" +import { useTimeline } from "@aitube/timeline" import { useRequestAnimationFrame } from "@/lib/hooks/useRequestAnimationFrame" +import { useMonitor } from "@/controllers/monitor/useMonitor" -export function StaticVideo({ +export function StaticPlayer({ video = "", isBusy = false, progress = 0, @@ -28,16 +29,23 @@ export function StaticVideo({ children: undefined, }) { - const setCursorTimestampAt = useTimelineState(s => s.setCursorTimestampAt) + const setStaticVideoRef = useMonitor(s => s.setStaticVideoRef) + + const setCursorTimestampAtInMs = useTimeline(s => s.setCursorTimestampAtInMs) const ref = useRef(null) - useRequestAnimationFrame(() => { - if (!ref.current) { return } - setCursorTimestampAt(ref.current.currentTime) + if (!ref.current || ref.current.paused) { return } + // important: we only update this if we have an actual PLAYING video + setCursorTimestampAtInMs(ref.current.currentTime * 1000) }) + useEffect(() => { + if (!ref.current) { return } + setStaticVideoRef(ref.current) + }, [ref]) + const placeholder =
@@ -70,14 +78,14 @@ export function StaticVideo({ : hasVideoContent ?