Fraser commited on
Commit
7ac86fa
·
0 Parent(s):

Initial backend setup with Gradio

Browse files
.gitignore ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Logs
2
+ logs
3
+ *.log
4
+ npm-debug.log*
5
+ yarn-debug.log*
6
+ yarn-error.log*
7
+ pnpm-debug.log*
8
+ lerna-debug.log*
9
+
10
+ node_modules
11
+ dist
12
+ dist-ssr
13
+ *.local
14
+
15
+ # Editor directories and files
16
+ .vscode/*
17
+ !.vscode/extensions.json
18
+ .idea
19
+ .DS_Store
20
+ *.suo
21
+ *.ntvs*
22
+ *.njsproj
23
+ *.sln
24
+ *.sw?
README.md ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Odyssey
3
+ emoji: 🎬
4
+ colorFrom: purple
5
+ colorTo: blue
6
+ sdk: static
7
+ app_build_command: npm run build
8
+ app_file: dist/index.html
9
+ pinned: false
10
+ short_description: AI video adventure powered by GPT-4 and Sora2
11
+ ---
12
+
13
+ # 🎬 Odyssey
14
+
15
+ An interactive video-based choose-your-own-adventure game powered by OpenAI's GPT-4 and Sora2.
16
+
17
+ [**Play Now!**](https://fraser-odyssey.static.hf.space/)
18
+
19
+ ## Features
20
+
21
+ - **AI-Generated Narratives**: GPT-4 creates engaging first-person story experiences
22
+ - **Sora2 Video Generation**: Each scene is brought to life with AI-generated videos
23
+ - **Seamless Continuity**: Videos flow smoothly using the final frame of each clip as the starting point for the next
24
+ - **Interactive Choices**: Shape your adventure with meaningful decisions at each turn
25
+
26
+ ## How to Use
27
+
28
+ 1. Enter your OpenAI API key (requires access to GPT-4o and Sora2)
29
+ 2. Start your adventure
30
+ 3. Watch the AI-generated video scene
31
+ 4. Make your choice to continue the story
32
+ 5. Experience seamless video continuity as your story unfolds
33
+
34
+ ## Architecture
35
+
36
+ - **Frontend**: This Space - Static Svelte 5 app
37
+ - **Backend**: [odyssey-backend](https://huggingface.co/spaces/Fraser/odyssey-backend) - Video upload service
38
+ - **Storage**: [odyssey-videos](https://huggingface.co/datasets/Fraser/odyssey-videos) - Dataset of generated videos
39
+
40
+ ## Technical Details
41
+
42
+ - Built with Svelte 5 + TypeScript + Vite
43
+ - Uses OpenAI's GPT-4o for narrative generation
44
+ - Uses OpenAI's Sora2 for video generation
45
+ - Implements frame-based continuity inspired by [sora-extend](https://github.com/mshumer/sora-extend)
46
+ - Canvas API for final frame extraction
47
+ - Gradio client for optional video uploads
48
+
49
+ ## Privacy
50
+
51
+ Your API key is stored only in your browser and never sent to our servers. All OpenAI API calls go directly from your browser to OpenAI.
index.html ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Odyssey Video Generator</title>
8
+ </head>
9
+ <body>
10
+ <div id="app"></div>
11
+ <script type="module" src="/src/main.ts"></script>
12
+ </body>
13
+ </html>
package-lock.json ADDED
@@ -0,0 +1,1435 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "odyssey-frontend",
3
+ "version": "0.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "odyssey-frontend",
9
+ "version": "0.0.0",
10
+ "devDependencies": {
11
+ "@sveltejs/vite-plugin-svelte": "^5.0.3",
12
+ "@tsconfig/svelte": "^5.0.4",
13
+ "@types/node": "^24.0.14",
14
+ "svelte": "^5.28.1",
15
+ "svelte-check": "^4.1.6",
16
+ "typescript": "~5.8.3",
17
+ "vite": "^6.3.5"
18
+ }
19
+ },
20
+ "node_modules/@esbuild/aix-ppc64": {
21
+ "version": "0.25.9",
22
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz",
23
+ "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==",
24
+ "cpu": [
25
+ "ppc64"
26
+ ],
27
+ "dev": true,
28
+ "license": "MIT",
29
+ "optional": true,
30
+ "os": [
31
+ "aix"
32
+ ],
33
+ "engines": {
34
+ "node": ">=18"
35
+ }
36
+ },
37
+ "node_modules/@esbuild/android-arm": {
38
+ "version": "0.25.9",
39
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz",
40
+ "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==",
41
+ "cpu": [
42
+ "arm"
43
+ ],
44
+ "dev": true,
45
+ "license": "MIT",
46
+ "optional": true,
47
+ "os": [
48
+ "android"
49
+ ],
50
+ "engines": {
51
+ "node": ">=18"
52
+ }
53
+ },
54
+ "node_modules/@esbuild/android-arm64": {
55
+ "version": "0.25.9",
56
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz",
57
+ "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==",
58
+ "cpu": [
59
+ "arm64"
60
+ ],
61
+ "dev": true,
62
+ "license": "MIT",
63
+ "optional": true,
64
+ "os": [
65
+ "android"
66
+ ],
67
+ "engines": {
68
+ "node": ">=18"
69
+ }
70
+ },
71
+ "node_modules/@esbuild/android-x64": {
72
+ "version": "0.25.9",
73
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz",
74
+ "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==",
75
+ "cpu": [
76
+ "x64"
77
+ ],
78
+ "dev": true,
79
+ "license": "MIT",
80
+ "optional": true,
81
+ "os": [
82
+ "android"
83
+ ],
84
+ "engines": {
85
+ "node": ">=18"
86
+ }
87
+ },
88
+ "node_modules/@esbuild/darwin-arm64": {
89
+ "version": "0.25.9",
90
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz",
91
+ "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==",
92
+ "cpu": [
93
+ "arm64"
94
+ ],
95
+ "dev": true,
96
+ "license": "MIT",
97
+ "optional": true,
98
+ "os": [
99
+ "darwin"
100
+ ],
101
+ "engines": {
102
+ "node": ">=18"
103
+ }
104
+ },
105
+ "node_modules/@esbuild/darwin-x64": {
106
+ "version": "0.25.9",
107
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz",
108
+ "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==",
109
+ "cpu": [
110
+ "x64"
111
+ ],
112
+ "dev": true,
113
+ "license": "MIT",
114
+ "optional": true,
115
+ "os": [
116
+ "darwin"
117
+ ],
118
+ "engines": {
119
+ "node": ">=18"
120
+ }
121
+ },
122
+ "node_modules/@esbuild/freebsd-arm64": {
123
+ "version": "0.25.9",
124
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz",
125
+ "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==",
126
+ "cpu": [
127
+ "arm64"
128
+ ],
129
+ "dev": true,
130
+ "license": "MIT",
131
+ "optional": true,
132
+ "os": [
133
+ "freebsd"
134
+ ],
135
+ "engines": {
136
+ "node": ">=18"
137
+ }
138
+ },
139
+ "node_modules/@esbuild/freebsd-x64": {
140
+ "version": "0.25.9",
141
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz",
142
+ "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==",
143
+ "cpu": [
144
+ "x64"
145
+ ],
146
+ "dev": true,
147
+ "license": "MIT",
148
+ "optional": true,
149
+ "os": [
150
+ "freebsd"
151
+ ],
152
+ "engines": {
153
+ "node": ">=18"
154
+ }
155
+ },
156
+ "node_modules/@esbuild/linux-arm": {
157
+ "version": "0.25.9",
158
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz",
159
+ "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==",
160
+ "cpu": [
161
+ "arm"
162
+ ],
163
+ "dev": true,
164
+ "license": "MIT",
165
+ "optional": true,
166
+ "os": [
167
+ "linux"
168
+ ],
169
+ "engines": {
170
+ "node": ">=18"
171
+ }
172
+ },
173
+ "node_modules/@esbuild/linux-arm64": {
174
+ "version": "0.25.9",
175
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz",
176
+ "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==",
177
+ "cpu": [
178
+ "arm64"
179
+ ],
180
+ "dev": true,
181
+ "license": "MIT",
182
+ "optional": true,
183
+ "os": [
184
+ "linux"
185
+ ],
186
+ "engines": {
187
+ "node": ">=18"
188
+ }
189
+ },
190
+ "node_modules/@esbuild/linux-ia32": {
191
+ "version": "0.25.9",
192
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz",
193
+ "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==",
194
+ "cpu": [
195
+ "ia32"
196
+ ],
197
+ "dev": true,
198
+ "license": "MIT",
199
+ "optional": true,
200
+ "os": [
201
+ "linux"
202
+ ],
203
+ "engines": {
204
+ "node": ">=18"
205
+ }
206
+ },
207
+ "node_modules/@esbuild/linux-loong64": {
208
+ "version": "0.25.9",
209
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz",
210
+ "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==",
211
+ "cpu": [
212
+ "loong64"
213
+ ],
214
+ "dev": true,
215
+ "license": "MIT",
216
+ "optional": true,
217
+ "os": [
218
+ "linux"
219
+ ],
220
+ "engines": {
221
+ "node": ">=18"
222
+ }
223
+ },
224
+ "node_modules/@esbuild/linux-mips64el": {
225
+ "version": "0.25.9",
226
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz",
227
+ "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==",
228
+ "cpu": [
229
+ "mips64el"
230
+ ],
231
+ "dev": true,
232
+ "license": "MIT",
233
+ "optional": true,
234
+ "os": [
235
+ "linux"
236
+ ],
237
+ "engines": {
238
+ "node": ">=18"
239
+ }
240
+ },
241
+ "node_modules/@esbuild/linux-ppc64": {
242
+ "version": "0.25.9",
243
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz",
244
+ "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==",
245
+ "cpu": [
246
+ "ppc64"
247
+ ],
248
+ "dev": true,
249
+ "license": "MIT",
250
+ "optional": true,
251
+ "os": [
252
+ "linux"
253
+ ],
254
+ "engines": {
255
+ "node": ">=18"
256
+ }
257
+ },
258
+ "node_modules/@esbuild/linux-riscv64": {
259
+ "version": "0.25.9",
260
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz",
261
+ "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==",
262
+ "cpu": [
263
+ "riscv64"
264
+ ],
265
+ "dev": true,
266
+ "license": "MIT",
267
+ "optional": true,
268
+ "os": [
269
+ "linux"
270
+ ],
271
+ "engines": {
272
+ "node": ">=18"
273
+ }
274
+ },
275
+ "node_modules/@esbuild/linux-s390x": {
276
+ "version": "0.25.9",
277
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz",
278
+ "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==",
279
+ "cpu": [
280
+ "s390x"
281
+ ],
282
+ "dev": true,
283
+ "license": "MIT",
284
+ "optional": true,
285
+ "os": [
286
+ "linux"
287
+ ],
288
+ "engines": {
289
+ "node": ">=18"
290
+ }
291
+ },
292
+ "node_modules/@esbuild/linux-x64": {
293
+ "version": "0.25.9",
294
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz",
295
+ "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==",
296
+ "cpu": [
297
+ "x64"
298
+ ],
299
+ "dev": true,
300
+ "license": "MIT",
301
+ "optional": true,
302
+ "os": [
303
+ "linux"
304
+ ],
305
+ "engines": {
306
+ "node": ">=18"
307
+ }
308
+ },
309
+ "node_modules/@esbuild/netbsd-arm64": {
310
+ "version": "0.25.9",
311
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz",
312
+ "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==",
313
+ "cpu": [
314
+ "arm64"
315
+ ],
316
+ "dev": true,
317
+ "license": "MIT",
318
+ "optional": true,
319
+ "os": [
320
+ "netbsd"
321
+ ],
322
+ "engines": {
323
+ "node": ">=18"
324
+ }
325
+ },
326
+ "node_modules/@esbuild/netbsd-x64": {
327
+ "version": "0.25.9",
328
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz",
329
+ "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==",
330
+ "cpu": [
331
+ "x64"
332
+ ],
333
+ "dev": true,
334
+ "license": "MIT",
335
+ "optional": true,
336
+ "os": [
337
+ "netbsd"
338
+ ],
339
+ "engines": {
340
+ "node": ">=18"
341
+ }
342
+ },
343
+ "node_modules/@esbuild/openbsd-arm64": {
344
+ "version": "0.25.9",
345
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz",
346
+ "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==",
347
+ "cpu": [
348
+ "arm64"
349
+ ],
350
+ "dev": true,
351
+ "license": "MIT",
352
+ "optional": true,
353
+ "os": [
354
+ "openbsd"
355
+ ],
356
+ "engines": {
357
+ "node": ">=18"
358
+ }
359
+ },
360
+ "node_modules/@esbuild/openbsd-x64": {
361
+ "version": "0.25.9",
362
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz",
363
+ "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==",
364
+ "cpu": [
365
+ "x64"
366
+ ],
367
+ "dev": true,
368
+ "license": "MIT",
369
+ "optional": true,
370
+ "os": [
371
+ "openbsd"
372
+ ],
373
+ "engines": {
374
+ "node": ">=18"
375
+ }
376
+ },
377
+ "node_modules/@esbuild/openharmony-arm64": {
378
+ "version": "0.25.9",
379
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz",
380
+ "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==",
381
+ "cpu": [
382
+ "arm64"
383
+ ],
384
+ "dev": true,
385
+ "license": "MIT",
386
+ "optional": true,
387
+ "os": [
388
+ "openharmony"
389
+ ],
390
+ "engines": {
391
+ "node": ">=18"
392
+ }
393
+ },
394
+ "node_modules/@esbuild/sunos-x64": {
395
+ "version": "0.25.9",
396
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz",
397
+ "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==",
398
+ "cpu": [
399
+ "x64"
400
+ ],
401
+ "dev": true,
402
+ "license": "MIT",
403
+ "optional": true,
404
+ "os": [
405
+ "sunos"
406
+ ],
407
+ "engines": {
408
+ "node": ">=18"
409
+ }
410
+ },
411
+ "node_modules/@esbuild/win32-arm64": {
412
+ "version": "0.25.9",
413
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz",
414
+ "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==",
415
+ "cpu": [
416
+ "arm64"
417
+ ],
418
+ "dev": true,
419
+ "license": "MIT",
420
+ "optional": true,
421
+ "os": [
422
+ "win32"
423
+ ],
424
+ "engines": {
425
+ "node": ">=18"
426
+ }
427
+ },
428
+ "node_modules/@esbuild/win32-ia32": {
429
+ "version": "0.25.9",
430
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz",
431
+ "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==",
432
+ "cpu": [
433
+ "ia32"
434
+ ],
435
+ "dev": true,
436
+ "license": "MIT",
437
+ "optional": true,
438
+ "os": [
439
+ "win32"
440
+ ],
441
+ "engines": {
442
+ "node": ">=18"
443
+ }
444
+ },
445
+ "node_modules/@esbuild/win32-x64": {
446
+ "version": "0.25.9",
447
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz",
448
+ "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==",
449
+ "cpu": [
450
+ "x64"
451
+ ],
452
+ "dev": true,
453
+ "license": "MIT",
454
+ "optional": true,
455
+ "os": [
456
+ "win32"
457
+ ],
458
+ "engines": {
459
+ "node": ">=18"
460
+ }
461
+ },
462
+ "node_modules/@jridgewell/gen-mapping": {
463
+ "version": "0.3.13",
464
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
465
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
466
+ "dev": true,
467
+ "license": "MIT",
468
+ "dependencies": {
469
+ "@jridgewell/sourcemap-codec": "^1.5.0",
470
+ "@jridgewell/trace-mapping": "^0.3.24"
471
+ }
472
+ },
473
+ "node_modules/@jridgewell/remapping": {
474
+ "version": "2.3.5",
475
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
476
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
477
+ "dev": true,
478
+ "license": "MIT",
479
+ "dependencies": {
480
+ "@jridgewell/gen-mapping": "^0.3.5",
481
+ "@jridgewell/trace-mapping": "^0.3.24"
482
+ }
483
+ },
484
+ "node_modules/@jridgewell/resolve-uri": {
485
+ "version": "3.1.2",
486
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
487
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
488
+ "dev": true,
489
+ "license": "MIT",
490
+ "engines": {
491
+ "node": ">=6.0.0"
492
+ }
493
+ },
494
+ "node_modules/@jridgewell/sourcemap-codec": {
495
+ "version": "1.5.5",
496
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
497
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
498
+ "dev": true,
499
+ "license": "MIT"
500
+ },
501
+ "node_modules/@jridgewell/trace-mapping": {
502
+ "version": "0.3.30",
503
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
504
+ "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
505
+ "dev": true,
506
+ "license": "MIT",
507
+ "dependencies": {
508
+ "@jridgewell/resolve-uri": "^3.1.0",
509
+ "@jridgewell/sourcemap-codec": "^1.4.14"
510
+ }
511
+ },
512
+ "node_modules/@rollup/rollup-android-arm-eabi": {
513
+ "version": "4.47.1",
514
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.47.1.tgz",
515
+ "integrity": "sha512-lTahKRJip0knffA/GTNFJMrToD+CM+JJ+Qt5kjzBK/sFQ0EWqfKW3AYQSlZXN98tX0lx66083U9JYIMioMMK7g==",
516
+ "cpu": [
517
+ "arm"
518
+ ],
519
+ "dev": true,
520
+ "license": "MIT",
521
+ "optional": true,
522
+ "os": [
523
+ "android"
524
+ ]
525
+ },
526
+ "node_modules/@rollup/rollup-android-arm64": {
527
+ "version": "4.47.1",
528
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.47.1.tgz",
529
+ "integrity": "sha512-uqxkb3RJLzlBbh/bbNQ4r7YpSZnjgMgyoEOY7Fy6GCbelkDSAzeiogxMG9TfLsBbqmGsdDObo3mzGqa8hps4MA==",
530
+ "cpu": [
531
+ "arm64"
532
+ ],
533
+ "dev": true,
534
+ "license": "MIT",
535
+ "optional": true,
536
+ "os": [
537
+ "android"
538
+ ]
539
+ },
540
+ "node_modules/@rollup/rollup-darwin-arm64": {
541
+ "version": "4.47.1",
542
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.47.1.tgz",
543
+ "integrity": "sha512-tV6reObmxBDS4DDyLzTDIpymthNlxrLBGAoQx6m2a7eifSNEZdkXQl1PE4ZjCkEDPVgNXSzND/k9AQ3mC4IOEQ==",
544
+ "cpu": [
545
+ "arm64"
546
+ ],
547
+ "dev": true,
548
+ "license": "MIT",
549
+ "optional": true,
550
+ "os": [
551
+ "darwin"
552
+ ]
553
+ },
554
+ "node_modules/@rollup/rollup-darwin-x64": {
555
+ "version": "4.47.1",
556
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.47.1.tgz",
557
+ "integrity": "sha512-XuJRPTnMk1lwsSnS3vYyVMu4x/+WIw1MMSiqj5C4j3QOWsMzbJEK90zG+SWV1h0B1ABGCQ0UZUjti+TQK35uHQ==",
558
+ "cpu": [
559
+ "x64"
560
+ ],
561
+ "dev": true,
562
+ "license": "MIT",
563
+ "optional": true,
564
+ "os": [
565
+ "darwin"
566
+ ]
567
+ },
568
+ "node_modules/@rollup/rollup-freebsd-arm64": {
569
+ "version": "4.47.1",
570
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.47.1.tgz",
571
+ "integrity": "sha512-79BAm8Ag/tmJ5asCqgOXsb3WY28Rdd5Lxj8ONiQzWzy9LvWORd5qVuOnjlqiWWZJw+dWewEktZb5yiM1DLLaHw==",
572
+ "cpu": [
573
+ "arm64"
574
+ ],
575
+ "dev": true,
576
+ "license": "MIT",
577
+ "optional": true,
578
+ "os": [
579
+ "freebsd"
580
+ ]
581
+ },
582
+ "node_modules/@rollup/rollup-freebsd-x64": {
583
+ "version": "4.47.1",
584
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.47.1.tgz",
585
+ "integrity": "sha512-OQ2/ZDGzdOOlyfqBiip0ZX/jVFekzYrGtUsqAfLDbWy0jh1PUU18+jYp8UMpqhly5ltEqotc2miLngf9FPSWIA==",
586
+ "cpu": [
587
+ "x64"
588
+ ],
589
+ "dev": true,
590
+ "license": "MIT",
591
+ "optional": true,
592
+ "os": [
593
+ "freebsd"
594
+ ]
595
+ },
596
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
597
+ "version": "4.47.1",
598
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.47.1.tgz",
599
+ "integrity": "sha512-HZZBXJL1udxlCVvoVadstgiU26seKkHbbAMLg7680gAcMnRNP9SAwTMVet02ANA94kXEI2VhBnXs4e5nf7KG2A==",
600
+ "cpu": [
601
+ "arm"
602
+ ],
603
+ "dev": true,
604
+ "license": "MIT",
605
+ "optional": true,
606
+ "os": [
607
+ "linux"
608
+ ]
609
+ },
610
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
611
+ "version": "4.47.1",
612
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.47.1.tgz",
613
+ "integrity": "sha512-sZ5p2I9UA7T950JmuZ3pgdKA6+RTBr+0FpK427ExW0t7n+QwYOcmDTK/aRlzoBrWyTpJNlS3kacgSlSTUg6P/Q==",
614
+ "cpu": [
615
+ "arm"
616
+ ],
617
+ "dev": true,
618
+ "license": "MIT",
619
+ "optional": true,
620
+ "os": [
621
+ "linux"
622
+ ]
623
+ },
624
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
625
+ "version": "4.47.1",
626
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.47.1.tgz",
627
+ "integrity": "sha512-3hBFoqPyU89Dyf1mQRXCdpc6qC6At3LV6jbbIOZd72jcx7xNk3aAp+EjzAtN6sDlmHFzsDJN5yeUySvorWeRXA==",
628
+ "cpu": [
629
+ "arm64"
630
+ ],
631
+ "dev": true,
632
+ "license": "MIT",
633
+ "optional": true,
634
+ "os": [
635
+ "linux"
636
+ ]
637
+ },
638
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
639
+ "version": "4.47.1",
640
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.47.1.tgz",
641
+ "integrity": "sha512-49J4FnMHfGodJWPw73Ve+/hsPjZgcXQGkmqBGZFvltzBKRS+cvMiWNLadOMXKGnYRhs1ToTGM0sItKISoSGUNA==",
642
+ "cpu": [
643
+ "arm64"
644
+ ],
645
+ "dev": true,
646
+ "license": "MIT",
647
+ "optional": true,
648
+ "os": [
649
+ "linux"
650
+ ]
651
+ },
652
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
653
+ "version": "4.47.1",
654
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.47.1.tgz",
655
+ "integrity": "sha512-4yYU8p7AneEpQkRX03pbpLmE21z5JNys16F1BZBZg5fP9rIlb0TkeQjn5du5w4agConCCEoYIG57sNxjryHEGg==",
656
+ "cpu": [
657
+ "loong64"
658
+ ],
659
+ "dev": true,
660
+ "license": "MIT",
661
+ "optional": true,
662
+ "os": [
663
+ "linux"
664
+ ]
665
+ },
666
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
667
+ "version": "4.47.1",
668
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.47.1.tgz",
669
+ "integrity": "sha512-fAiq+J28l2YMWgC39jz/zPi2jqc0y3GSRo1yyxlBHt6UN0yYgnegHSRPa3pnHS5amT/efXQrm0ug5+aNEu9UuQ==",
670
+ "cpu": [
671
+ "ppc64"
672
+ ],
673
+ "dev": true,
674
+ "license": "MIT",
675
+ "optional": true,
676
+ "os": [
677
+ "linux"
678
+ ]
679
+ },
680
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
681
+ "version": "4.47.1",
682
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.47.1.tgz",
683
+ "integrity": "sha512-daoT0PMENNdjVYYU9xec30Y2prb1AbEIbb64sqkcQcSaR0zYuKkoPuhIztfxuqN82KYCKKrj+tQe4Gi7OSm1ow==",
684
+ "cpu": [
685
+ "riscv64"
686
+ ],
687
+ "dev": true,
688
+ "license": "MIT",
689
+ "optional": true,
690
+ "os": [
691
+ "linux"
692
+ ]
693
+ },
694
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
695
+ "version": "4.47.1",
696
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.47.1.tgz",
697
+ "integrity": "sha512-JNyXaAhWtdzfXu5pUcHAuNwGQKevR+6z/poYQKVW+pLaYOj9G1meYc57/1Xv2u4uTxfu9qEWmNTjv/H/EpAisw==",
698
+ "cpu": [
699
+ "riscv64"
700
+ ],
701
+ "dev": true,
702
+ "license": "MIT",
703
+ "optional": true,
704
+ "os": [
705
+ "linux"
706
+ ]
707
+ },
708
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
709
+ "version": "4.47.1",
710
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.47.1.tgz",
711
+ "integrity": "sha512-U/CHbqKSwEQyZXjCpY43/GLYcTVKEXeRHw0rMBJP7fP3x6WpYG4LTJWR3ic6TeYKX6ZK7mrhltP4ppolyVhLVQ==",
712
+ "cpu": [
713
+ "s390x"
714
+ ],
715
+ "dev": true,
716
+ "license": "MIT",
717
+ "optional": true,
718
+ "os": [
719
+ "linux"
720
+ ]
721
+ },
722
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
723
+ "version": "4.47.1",
724
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.47.1.tgz",
725
+ "integrity": "sha512-uTLEakjxOTElfeZIGWkC34u2auLHB1AYS6wBjPGI00bWdxdLcCzK5awjs25YXpqB9lS8S0vbO0t9ZcBeNibA7g==",
726
+ "cpu": [
727
+ "x64"
728
+ ],
729
+ "dev": true,
730
+ "license": "MIT",
731
+ "optional": true,
732
+ "os": [
733
+ "linux"
734
+ ]
735
+ },
736
+ "node_modules/@rollup/rollup-linux-x64-musl": {
737
+ "version": "4.47.1",
738
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.47.1.tgz",
739
+ "integrity": "sha512-Ft+d/9DXs30BK7CHCTX11FtQGHUdpNDLJW0HHLign4lgMgBcPFN3NkdIXhC5r9iwsMwYreBBc4Rho5ieOmKNVQ==",
740
+ "cpu": [
741
+ "x64"
742
+ ],
743
+ "dev": true,
744
+ "license": "MIT",
745
+ "optional": true,
746
+ "os": [
747
+ "linux"
748
+ ]
749
+ },
750
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
751
+ "version": "4.47.1",
752
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.47.1.tgz",
753
+ "integrity": "sha512-N9X5WqGYzZnjGAFsKSfYFtAShYjwOmFJoWbLg3dYixZOZqU7hdMq+/xyS14zKLhFhZDhP9VfkzQnsdk0ZDS9IA==",
754
+ "cpu": [
755
+ "arm64"
756
+ ],
757
+ "dev": true,
758
+ "license": "MIT",
759
+ "optional": true,
760
+ "os": [
761
+ "win32"
762
+ ]
763
+ },
764
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
765
+ "version": "4.47.1",
766
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.47.1.tgz",
767
+ "integrity": "sha512-O+KcfeCORZADEY8oQJk4HK8wtEOCRE4MdOkb8qGZQNun3jzmj2nmhV/B/ZaaZOkPmJyvm/gW9n0gsB4eRa1eiQ==",
768
+ "cpu": [
769
+ "ia32"
770
+ ],
771
+ "dev": true,
772
+ "license": "MIT",
773
+ "optional": true,
774
+ "os": [
775
+ "win32"
776
+ ]
777
+ },
778
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
779
+ "version": "4.47.1",
780
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.47.1.tgz",
781
+ "integrity": "sha512-CpKnYa8eHthJa3c+C38v/E+/KZyF1Jdh2Cz3DyKZqEWYgrM1IHFArXNWvBLPQCKUEsAqqKX27tTqVEFbDNUcOA==",
782
+ "cpu": [
783
+ "x64"
784
+ ],
785
+ "dev": true,
786
+ "license": "MIT",
787
+ "optional": true,
788
+ "os": [
789
+ "win32"
790
+ ]
791
+ },
792
+ "node_modules/@sveltejs/acorn-typescript": {
793
+ "version": "1.0.5",
794
+ "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz",
795
+ "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==",
796
+ "dev": true,
797
+ "license": "MIT",
798
+ "peerDependencies": {
799
+ "acorn": "^8.9.0"
800
+ }
801
+ },
802
+ "node_modules/@sveltejs/vite-plugin-svelte": {
803
+ "version": "5.1.1",
804
+ "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.1.tgz",
805
+ "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==",
806
+ "dev": true,
807
+ "license": "MIT",
808
+ "dependencies": {
809
+ "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1",
810
+ "debug": "^4.4.1",
811
+ "deepmerge": "^4.3.1",
812
+ "kleur": "^4.1.5",
813
+ "magic-string": "^0.30.17",
814
+ "vitefu": "^1.0.6"
815
+ },
816
+ "engines": {
817
+ "node": "^18.0.0 || ^20.0.0 || >=22"
818
+ },
819
+ "peerDependencies": {
820
+ "svelte": "^5.0.0",
821
+ "vite": "^6.0.0"
822
+ }
823
+ },
824
+ "node_modules/@sveltejs/vite-plugin-svelte-inspector": {
825
+ "version": "4.0.1",
826
+ "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz",
827
+ "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==",
828
+ "dev": true,
829
+ "license": "MIT",
830
+ "dependencies": {
831
+ "debug": "^4.3.7"
832
+ },
833
+ "engines": {
834
+ "node": "^18.0.0 || ^20.0.0 || >=22"
835
+ },
836
+ "peerDependencies": {
837
+ "@sveltejs/vite-plugin-svelte": "^5.0.0",
838
+ "svelte": "^5.0.0",
839
+ "vite": "^6.0.0"
840
+ }
841
+ },
842
+ "node_modules/@tsconfig/svelte": {
843
+ "version": "5.0.4",
844
+ "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.4.tgz",
845
+ "integrity": "sha512-BV9NplVgLmSi4mwKzD8BD/NQ8erOY/nUE/GpgWe2ckx+wIQF5RyRirn/QsSSCPeulVpc3RA/iJt6DpfTIZps0Q==",
846
+ "dev": true,
847
+ "license": "MIT"
848
+ },
849
+ "node_modules/@types/estree": {
850
+ "version": "1.0.8",
851
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
852
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
853
+ "dev": true,
854
+ "license": "MIT"
855
+ },
856
+ "node_modules/@types/node": {
857
+ "version": "24.3.0",
858
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz",
859
+ "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==",
860
+ "dev": true,
861
+ "license": "MIT",
862
+ "dependencies": {
863
+ "undici-types": "~7.10.0"
864
+ }
865
+ },
866
+ "node_modules/acorn": {
867
+ "version": "8.15.0",
868
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
869
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
870
+ "dev": true,
871
+ "license": "MIT",
872
+ "bin": {
873
+ "acorn": "bin/acorn"
874
+ },
875
+ "engines": {
876
+ "node": ">=0.4.0"
877
+ }
878
+ },
879
+ "node_modules/aria-query": {
880
+ "version": "5.3.2",
881
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
882
+ "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
883
+ "dev": true,
884
+ "license": "Apache-2.0",
885
+ "engines": {
886
+ "node": ">= 0.4"
887
+ }
888
+ },
889
+ "node_modules/axobject-query": {
890
+ "version": "4.1.0",
891
+ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
892
+ "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
893
+ "dev": true,
894
+ "license": "Apache-2.0",
895
+ "engines": {
896
+ "node": ">= 0.4"
897
+ }
898
+ },
899
+ "node_modules/chokidar": {
900
+ "version": "4.0.3",
901
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
902
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
903
+ "dev": true,
904
+ "license": "MIT",
905
+ "dependencies": {
906
+ "readdirp": "^4.0.1"
907
+ },
908
+ "engines": {
909
+ "node": ">= 14.16.0"
910
+ },
911
+ "funding": {
912
+ "url": "https://paulmillr.com/funding/"
913
+ }
914
+ },
915
+ "node_modules/clsx": {
916
+ "version": "2.1.1",
917
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
918
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
919
+ "dev": true,
920
+ "license": "MIT",
921
+ "engines": {
922
+ "node": ">=6"
923
+ }
924
+ },
925
+ "node_modules/debug": {
926
+ "version": "4.4.1",
927
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
928
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
929
+ "dev": true,
930
+ "license": "MIT",
931
+ "dependencies": {
932
+ "ms": "^2.1.3"
933
+ },
934
+ "engines": {
935
+ "node": ">=6.0"
936
+ },
937
+ "peerDependenciesMeta": {
938
+ "supports-color": {
939
+ "optional": true
940
+ }
941
+ }
942
+ },
943
+ "node_modules/deepmerge": {
944
+ "version": "4.3.1",
945
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
946
+ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
947
+ "dev": true,
948
+ "license": "MIT",
949
+ "engines": {
950
+ "node": ">=0.10.0"
951
+ }
952
+ },
953
+ "node_modules/esbuild": {
954
+ "version": "0.25.9",
955
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz",
956
+ "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==",
957
+ "dev": true,
958
+ "hasInstallScript": true,
959
+ "license": "MIT",
960
+ "bin": {
961
+ "esbuild": "bin/esbuild"
962
+ },
963
+ "engines": {
964
+ "node": ">=18"
965
+ },
966
+ "optionalDependencies": {
967
+ "@esbuild/aix-ppc64": "0.25.9",
968
+ "@esbuild/android-arm": "0.25.9",
969
+ "@esbuild/android-arm64": "0.25.9",
970
+ "@esbuild/android-x64": "0.25.9",
971
+ "@esbuild/darwin-arm64": "0.25.9",
972
+ "@esbuild/darwin-x64": "0.25.9",
973
+ "@esbuild/freebsd-arm64": "0.25.9",
974
+ "@esbuild/freebsd-x64": "0.25.9",
975
+ "@esbuild/linux-arm": "0.25.9",
976
+ "@esbuild/linux-arm64": "0.25.9",
977
+ "@esbuild/linux-ia32": "0.25.9",
978
+ "@esbuild/linux-loong64": "0.25.9",
979
+ "@esbuild/linux-mips64el": "0.25.9",
980
+ "@esbuild/linux-ppc64": "0.25.9",
981
+ "@esbuild/linux-riscv64": "0.25.9",
982
+ "@esbuild/linux-s390x": "0.25.9",
983
+ "@esbuild/linux-x64": "0.25.9",
984
+ "@esbuild/netbsd-arm64": "0.25.9",
985
+ "@esbuild/netbsd-x64": "0.25.9",
986
+ "@esbuild/openbsd-arm64": "0.25.9",
987
+ "@esbuild/openbsd-x64": "0.25.9",
988
+ "@esbuild/openharmony-arm64": "0.25.9",
989
+ "@esbuild/sunos-x64": "0.25.9",
990
+ "@esbuild/win32-arm64": "0.25.9",
991
+ "@esbuild/win32-ia32": "0.25.9",
992
+ "@esbuild/win32-x64": "0.25.9"
993
+ }
994
+ },
995
+ "node_modules/esm-env": {
996
+ "version": "1.2.2",
997
+ "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
998
+ "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
999
+ "dev": true,
1000
+ "license": "MIT"
1001
+ },
1002
+ "node_modules/esrap": {
1003
+ "version": "2.1.0",
1004
+ "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.0.tgz",
1005
+ "integrity": "sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==",
1006
+ "dev": true,
1007
+ "license": "MIT",
1008
+ "dependencies": {
1009
+ "@jridgewell/sourcemap-codec": "^1.4.15"
1010
+ }
1011
+ },
1012
+ "node_modules/fdir": {
1013
+ "version": "6.5.0",
1014
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
1015
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
1016
+ "dev": true,
1017
+ "license": "MIT",
1018
+ "engines": {
1019
+ "node": ">=12.0.0"
1020
+ },
1021
+ "peerDependencies": {
1022
+ "picomatch": "^3 || ^4"
1023
+ },
1024
+ "peerDependenciesMeta": {
1025
+ "picomatch": {
1026
+ "optional": true
1027
+ }
1028
+ }
1029
+ },
1030
+ "node_modules/fsevents": {
1031
+ "version": "2.3.3",
1032
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
1033
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
1034
+ "dev": true,
1035
+ "hasInstallScript": true,
1036
+ "license": "MIT",
1037
+ "optional": true,
1038
+ "os": [
1039
+ "darwin"
1040
+ ],
1041
+ "engines": {
1042
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1043
+ }
1044
+ },
1045
+ "node_modules/is-reference": {
1046
+ "version": "3.0.3",
1047
+ "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
1048
+ "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
1049
+ "dev": true,
1050
+ "license": "MIT",
1051
+ "dependencies": {
1052
+ "@types/estree": "^1.0.6"
1053
+ }
1054
+ },
1055
+ "node_modules/kleur": {
1056
+ "version": "4.1.5",
1057
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
1058
+ "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
1059
+ "dev": true,
1060
+ "license": "MIT",
1061
+ "engines": {
1062
+ "node": ">=6"
1063
+ }
1064
+ },
1065
+ "node_modules/locate-character": {
1066
+ "version": "3.0.0",
1067
+ "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
1068
+ "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
1069
+ "dev": true,
1070
+ "license": "MIT"
1071
+ },
1072
+ "node_modules/magic-string": {
1073
+ "version": "0.30.17",
1074
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
1075
+ "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
1076
+ "dev": true,
1077
+ "license": "MIT",
1078
+ "dependencies": {
1079
+ "@jridgewell/sourcemap-codec": "^1.5.0"
1080
+ }
1081
+ },
1082
+ "node_modules/mri": {
1083
+ "version": "1.2.0",
1084
+ "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
1085
+ "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==",
1086
+ "dev": true,
1087
+ "license": "MIT",
1088
+ "engines": {
1089
+ "node": ">=4"
1090
+ }
1091
+ },
1092
+ "node_modules/ms": {
1093
+ "version": "2.1.3",
1094
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1095
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1096
+ "dev": true,
1097
+ "license": "MIT"
1098
+ },
1099
+ "node_modules/nanoid": {
1100
+ "version": "3.3.11",
1101
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
1102
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
1103
+ "dev": true,
1104
+ "funding": [
1105
+ {
1106
+ "type": "github",
1107
+ "url": "https://github.com/sponsors/ai"
1108
+ }
1109
+ ],
1110
+ "license": "MIT",
1111
+ "bin": {
1112
+ "nanoid": "bin/nanoid.cjs"
1113
+ },
1114
+ "engines": {
1115
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
1116
+ }
1117
+ },
1118
+ "node_modules/picocolors": {
1119
+ "version": "1.1.1",
1120
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
1121
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
1122
+ "dev": true,
1123
+ "license": "ISC"
1124
+ },
1125
+ "node_modules/picomatch": {
1126
+ "version": "4.0.3",
1127
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
1128
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
1129
+ "dev": true,
1130
+ "license": "MIT",
1131
+ "engines": {
1132
+ "node": ">=12"
1133
+ },
1134
+ "funding": {
1135
+ "url": "https://github.com/sponsors/jonschlinkert"
1136
+ }
1137
+ },
1138
+ "node_modules/postcss": {
1139
+ "version": "8.5.6",
1140
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
1141
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
1142
+ "dev": true,
1143
+ "funding": [
1144
+ {
1145
+ "type": "opencollective",
1146
+ "url": "https://opencollective.com/postcss/"
1147
+ },
1148
+ {
1149
+ "type": "tidelift",
1150
+ "url": "https://tidelift.com/funding/github/npm/postcss"
1151
+ },
1152
+ {
1153
+ "type": "github",
1154
+ "url": "https://github.com/sponsors/ai"
1155
+ }
1156
+ ],
1157
+ "license": "MIT",
1158
+ "dependencies": {
1159
+ "nanoid": "^3.3.11",
1160
+ "picocolors": "^1.1.1",
1161
+ "source-map-js": "^1.2.1"
1162
+ },
1163
+ "engines": {
1164
+ "node": "^10 || ^12 || >=14"
1165
+ }
1166
+ },
1167
+ "node_modules/readdirp": {
1168
+ "version": "4.1.2",
1169
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
1170
+ "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
1171
+ "dev": true,
1172
+ "license": "MIT",
1173
+ "engines": {
1174
+ "node": ">= 14.18.0"
1175
+ },
1176
+ "funding": {
1177
+ "type": "individual",
1178
+ "url": "https://paulmillr.com/funding/"
1179
+ }
1180
+ },
1181
+ "node_modules/rollup": {
1182
+ "version": "4.47.1",
1183
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.47.1.tgz",
1184
+ "integrity": "sha512-iasGAQoZ5dWDzULEUX3jiW0oB1qyFOepSyDyoU6S/OhVlDIwj5knI5QBa5RRQ0sK7OE0v+8VIi2JuV+G+3tfNg==",
1185
+ "dev": true,
1186
+ "license": "MIT",
1187
+ "dependencies": {
1188
+ "@types/estree": "1.0.8"
1189
+ },
1190
+ "bin": {
1191
+ "rollup": "dist/bin/rollup"
1192
+ },
1193
+ "engines": {
1194
+ "node": ">=18.0.0",
1195
+ "npm": ">=8.0.0"
1196
+ },
1197
+ "optionalDependencies": {
1198
+ "@rollup/rollup-android-arm-eabi": "4.47.1",
1199
+ "@rollup/rollup-android-arm64": "4.47.1",
1200
+ "@rollup/rollup-darwin-arm64": "4.47.1",
1201
+ "@rollup/rollup-darwin-x64": "4.47.1",
1202
+ "@rollup/rollup-freebsd-arm64": "4.47.1",
1203
+ "@rollup/rollup-freebsd-x64": "4.47.1",
1204
+ "@rollup/rollup-linux-arm-gnueabihf": "4.47.1",
1205
+ "@rollup/rollup-linux-arm-musleabihf": "4.47.1",
1206
+ "@rollup/rollup-linux-arm64-gnu": "4.47.1",
1207
+ "@rollup/rollup-linux-arm64-musl": "4.47.1",
1208
+ "@rollup/rollup-linux-loongarch64-gnu": "4.47.1",
1209
+ "@rollup/rollup-linux-ppc64-gnu": "4.47.1",
1210
+ "@rollup/rollup-linux-riscv64-gnu": "4.47.1",
1211
+ "@rollup/rollup-linux-riscv64-musl": "4.47.1",
1212
+ "@rollup/rollup-linux-s390x-gnu": "4.47.1",
1213
+ "@rollup/rollup-linux-x64-gnu": "4.47.1",
1214
+ "@rollup/rollup-linux-x64-musl": "4.47.1",
1215
+ "@rollup/rollup-win32-arm64-msvc": "4.47.1",
1216
+ "@rollup/rollup-win32-ia32-msvc": "4.47.1",
1217
+ "@rollup/rollup-win32-x64-msvc": "4.47.1",
1218
+ "fsevents": "~2.3.2"
1219
+ }
1220
+ },
1221
+ "node_modules/sade": {
1222
+ "version": "1.8.1",
1223
+ "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
1224
+ "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==",
1225
+ "dev": true,
1226
+ "license": "MIT",
1227
+ "dependencies": {
1228
+ "mri": "^1.1.0"
1229
+ },
1230
+ "engines": {
1231
+ "node": ">=6"
1232
+ }
1233
+ },
1234
+ "node_modules/source-map-js": {
1235
+ "version": "1.2.1",
1236
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
1237
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
1238
+ "dev": true,
1239
+ "license": "BSD-3-Clause",
1240
+ "engines": {
1241
+ "node": ">=0.10.0"
1242
+ }
1243
+ },
1244
+ "node_modules/svelte": {
1245
+ "version": "5.38.2",
1246
+ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.38.2.tgz",
1247
+ "integrity": "sha512-iAcp/oFAWauVSGILdD67n7DiwgLHXZzWZIdzl7araRxu72jUr7PFAo2Iie7gXt0IbnlYvhxCb9GT3ZJUquO3PA==",
1248
+ "dev": true,
1249
+ "license": "MIT",
1250
+ "dependencies": {
1251
+ "@jridgewell/remapping": "^2.3.4",
1252
+ "@jridgewell/sourcemap-codec": "^1.5.0",
1253
+ "@sveltejs/acorn-typescript": "^1.0.5",
1254
+ "@types/estree": "^1.0.5",
1255
+ "acorn": "^8.12.1",
1256
+ "aria-query": "^5.3.1",
1257
+ "axobject-query": "^4.1.0",
1258
+ "clsx": "^2.1.1",
1259
+ "esm-env": "^1.2.1",
1260
+ "esrap": "^2.1.0",
1261
+ "is-reference": "^3.0.3",
1262
+ "locate-character": "^3.0.0",
1263
+ "magic-string": "^0.30.11",
1264
+ "zimmerframe": "^1.1.2"
1265
+ },
1266
+ "engines": {
1267
+ "node": ">=18"
1268
+ }
1269
+ },
1270
+ "node_modules/svelte-check": {
1271
+ "version": "4.3.1",
1272
+ "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.1.tgz",
1273
+ "integrity": "sha512-lkh8gff5gpHLjxIV+IaApMxQhTGnir2pNUAqcNgeKkvK5bT/30Ey/nzBxNLDlkztCH4dP7PixkMt9SWEKFPBWg==",
1274
+ "dev": true,
1275
+ "license": "MIT",
1276
+ "dependencies": {
1277
+ "@jridgewell/trace-mapping": "^0.3.25",
1278
+ "chokidar": "^4.0.1",
1279
+ "fdir": "^6.2.0",
1280
+ "picocolors": "^1.0.0",
1281
+ "sade": "^1.7.4"
1282
+ },
1283
+ "bin": {
1284
+ "svelte-check": "bin/svelte-check"
1285
+ },
1286
+ "engines": {
1287
+ "node": ">= 18.0.0"
1288
+ },
1289
+ "peerDependencies": {
1290
+ "svelte": "^4.0.0 || ^5.0.0-next.0",
1291
+ "typescript": ">=5.0.0"
1292
+ }
1293
+ },
1294
+ "node_modules/tinyglobby": {
1295
+ "version": "0.2.14",
1296
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
1297
+ "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
1298
+ "dev": true,
1299
+ "license": "MIT",
1300
+ "dependencies": {
1301
+ "fdir": "^6.4.4",
1302
+ "picomatch": "^4.0.2"
1303
+ },
1304
+ "engines": {
1305
+ "node": ">=12.0.0"
1306
+ },
1307
+ "funding": {
1308
+ "url": "https://github.com/sponsors/SuperchupuDev"
1309
+ }
1310
+ },
1311
+ "node_modules/typescript": {
1312
+ "version": "5.8.3",
1313
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
1314
+ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
1315
+ "dev": true,
1316
+ "license": "Apache-2.0",
1317
+ "bin": {
1318
+ "tsc": "bin/tsc",
1319
+ "tsserver": "bin/tsserver"
1320
+ },
1321
+ "engines": {
1322
+ "node": ">=14.17"
1323
+ }
1324
+ },
1325
+ "node_modules/undici-types": {
1326
+ "version": "7.10.0",
1327
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
1328
+ "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
1329
+ "dev": true,
1330
+ "license": "MIT"
1331
+ },
1332
+ "node_modules/vite": {
1333
+ "version": "6.3.5",
1334
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
1335
+ "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
1336
+ "dev": true,
1337
+ "license": "MIT",
1338
+ "dependencies": {
1339
+ "esbuild": "^0.25.0",
1340
+ "fdir": "^6.4.4",
1341
+ "picomatch": "^4.0.2",
1342
+ "postcss": "^8.5.3",
1343
+ "rollup": "^4.34.9",
1344
+ "tinyglobby": "^0.2.13"
1345
+ },
1346
+ "bin": {
1347
+ "vite": "bin/vite.js"
1348
+ },
1349
+ "engines": {
1350
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
1351
+ },
1352
+ "funding": {
1353
+ "url": "https://github.com/vitejs/vite?sponsor=1"
1354
+ },
1355
+ "optionalDependencies": {
1356
+ "fsevents": "~2.3.3"
1357
+ },
1358
+ "peerDependencies": {
1359
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
1360
+ "jiti": ">=1.21.0",
1361
+ "less": "*",
1362
+ "lightningcss": "^1.21.0",
1363
+ "sass": "*",
1364
+ "sass-embedded": "*",
1365
+ "stylus": "*",
1366
+ "sugarss": "*",
1367
+ "terser": "^5.16.0",
1368
+ "tsx": "^4.8.1",
1369
+ "yaml": "^2.4.2"
1370
+ },
1371
+ "peerDependenciesMeta": {
1372
+ "@types/node": {
1373
+ "optional": true
1374
+ },
1375
+ "jiti": {
1376
+ "optional": true
1377
+ },
1378
+ "less": {
1379
+ "optional": true
1380
+ },
1381
+ "lightningcss": {
1382
+ "optional": true
1383
+ },
1384
+ "sass": {
1385
+ "optional": true
1386
+ },
1387
+ "sass-embedded": {
1388
+ "optional": true
1389
+ },
1390
+ "stylus": {
1391
+ "optional": true
1392
+ },
1393
+ "sugarss": {
1394
+ "optional": true
1395
+ },
1396
+ "terser": {
1397
+ "optional": true
1398
+ },
1399
+ "tsx": {
1400
+ "optional": true
1401
+ },
1402
+ "yaml": {
1403
+ "optional": true
1404
+ }
1405
+ }
1406
+ },
1407
+ "node_modules/vitefu": {
1408
+ "version": "1.1.1",
1409
+ "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz",
1410
+ "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==",
1411
+ "dev": true,
1412
+ "license": "MIT",
1413
+ "workspaces": [
1414
+ "tests/deps/*",
1415
+ "tests/projects/*",
1416
+ "tests/projects/workspace/packages/*"
1417
+ ],
1418
+ "peerDependencies": {
1419
+ "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0"
1420
+ },
1421
+ "peerDependenciesMeta": {
1422
+ "vite": {
1423
+ "optional": true
1424
+ }
1425
+ }
1426
+ },
1427
+ "node_modules/zimmerframe": {
1428
+ "version": "1.1.2",
1429
+ "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
1430
+ "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==",
1431
+ "dev": true,
1432
+ "license": "MIT"
1433
+ }
1434
+ }
1435
+ }
package.json ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "odyssey-frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview",
10
+ "check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
11
+ },
12
+ "devDependencies": {
13
+ "@sveltejs/vite-plugin-svelte": "^5.0.3",
14
+ "@tsconfig/svelte": "^5.0.4",
15
+ "@types/node": "^24.0.14",
16
+ "svelte": "^5.28.1",
17
+ "svelte-check": "^4.1.6",
18
+ "typescript": "~5.8.3",
19
+ "vite": "^6.3.5"
20
+ }
21
+ }
public/vite.svg ADDED
src/App.svelte ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import ApiKeyInput from '$lib/components/ApiKeyInput.svelte';
3
+ import StoryEngine from '$lib/components/StoryEngine.svelte';
4
+ import { apiKey, isGenerating, generationError, resetStory } from '$lib/stores/story';
5
+
6
+ let showEngine = false;
7
+ let errorMessage = '';
8
+
9
+ function handleApiKeySet() {
10
+ showEngine = true;
11
+ errorMessage = '';
12
+ }
13
+
14
+ function handleEngineError(error: string) {
15
+ errorMessage = error;
16
+ }
17
+
18
+ function handleReset() {
19
+ resetStory();
20
+ showEngine = false;
21
+ errorMessage = '';
22
+ }
23
+ </script>
24
+
25
+ <div class="app">
26
+ <header class="app-header">
27
+ <h1>🎬 Odyssey</h1>
28
+ <p class="subtitle">An interactive video-based choose-your-own-adventure</p>
29
+ </header>
30
+
31
+ <main>
32
+ {#if !$apiKey}
33
+ <ApiKeyInput onApiKeySet={handleApiKeySet} />
34
+ {:else if showEngine}
35
+ <div class="engine-container">
36
+ {#if errorMessage}
37
+ <div class="error-banner">
38
+ <p>❌ {errorMessage}</p>
39
+ <button on:click={handleReset} class="retry-button">
40
+ Start Over
41
+ </button>
42
+ </div>
43
+ {/if}
44
+
45
+ {#if $isGenerating}
46
+ <div class="generating-banner">
47
+ <p>🎬 Generating your adventure...</p>
48
+ </div>
49
+ {/if}
50
+
51
+ <StoryEngine onError={handleEngineError} />
52
+
53
+ <div class="controls">
54
+ <button on:click={handleReset} class="reset-button">
55
+ Restart Adventure
56
+ </button>
57
+ </div>
58
+ </div>
59
+ {/if}
60
+ </main>
61
+
62
+ <footer class="app-footer">
63
+ <p>Powered by OpenAI's GPT-4 and Sora</p>
64
+ </footer>
65
+ </div>
66
+
67
+ <style>
68
+ .app {
69
+ min-height: 100vh;
70
+ display: flex;
71
+ flex-direction: column;
72
+ font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
73
+ background: linear-gradient(to bottom, #f8f9fa, #e9ecef);
74
+ }
75
+
76
+ .app-header {
77
+ text-align: center;
78
+ padding: 2rem 1rem 1rem;
79
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
80
+ color: white;
81
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
82
+ }
83
+
84
+ .app-header h1 {
85
+ margin: 0;
86
+ font-size: 2.5rem;
87
+ font-weight: 700;
88
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
89
+ }
90
+
91
+ .subtitle {
92
+ margin: 0.5rem 0 0;
93
+ font-size: 1.1rem;
94
+ opacity: 0.95;
95
+ }
96
+
97
+ main {
98
+ flex: 1;
99
+ max-width: 1400px;
100
+ width: 100%;
101
+ margin: 0 auto;
102
+ padding: 2rem 1rem;
103
+ }
104
+
105
+ .engine-container {
106
+ display: flex;
107
+ flex-direction: column;
108
+ gap: 1rem;
109
+ }
110
+
111
+ .error-banner,
112
+ .generating-banner {
113
+ padding: 1rem 1.5rem;
114
+ border-radius: 0.5rem;
115
+ text-align: center;
116
+ display: flex;
117
+ align-items: center;
118
+ justify-content: center;
119
+ gap: 1rem;
120
+ }
121
+
122
+ .error-banner {
123
+ background: #ffebee;
124
+ color: #c62828;
125
+ border: 1px solid #ffcdd2;
126
+ }
127
+
128
+ .generating-banner {
129
+ background: #e3f2fd;
130
+ color: #1565c0;
131
+ border: 1px solid #bbdefb;
132
+ }
133
+
134
+ .error-banner p,
135
+ .generating-banner p {
136
+ margin: 0;
137
+ font-weight: 600;
138
+ }
139
+
140
+ .retry-button {
141
+ padding: 0.5rem 1rem;
142
+ background: white;
143
+ color: #c62828;
144
+ border: 1px solid #ffcdd2;
145
+ border-radius: 0.375rem;
146
+ font-weight: 600;
147
+ cursor: pointer;
148
+ transition: background 0.2s;
149
+ }
150
+
151
+ .retry-button:hover {
152
+ background: #fff5f5;
153
+ }
154
+
155
+ .controls {
156
+ display: flex;
157
+ justify-content: center;
158
+ padding: 2rem 0;
159
+ }
160
+
161
+ .reset-button {
162
+ padding: 0.75rem 1.5rem;
163
+ background: #6c757d;
164
+ color: white;
165
+ border: none;
166
+ border-radius: 0.5rem;
167
+ font-weight: 600;
168
+ cursor: pointer;
169
+ transition: background 0.2s;
170
+ }
171
+
172
+ .reset-button:hover {
173
+ background: #5a6268;
174
+ }
175
+
176
+ .app-footer {
177
+ text-align: center;
178
+ padding: 2rem 1rem;
179
+ background: white;
180
+ border-top: 1px solid #dee2e6;
181
+ color: #6c757d;
182
+ font-size: 0.9rem;
183
+ }
184
+
185
+ .app-footer p {
186
+ margin: 0;
187
+ }
188
+
189
+ @media (max-width: 768px) {
190
+ .app-header h1 {
191
+ font-size: 2rem;
192
+ }
193
+
194
+ .subtitle {
195
+ font-size: 1rem;
196
+ }
197
+
198
+ main {
199
+ padding: 1rem 0.5rem;
200
+ }
201
+ }
202
+ </style>
src/app.css ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Reset and base styles */
2
+ *, *::before, *::after {
3
+ box-sizing: border-box;
4
+ }
5
+
6
+ * {
7
+ margin: 0;
8
+ }
9
+
10
+ body {
11
+ line-height: 1.5;
12
+ -webkit-font-smoothing: antialiased;
13
+ background: #ffffff;
14
+ color: #1a1a1a;
15
+ }
16
+
17
+ img, picture, video, canvas, svg {
18
+ display: block;
19
+ max-width: 100%;
20
+ }
21
+
22
+ input, button, textarea, select {
23
+ font: inherit;
24
+ }
25
+
26
+ p, h1, h2, h3, h4, h5, h6 {
27
+ overflow-wrap: break-word;
28
+ }
29
+
30
+ /* Utility classes */
31
+ .visually-hidden {
32
+ position: absolute;
33
+ width: 1px;
34
+ height: 1px;
35
+ padding: 0;
36
+ margin: -1px;
37
+ overflow: hidden;
38
+ clip: rect(0, 0, 0, 0);
39
+ white-space: nowrap;
40
+ border: 0;
41
+ }
42
+
43
+ /* Common button styles */
44
+ button {
45
+ padding: 0.75rem 1.5rem;
46
+ border: none;
47
+ border-radius: 0.5rem;
48
+ cursor: pointer;
49
+ font-weight: 500;
50
+ transition: all 0.2s ease;
51
+ background: #007bff;
52
+ color: white;
53
+ }
54
+
55
+ button:hover:not(:disabled) {
56
+ background: #0056b3;
57
+ transform: translateY(-1px);
58
+ }
59
+
60
+ button:disabled {
61
+ background: #9ac7ff;
62
+ cursor: not-allowed;
63
+ transform: none;
64
+ }
65
+
66
+ /* Form styles */
67
+ input[type="text"],
68
+ input[type="file"],
69
+ select {
70
+ padding: 0.75rem;
71
+ border: 1px solid #ccc;
72
+ border-radius: 0.375rem;
73
+ width: 100%;
74
+ transition: border-color 0.2s ease;
75
+ }
76
+
77
+ input[type="text"]:focus,
78
+ select:focus {
79
+ outline: none;
80
+ border-color: #007bff;
81
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
82
+ }
83
+
84
+ label {
85
+ display: block;
86
+ margin-bottom: 0.5rem;
87
+ font-weight: 600;
88
+ color: #333;
89
+ }
90
+
91
+ /* Video styles */
92
+ video {
93
+ width: 100%;
94
+ max-height: 60vh;
95
+ border-radius: 0.5rem;
96
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
97
+ }
98
+
99
+ /* Responsive design */
100
+ @media (max-width: 768px) {
101
+ body {
102
+ font-size: 14px;
103
+ }
104
+
105
+ button {
106
+ padding: 0.5rem 1rem;
107
+ }
108
+
109
+ input[type="text"],
110
+ input[type="file"],
111
+ select {
112
+ padding: 0.5rem;
113
+ }
114
+ }
src/lib/api/openai.ts ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type {
2
+ SoraGenerationParams,
3
+ SoraGenerationResult,
4
+ SoraJob,
5
+ NarrativeGenerationParams,
6
+ NarrativeGenerationResult,
7
+ StoryChoice
8
+ } from '$lib/types';
9
+
10
+ const API_BASE = 'https://api.openai.com/v1';
11
+
12
+ /**
13
+ * Create a Sora video generation job
14
+ */
15
+ export async function createSoraVideo(
16
+ apiKey: string,
17
+ params: SoraGenerationParams
18
+ ): Promise<SoraJob> {
19
+ const formData = new FormData();
20
+ formData.append('model', params.model || 'sora-2');
21
+ formData.append('prompt', params.prompt);
22
+ formData.append('seconds', String(params.seconds || 8));
23
+
24
+ if (params.size) {
25
+ formData.append('size', params.size);
26
+ }
27
+
28
+ if (params.inputReference) {
29
+ formData.append('input_reference', params.inputReference, 'reference.jpg');
30
+ }
31
+
32
+ const response = await fetch(`${API_BASE}/videos`, {
33
+ method: 'POST',
34
+ headers: {
35
+ 'Authorization': `Bearer ${apiKey}`
36
+ },
37
+ body: formData
38
+ });
39
+
40
+ if (!response.ok) {
41
+ const error = await response.json().catch(() => ({ error: { message: response.statusText } }));
42
+ throw new Error(`Failed to create video: ${error.error?.message || response.statusText}`);
43
+ }
44
+
45
+ return await response.json();
46
+ }
47
+
48
+ /**
49
+ * Poll a Sora video job until completion
50
+ */
51
+ export async function pollSoraJob(
52
+ apiKey: string,
53
+ jobId: string,
54
+ onProgress?: (progress: number) => void
55
+ ): Promise<SoraJob> {
56
+ const POLL_INTERVAL = 2000; // 2 seconds
57
+ const MAX_ATTEMPTS = 300; // 10 minutes max
58
+
59
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
60
+ const response = await fetch(`${API_BASE}/videos/${jobId}`, {
61
+ headers: {
62
+ 'Authorization': `Bearer ${apiKey}`
63
+ }
64
+ });
65
+
66
+ if (!response.ok) {
67
+ throw new Error(`Failed to poll job: ${response.statusText}`);
68
+ }
69
+
70
+ const job: SoraJob = await response.json();
71
+
72
+ if (onProgress && job.progress !== undefined) {
73
+ onProgress(job.progress);
74
+ }
75
+
76
+ if (job.status === 'completed') {
77
+ return job;
78
+ }
79
+
80
+ if (job.status === 'failed') {
81
+ throw new Error(job.error?.message || 'Video generation failed');
82
+ }
83
+
84
+ await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL));
85
+ }
86
+
87
+ throw new Error('Video generation timed out');
88
+ }
89
+
90
+ /**
91
+ * Download the generated video content
92
+ */
93
+ export async function downloadSoraVideo(
94
+ apiKey: string,
95
+ jobId: string
96
+ ): Promise<string> {
97
+ const response = await fetch(`${API_BASE}/videos/${jobId}/content?variant=video`, {
98
+ headers: {
99
+ 'Authorization': `Bearer ${apiKey}`
100
+ }
101
+ });
102
+
103
+ if (!response.ok) {
104
+ throw new Error(`Failed to download video: ${response.statusText}`);
105
+ }
106
+
107
+ // Create a blob URL for the video
108
+ const blob = await response.blob();
109
+ return URL.createObjectURL(blob);
110
+ }
111
+
112
+ /**
113
+ * Complete Sora video generation workflow
114
+ */
115
+ export async function generateSoraVideo(
116
+ apiKey: string,
117
+ params: SoraGenerationParams,
118
+ onProgress?: (progress: number) => void
119
+ ): Promise<SoraGenerationResult> {
120
+ // Create the job
121
+ const job = await createSoraVideo(apiKey, params);
122
+
123
+ // Poll until complete
124
+ const completedJob = await pollSoraJob(apiKey, job.id, onProgress);
125
+
126
+ // Download the video
127
+ const videoUrl = await downloadSoraVideo(apiKey, completedJob.id);
128
+
129
+ return {
130
+ videoUrl,
131
+ jobId: job.id
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Generate narrative and choices using GPT-4
137
+ */
138
+ export async function generateNarrative(
139
+ apiKey: string,
140
+ params: NarrativeGenerationParams
141
+ ): Promise<NarrativeGenerationResult> {
142
+ const systemPrompt = `You are a creative storyteller for an interactive video-based choose-your-own-adventure game.
143
+
144
+ Your role:
145
+ 1. Write engaging first-person narrative text that describes what the protagonist sees and experiences
146
+ 2. Create a detailed scene description optimized for Sora video generation (cinematic, specific about visuals, camera movement, lighting)
147
+ 3. Generate 2-4 meaningful choices that continue the story in interesting directions
148
+
149
+ Guidelines:
150
+ - Keep narratives concise but immersive (2-4 sentences)
151
+ - Scene descriptions should be cinematic and specific about visual details
152
+ - Choices should be distinct and lead to different narrative paths
153
+ - Maintain story coherence and continuity
154
+ - Keep content appropriate for general audiences
155
+
156
+ Return a JSON object with this structure:
157
+ {
158
+ "narrative": "First-person narrative text shown to the player",
159
+ "sceneDescription": "Detailed visual description for Sora video generation",
160
+ "choices": [
161
+ {"id": "choice1", "text": "Action the player can take"},
162
+ {"id": "choice2", "text": "Another action the player can take"},
163
+ ...
164
+ ]
165
+ }`;
166
+
167
+ let userPrompt = '';
168
+
169
+ if (params.isFirstScene) {
170
+ userPrompt = `Create the opening scene for a new adventure. The player should start in an intriguing situation with clear choices ahead.`;
171
+ } else {
172
+ userPrompt = `Story context so far:\n${params.storyContext}\n\n`;
173
+ if (params.userChoice) {
174
+ userPrompt += `The player chose: ${params.userChoice}\n\n`;
175
+ }
176
+ userPrompt += `Continue the story from this point. What happens next?`;
177
+ }
178
+
179
+ const response = await fetch(`${API_BASE}/chat/completions`, {
180
+ method: 'POST',
181
+ headers: {
182
+ 'Authorization': `Bearer ${apiKey}`,
183
+ 'Content-Type': 'application/json'
184
+ },
185
+ body: JSON.stringify({
186
+ model: 'gpt-4o',
187
+ messages: [
188
+ { role: 'system', content: systemPrompt },
189
+ { role: 'user', content: userPrompt }
190
+ ],
191
+ response_format: { type: 'json_object' },
192
+ temperature: 0.8
193
+ })
194
+ });
195
+
196
+ if (!response.ok) {
197
+ const error = await response.json().catch(() => ({ error: { message: response.statusText } }));
198
+ throw new Error(`Failed to generate narrative: ${error.error?.message || response.statusText}`);
199
+ }
200
+
201
+ const data = await response.json();
202
+ const content = data.choices[0]?.message?.content;
203
+
204
+ if (!content) {
205
+ throw new Error('No narrative generated');
206
+ }
207
+
208
+ const result = JSON.parse(content);
209
+
210
+ // Validate and ensure IDs for choices
211
+ const choices: StoryChoice[] = (result.choices || []).map((choice: any, index: number) => ({
212
+ id: choice.id || `choice-${Date.now()}-${index}`,
213
+ text: choice.text,
214
+ description: choice.description
215
+ }));
216
+
217
+ return {
218
+ narrative: result.narrative || '',
219
+ sceneDescription: result.sceneDescription || result.narrative,
220
+ choices
221
+ };
222
+ }
223
+
224
+ /**
225
+ * Build a contextual prompt for Sora that includes continuity hints
226
+ */
227
+ export function buildSoraPrompt(sceneDescription: string, storyContext?: string): string {
228
+ if (!storyContext) {
229
+ return sceneDescription;
230
+ }
231
+
232
+ // Add context similarly to sora-extend approach
233
+ return `Context (for continuity):
234
+ ${storyContext}
235
+
236
+ Scene:
237
+ ${sceneDescription}
238
+
239
+ The scene should continue smoothly from the previous moment, maintaining consistent visual style, lighting, and subject identity.`;
240
+ }
src/lib/components/ApiKeyInput.svelte ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { setApiKey } from '$lib/stores/story';
3
+
4
+ export let onApiKeySet: ((apiKey: string) => void) | undefined = undefined;
5
+
6
+ let inputKey = '';
7
+ let isSet = false;
8
+
9
+ function handleSubmit(event: Event) {
10
+ event.preventDefault();
11
+
12
+ if (inputKey.trim()) {
13
+ setApiKey(inputKey.trim());
14
+ isSet = true;
15
+ onApiKeySet?.(inputKey.trim());
16
+ }
17
+ }
18
+
19
+ function handleReset() {
20
+ inputKey = '';
21
+ isSet = false;
22
+ setApiKey('');
23
+ }
24
+ </script>
25
+
26
+ <div class="api-key-input">
27
+ {#if !isSet}
28
+ <div class="input-container">
29
+ <h2>Enter OpenAI API Key</h2>
30
+ <p class="description">
31
+ Your API key is used to generate the story and videos with GPT-4 and Sora.
32
+ It's stored only in your browser and never sent to our servers.
33
+ </p>
34
+
35
+ <form on:submit={handleSubmit}>
36
+ <input
37
+ type="password"
38
+ bind:value={inputKey}
39
+ placeholder="sk-..."
40
+ class="api-key-field"
41
+ required
42
+ />
43
+ <button type="submit" class="submit-button">
44
+ Start Adventure
45
+ </button>
46
+ </form>
47
+
48
+ <p class="info">
49
+ Don't have an API key? Get one at <a href="https://platform.openai.com/api-keys" target="_blank" rel="noopener">OpenAI Platform</a>
50
+ </p>
51
+ </div>
52
+ {:else}
53
+ <div class="key-set">
54
+ <p>✓ API Key Set</p>
55
+ <button on:click={handleReset} class="reset-button">
56
+ Change Key
57
+ </button>
58
+ </div>
59
+ {/if}
60
+ </div>
61
+
62
+ <style>
63
+ .api-key-input {
64
+ display: flex;
65
+ flex-direction: column;
66
+ align-items: center;
67
+ justify-content: center;
68
+ padding: 2rem;
69
+ min-height: 300px;
70
+ }
71
+
72
+ .input-container {
73
+ max-width: 500px;
74
+ width: 100%;
75
+ text-align: center;
76
+ }
77
+
78
+ h2 {
79
+ margin: 0 0 0.5rem 0;
80
+ color: #1a1a1a;
81
+ font-size: 1.75rem;
82
+ }
83
+
84
+ .description {
85
+ color: #666;
86
+ margin: 0 0 2rem 0;
87
+ line-height: 1.5;
88
+ font-size: 0.95rem;
89
+ }
90
+
91
+ form {
92
+ display: flex;
93
+ flex-direction: column;
94
+ gap: 1rem;
95
+ }
96
+
97
+ .api-key-field {
98
+ padding: 0.75rem 1rem;
99
+ font-size: 1rem;
100
+ border: 2px solid #ddd;
101
+ border-radius: 0.5rem;
102
+ font-family: 'Monaco', 'Consolas', monospace;
103
+ }
104
+
105
+ .api-key-field:focus {
106
+ outline: none;
107
+ border-color: #007bff;
108
+ }
109
+
110
+ .submit-button {
111
+ padding: 1rem 2rem;
112
+ font-size: 1.1rem;
113
+ font-weight: 600;
114
+ background: #007bff;
115
+ color: white;
116
+ border: none;
117
+ border-radius: 0.5rem;
118
+ cursor: pointer;
119
+ transition: background 0.2s;
120
+ }
121
+
122
+ .submit-button:hover {
123
+ background: #0056b3;
124
+ }
125
+
126
+ .info {
127
+ margin-top: 1.5rem;
128
+ font-size: 0.85rem;
129
+ color: #666;
130
+ }
131
+
132
+ .info a {
133
+ color: #007bff;
134
+ text-decoration: none;
135
+ }
136
+
137
+ .info a:hover {
138
+ text-decoration: underline;
139
+ }
140
+
141
+ .key-set {
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 1rem;
145
+ padding: 1rem 1.5rem;
146
+ background: #d4edda;
147
+ border: 1px solid #c3e6cb;
148
+ border-radius: 0.5rem;
149
+ color: #155724;
150
+ }
151
+
152
+ .key-set p {
153
+ margin: 0;
154
+ font-weight: 600;
155
+ }
156
+
157
+ .reset-button {
158
+ padding: 0.5rem 1rem;
159
+ font-size: 0.9rem;
160
+ background: white;
161
+ color: #155724;
162
+ border: 1px solid #c3e6cb;
163
+ border-radius: 0.375rem;
164
+ cursor: pointer;
165
+ transition: background 0.2s;
166
+ }
167
+
168
+ .reset-button:hover {
169
+ background: #f1f1f1;
170
+ }
171
+ </style>
src/lib/components/ChoiceInterface.svelte ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { StoryChoice } from '$lib/types';
3
+
4
+ export let choices: StoryChoice[];
5
+ export let onChoiceSelected: ((choice: StoryChoice) => void) | undefined = undefined;
6
+ export let disabled: boolean = false;
7
+
8
+ function handleChoice(choice: StoryChoice) {
9
+ if (!disabled) {
10
+ onChoiceSelected?.(choice);
11
+ }
12
+ }
13
+ </script>
14
+
15
+ {#if choices && choices.length > 0}
16
+ <div class="choice-interface">
17
+ <h3>What do you do?</h3>
18
+ <div class="choices">
19
+ {#each choices as choice}
20
+ <button
21
+ class="choice-button"
22
+ class:disabled={disabled}
23
+ on:click={() => handleChoice(choice)}
24
+ disabled={disabled}
25
+ >
26
+ <span class="choice-text">{choice.text}</span>
27
+ {#if choice.description}
28
+ <span class="choice-description">{choice.description}</span>
29
+ {/if}
30
+ </button>
31
+ {/each}
32
+ </div>
33
+ </div>
34
+ {/if}
35
+
36
+ <style>
37
+ .choice-interface {
38
+ padding: 2rem;
39
+ background: #f8f9fa;
40
+ border-radius: 0.5rem;
41
+ margin: 1.5rem 0;
42
+ }
43
+
44
+ h3 {
45
+ margin: 0 0 1.5rem 0;
46
+ color: #1a1a1a;
47
+ font-size: 1.5rem;
48
+ text-align: center;
49
+ }
50
+
51
+ .choices {
52
+ display: flex;
53
+ flex-direction: column;
54
+ gap: 1rem;
55
+ max-width: 600px;
56
+ margin: 0 auto;
57
+ }
58
+
59
+ .choice-button {
60
+ display: flex;
61
+ flex-direction: column;
62
+ align-items: flex-start;
63
+ padding: 1.25rem 1.5rem;
64
+ background: white;
65
+ border: 2px solid #dee2e6;
66
+ border-radius: 0.5rem;
67
+ cursor: pointer;
68
+ transition: all 0.2s;
69
+ text-align: left;
70
+ }
71
+
72
+ .choice-button:hover:not(.disabled) {
73
+ border-color: #007bff;
74
+ background: #f0f8ff;
75
+ transform: translateX(5px);
76
+ }
77
+
78
+ .choice-button.disabled {
79
+ opacity: 0.5;
80
+ cursor: not-allowed;
81
+ }
82
+
83
+ .choice-text {
84
+ font-size: 1.1rem;
85
+ font-weight: 600;
86
+ color: #1a1a1a;
87
+ margin-bottom: 0.5rem;
88
+ }
89
+
90
+ .choice-description {
91
+ font-size: 0.9rem;
92
+ color: #666;
93
+ line-height: 1.4;
94
+ }
95
+
96
+ @media (max-width: 768px) {
97
+ .choice-interface {
98
+ padding: 1.5rem 1rem;
99
+ }
100
+
101
+ h3 {
102
+ font-size: 1.25rem;
103
+ }
104
+
105
+ .choice-button {
106
+ padding: 1rem 1.25rem;
107
+ }
108
+
109
+ .choice-text {
110
+ font-size: 1rem;
111
+ }
112
+
113
+ .choice-description {
114
+ font-size: 0.85rem;
115
+ }
116
+ }
117
+ </style>
src/lib/components/ContinuousVideoPlayer.svelte ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount, onDestroy } from 'svelte';
3
+ import { extractFinalFrame } from '$lib/utils/video';
4
+
5
+ export let videoUrl: string | undefined;
6
+ export let onVideoEnd: ((finalFrame: Blob) => void) | undefined = undefined;
7
+ export let onError: ((error: string) => void) | undefined = undefined;
8
+
9
+ let videoElement: HTMLVideoElement;
10
+ let isPlaying = false;
11
+ let hasError = false;
12
+ let errorMessage = '';
13
+
14
+ onMount(() => {
15
+ if (videoElement) {
16
+ videoElement.addEventListener('ended', handleVideoEnd);
17
+ videoElement.addEventListener('error', handleError);
18
+ }
19
+ });
20
+
21
+ onDestroy(() => {
22
+ if (videoElement) {
23
+ videoElement.removeEventListener('ended', handleVideoEnd);
24
+ videoElement.removeEventListener('error', handleError);
25
+ }
26
+ });
27
+
28
+ async function handleVideoEnd() {
29
+ if (!videoElement) return;
30
+
31
+ try {
32
+ // Extract the final frame for continuity
33
+ const finalFrame = await extractFinalFrame(videoElement);
34
+ onVideoEnd?.(finalFrame);
35
+ } catch (error) {
36
+ console.error('Failed to extract final frame:', error);
37
+ const errorMsg = error instanceof Error ? error.message : 'Unknown error';
38
+ handleError(new Error(`Failed to extract final frame: ${errorMsg}`));
39
+ }
40
+ }
41
+
42
+ function handleError(error: Event | Error) {
43
+ hasError = true;
44
+
45
+ if (error instanceof Error) {
46
+ errorMessage = error.message;
47
+ } else {
48
+ const videoError = videoElement?.error;
49
+ errorMessage = videoError
50
+ ? `Video error (code ${videoError.code}): ${videoError.message}`
51
+ : 'Failed to load video';
52
+ }
53
+
54
+ onError?.(errorMessage);
55
+ }
56
+
57
+ function handlePlay() {
58
+ isPlaying = true;
59
+ }
60
+
61
+ function handlePause() {
62
+ isPlaying = false;
63
+ }
64
+
65
+ // Reset error when video URL changes
66
+ $: if (videoUrl) {
67
+ hasError = false;
68
+ errorMessage = '';
69
+ }
70
+ </script>
71
+
72
+ <div class="video-player-container">
73
+ {#if videoUrl}
74
+ {#if !hasError}
75
+ <video
76
+ bind:this={videoElement}
77
+ src={videoUrl}
78
+ controls
79
+ autoplay
80
+ class="video-player"
81
+ on:play={handlePlay}
82
+ on:pause={handlePause}
83
+ >
84
+ <track kind="captions" />
85
+ </video>
86
+ {:else}
87
+ <div class="error-container">
88
+ <p class="error-message">❌ {errorMessage}</p>
89
+ </div>
90
+ {/if}
91
+ {:else}
92
+ <div class="placeholder">
93
+ <p>No video loaded</p>
94
+ </div>
95
+ {/if}
96
+ </div>
97
+
98
+ <style>
99
+ .video-player-container {
100
+ width: 100%;
101
+ max-width: 1280px;
102
+ margin: 0 auto;
103
+ background: #000;
104
+ border-radius: 0.5rem;
105
+ overflow: hidden;
106
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
107
+ }
108
+
109
+ .video-player {
110
+ width: 100%;
111
+ height: auto;
112
+ display: block;
113
+ }
114
+
115
+ .placeholder,
116
+ .error-container {
117
+ display: flex;
118
+ align-items: center;
119
+ justify-content: center;
120
+ min-height: 400px;
121
+ background: #1a1a1a;
122
+ color: #999;
123
+ }
124
+
125
+ .placeholder p {
126
+ font-size: 1.1rem;
127
+ margin: 0;
128
+ }
129
+
130
+ .error-message {
131
+ color: #ff6b6b;
132
+ font-size: 1rem;
133
+ margin: 0;
134
+ padding: 2rem;
135
+ text-align: center;
136
+ }
137
+
138
+ @media (max-width: 768px) {
139
+ .placeholder,
140
+ .error-container {
141
+ min-height: 250px;
142
+ }
143
+
144
+ .placeholder p,
145
+ .error-message {
146
+ font-size: 0.9rem;
147
+ }
148
+ }
149
+ </style>
src/lib/components/NarrativeDisplay.svelte ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let narrative: string;
3
+ export let isVisible: boolean = true;
4
+ </script>
5
+
6
+ {#if isVisible && narrative}
7
+ <div class="narrative-display">
8
+ <div class="narrative-content">
9
+ <p>{narrative}</p>
10
+ </div>
11
+ </div>
12
+ {/if}
13
+
14
+ <style>
15
+ .narrative-display {
16
+ position: relative;
17
+ background: linear-gradient(to bottom, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0.6));
18
+ color: white;
19
+ padding: 1.5rem 2rem;
20
+ border-radius: 0.5rem;
21
+ margin: 1rem 0;
22
+ animation: fadeIn 0.5s ease-in;
23
+ }
24
+
25
+ @keyframes fadeIn {
26
+ from {
27
+ opacity: 0;
28
+ transform: translateY(-10px);
29
+ }
30
+ to {
31
+ opacity: 1;
32
+ transform: translateY(0);
33
+ }
34
+ }
35
+
36
+ .narrative-content {
37
+ max-width: 800px;
38
+ margin: 0 auto;
39
+ }
40
+
41
+ .narrative-content p {
42
+ margin: 0;
43
+ font-size: 1.1rem;
44
+ line-height: 1.6;
45
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
46
+ }
47
+
48
+ @media (max-width: 768px) {
49
+ .narrative-display {
50
+ padding: 1rem 1.5rem;
51
+ }
52
+
53
+ .narrative-content p {
54
+ font-size: 1rem;
55
+ }
56
+ }
57
+ </style>
src/lib/components/SoraGenerator.svelte ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { generateSoraVideo } from '$lib/api/openai';
3
+ import type { SoraGenerationParams, SoraGenerationResult } from '$lib/types';
4
+
5
+ export let apiKey: string;
6
+ export let params: SoraGenerationParams;
7
+ export let onVideoGenerated: ((result: SoraGenerationResult) => void) | undefined = undefined;
8
+ export let onProgress: ((progress: number) => void) | undefined = undefined;
9
+ export let onError: ((error: string) => void) | undefined = undefined;
10
+
11
+ let isGenerating = false;
12
+ let progress = 0;
13
+ let error: string | null = null;
14
+ let statusText = '';
15
+
16
+ export async function generate() {
17
+ if (isGenerating) return;
18
+
19
+ isGenerating = true;
20
+ progress = 0;
21
+ error = null;
22
+ statusText = 'Creating video generation job...';
23
+
24
+ try {
25
+ const result = await generateSoraVideo(
26
+ apiKey,
27
+ params,
28
+ (p) => {
29
+ progress = p;
30
+ statusText = `Generating video: ${Math.round(p)}%`;
31
+ onProgress?.(p);
32
+ }
33
+ );
34
+
35
+ statusText = 'Video generated successfully!';
36
+ onVideoGenerated?.(result);
37
+ } catch (err) {
38
+ const errorMsg = err instanceof Error ? err.message : 'Unknown error';
39
+ error = errorMsg;
40
+ statusText = 'Generation failed';
41
+ onError?.(errorMsg);
42
+ } finally {
43
+ isGenerating = false;
44
+ }
45
+ }
46
+ </script>
47
+
48
+ {#if isGenerating || error}
49
+ <div class="generator-status">
50
+ {#if isGenerating}
51
+ <div class="status generating">
52
+ <div class="spinner"></div>
53
+ <p>{statusText}</p>
54
+ {#if progress > 0}
55
+ <div class="progress-bar">
56
+ <div class="progress-fill" style="width: {progress}%"></div>
57
+ </div>
58
+ {/if}
59
+ </div>
60
+ {/if}
61
+
62
+ {#if error}
63
+ <div class="status error">
64
+ <p>❌ {error}</p>
65
+ </div>
66
+ {/if}
67
+ </div>
68
+ {/if}
69
+
70
+ <style>
71
+ .generator-status {
72
+ padding: 1.5rem;
73
+ margin: 1rem 0;
74
+ }
75
+
76
+ .status {
77
+ padding: 1.5rem;
78
+ border-radius: 0.5rem;
79
+ text-align: center;
80
+ }
81
+
82
+ .status.generating {
83
+ background: #e3f2fd;
84
+ border: 1px solid #bbdefb;
85
+ color: #1565c0;
86
+ }
87
+
88
+ .status.error {
89
+ background: #ffebee;
90
+ border: 1px solid #ffcdd2;
91
+ color: #c62828;
92
+ }
93
+
94
+ .status p {
95
+ margin: 0;
96
+ font-weight: 500;
97
+ font-size: 1rem;
98
+ }
99
+
100
+ .spinner {
101
+ width: 40px;
102
+ height: 40px;
103
+ margin: 0 auto 1rem;
104
+ border: 4px solid #bbdefb;
105
+ border-top-color: #1565c0;
106
+ border-radius: 50%;
107
+ animation: spin 1s linear infinite;
108
+ }
109
+
110
+ @keyframes spin {
111
+ to {
112
+ transform: rotate(360deg);
113
+ }
114
+ }
115
+
116
+ .progress-bar {
117
+ width: 100%;
118
+ max-width: 400px;
119
+ height: 8px;
120
+ background: #bbdefb;
121
+ border-radius: 4px;
122
+ margin: 1rem auto 0;
123
+ overflow: hidden;
124
+ }
125
+
126
+ .progress-fill {
127
+ height: 100%;
128
+ background: #1565c0;
129
+ transition: width 0.3s ease;
130
+ }
131
+ </style>
src/lib/components/StoryEngine.svelte ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import ContinuousVideoPlayer from './ContinuousVideoPlayer.svelte';
4
+ import NarrativeDisplay from './NarrativeDisplay.svelte';
5
+ import ChoiceInterface from './ChoiceInterface.svelte';
6
+ import SoraGenerator from './SoraGenerator.svelte';
7
+ import { generateNarrative, buildSoraPrompt } from '$lib/api/openai';
8
+ import {
9
+ apiKey,
10
+ currentScene,
11
+ previousFinalFrame,
12
+ addScene,
13
+ updateCurrentScene,
14
+ buildStoryContextText,
15
+ setGenerating,
16
+ setGenerationProgress,
17
+ setGenerationError,
18
+ isGenerating
19
+ } from '$lib/stores/story';
20
+ import type { StoryChoice, StoryScene, SoraGenerationParams } from '$lib/types';
21
+
22
+ export let onError: ((error: string) => void) | undefined = undefined;
23
+
24
+ let soraGenerator: any;
25
+ let waitingForVideo = false;
26
+ let showChoices = false;
27
+ let currentVideoUrl: string | undefined;
28
+
29
+ onMount(() => {
30
+ // Start the adventure automatically
31
+ startAdventure();
32
+ });
33
+
34
+ async function startAdventure() {
35
+ if (!$apiKey) {
36
+ const error = 'API key not set';
37
+ setGenerationError(error);
38
+ onError?.(error);
39
+ return;
40
+ }
41
+
42
+ setGenerating(true);
43
+ setGenerationError(null);
44
+
45
+ try {
46
+ // Generate the first scene
47
+ const narrative = await generateNarrative($apiKey, {
48
+ storyContext: '',
49
+ isFirstScene: true
50
+ });
51
+
52
+ // Create the first scene
53
+ const scene: StoryScene = {
54
+ id: `scene-${Date.now()}`,
55
+ narrative: narrative.narrative,
56
+ choices: narrative.choices,
57
+ timestamp: Date.now()
58
+ };
59
+
60
+ addScene(scene);
61
+
62
+ // Generate the first video
63
+ await generateVideoForCurrentScene(narrative.sceneDescription);
64
+
65
+ } catch (error) {
66
+ const errorMsg = error instanceof Error ? error.message : 'Failed to start adventure';
67
+ setGenerationError(errorMsg);
68
+ onError?.(errorMsg);
69
+ setGenerating(false);
70
+ }
71
+ }
72
+
73
+ async function generateVideoForCurrentScene(sceneDescription: string) {
74
+ if (!$apiKey || !soraGenerator) return;
75
+
76
+ waitingForVideo = true;
77
+ showChoices = false;
78
+
79
+ try {
80
+ // Build the Sora prompt with context if available
81
+ const storyContext = buildStoryContextText();
82
+ const soraPrompt = buildSoraPrompt(sceneDescription, storyContext);
83
+
84
+ // Create generation parameters
85
+ const params: SoraGenerationParams = {
86
+ prompt: soraPrompt,
87
+ size: '1280x720',
88
+ seconds: 8,
89
+ model: 'sora-2',
90
+ inputReference: $previousFinalFrame || undefined
91
+ };
92
+
93
+ // Trigger video generation
94
+ await soraGenerator.generate();
95
+
96
+ } catch (error) {
97
+ const errorMsg = error instanceof Error ? error.message : 'Video generation failed';
98
+ setGenerationError(errorMsg);
99
+ onError?.(errorMsg);
100
+ setGenerating(false);
101
+ waitingForVideo = false;
102
+ }
103
+ }
104
+
105
+ function handleVideoGenerated(event: CustomEvent<any>) {
106
+ const result = event.detail;
107
+
108
+ // Update current scene with video URL
109
+ updateCurrentScene({ videoUrl: result.videoUrl });
110
+ currentVideoUrl = result.videoUrl;
111
+
112
+ waitingForVideo = false;
113
+ setGenerating(false);
114
+ }
115
+
116
+ function handleVideoEnd(finalFrame: Blob) {
117
+ // Store the final frame for continuity
118
+ updateCurrentScene({ finalFrame });
119
+
120
+ // Show choices after video ends
121
+ showChoices = true;
122
+ }
123
+
124
+ async function handleChoiceSelected(choice: StoryChoice) {
125
+ if (!$apiKey || $isGenerating) return;
126
+
127
+ setGenerating(true);
128
+ setGenerationError(null);
129
+ showChoices = false;
130
+
131
+ try {
132
+ // Generate the next scene based on the choice
133
+ const storyContext = buildStoryContextText();
134
+ const narrative = await generateNarrative($apiKey, {
135
+ storyContext,
136
+ userChoice: choice.text,
137
+ isFirstScene: false
138
+ });
139
+
140
+ // Create the new scene
141
+ const scene: StoryScene = {
142
+ id: `scene-${Date.now()}`,
143
+ narrative: narrative.narrative,
144
+ choices: narrative.choices,
145
+ timestamp: Date.now()
146
+ };
147
+
148
+ addScene(scene);
149
+
150
+ // Generate video for the new scene
151
+ await generateVideoForCurrentScene(narrative.sceneDescription);
152
+
153
+ } catch (error) {
154
+ const errorMsg = error instanceof Error ? error.message : 'Failed to continue story';
155
+ setGenerationError(errorMsg);
156
+ onError?.(errorMsg);
157
+ setGenerating(false);
158
+ }
159
+ }
160
+
161
+ function handleVideoError(error: string) {
162
+ setGenerationError(error);
163
+ onError?.(error);
164
+ }
165
+
166
+ function handleProgress(progress: number) {
167
+ setGenerationProgress(progress);
168
+ }
169
+
170
+ function handleGenerationError(error: string) {
171
+ setGenerationError(error);
172
+ onError?.(error);
173
+ setGenerating(false);
174
+ }
175
+
176
+ // Create Sora params for the generator component
177
+ $: soraParams = $currentScene ? {
178
+ prompt: buildSoraPrompt($currentScene.narrative, buildStoryContextText()),
179
+ size: '1280x720',
180
+ seconds: 8,
181
+ model: 'sora-2',
182
+ inputReference: $previousFinalFrame || undefined
183
+ } as SoraGenerationParams : null;
184
+ </script>
185
+
186
+ <div class="story-engine">
187
+ {#if $currentScene}
188
+ <!-- Video Player -->
189
+ <div class="video-section">
190
+ <ContinuousVideoPlayer
191
+ videoUrl={currentVideoUrl}
192
+ onVideoEnd={handleVideoEnd}
193
+ onError={handleVideoError}
194
+ />
195
+ </div>
196
+
197
+ <!-- Narrative Display -->
198
+ <NarrativeDisplay
199
+ narrative={$currentScene.narrative}
200
+ isVisible={!$isGenerating}
201
+ />
202
+
203
+ <!-- Sora Generator (hidden UI, controlled programmatically) -->
204
+ {#if $apiKey && soraParams}
205
+ <SoraGenerator
206
+ bind:this={soraGenerator}
207
+ apiKey={$apiKey}
208
+ params={soraParams}
209
+ onVideoGenerated={handleVideoGenerated}
210
+ onProgress={handleProgress}
211
+ onError={handleGenerationError}
212
+ />
213
+ {/if}
214
+
215
+ <!-- Choices (shown after video ends) -->
216
+ {#if showChoices && !$isGenerating}
217
+ <ChoiceInterface
218
+ choices={$currentScene.choices}
219
+ onChoiceSelected={handleChoiceSelected}
220
+ disabled={$isGenerating}
221
+ />
222
+ {/if}
223
+ {:else if !$isGenerating}
224
+ <div class="loading">
225
+ <p>Initializing adventure...</p>
226
+ </div>
227
+ {/if}
228
+ </div>
229
+
230
+ <style>
231
+ .story-engine {
232
+ max-width: 1280px;
233
+ margin: 0 auto;
234
+ padding: 1rem;
235
+ }
236
+
237
+ .video-section {
238
+ margin-bottom: 1rem;
239
+ }
240
+
241
+ .loading {
242
+ text-align: center;
243
+ padding: 4rem 2rem;
244
+ color: #666;
245
+ }
246
+
247
+ .loading p {
248
+ font-size: 1.1rem;
249
+ margin: 0;
250
+ }
251
+
252
+ @media (max-width: 768px) {
253
+ .story-engine {
254
+ padding: 0.5rem;
255
+ }
256
+ }
257
+ </style>
src/lib/stores/story.ts ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { writable, derived, get } from 'svelte/store';
2
+ import type { StoryScene, StoryContext, StoryChoice } from '$lib/types';
3
+
4
+ // OpenAI API key
5
+ export const apiKey = writable<string | null>(null);
6
+
7
+ // Story context (all scenes and current position)
8
+ export const storyContext = writable<StoryContext>({
9
+ scenes: [],
10
+ currentSceneIndex: -1
11
+ });
12
+
13
+ // Current scene (derived from context)
14
+ export const currentScene = derived(
15
+ storyContext,
16
+ ($storyContext) => {
17
+ if ($storyContext.currentSceneIndex >= 0 && $storyContext.currentSceneIndex < $storyContext.scenes.length) {
18
+ return $storyContext.scenes[$storyContext.currentSceneIndex];
19
+ }
20
+ return null;
21
+ }
22
+ );
23
+
24
+ // Is there a previous scene with a final frame for continuity?
25
+ export const previousFinalFrame = derived(
26
+ storyContext,
27
+ ($storyContext) => {
28
+ const prevIndex = $storyContext.currentSceneIndex - 1;
29
+ if (prevIndex >= 0 && prevIndex < $storyContext.scenes.length) {
30
+ return $storyContext.scenes[prevIndex].finalFrame;
31
+ }
32
+ return null;
33
+ }
34
+ );
35
+
36
+ // Generation state
37
+ export const isGenerating = writable<boolean>(false);
38
+ export const generationProgress = writable<number>(0);
39
+ export const generationError = writable<string | null>(null);
40
+
41
+ /**
42
+ * Set the API key
43
+ */
44
+ export function setApiKey(key: string) {
45
+ apiKey.set(key);
46
+ }
47
+
48
+ /**
49
+ * Add a new scene to the story
50
+ */
51
+ export function addScene(scene: StoryScene) {
52
+ storyContext.update(ctx => {
53
+ const newScenes = [...ctx.scenes, scene];
54
+ return {
55
+ scenes: newScenes,
56
+ currentSceneIndex: newScenes.length - 1
57
+ };
58
+ });
59
+ }
60
+
61
+ /**
62
+ * Update the current scene
63
+ */
64
+ export function updateCurrentScene(updates: Partial<StoryScene>) {
65
+ storyContext.update(ctx => {
66
+ const scenes = [...ctx.scenes];
67
+ if (ctx.currentSceneIndex >= 0 && ctx.currentSceneIndex < scenes.length) {
68
+ scenes[ctx.currentSceneIndex] = {
69
+ ...scenes[ctx.currentSceneIndex],
70
+ ...updates
71
+ };
72
+ }
73
+ return {
74
+ ...ctx,
75
+ scenes
76
+ };
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Build a text summary of the story so far for context
82
+ */
83
+ export function buildStoryContextText(): string {
84
+ const ctx = get(storyContext);
85
+ if (ctx.scenes.length === 0) {
86
+ return '';
87
+ }
88
+
89
+ const summaries = ctx.scenes.map((scene, index) => {
90
+ return `Scene ${index + 1}: ${scene.narrative}`;
91
+ });
92
+
93
+ return summaries.join('\n\n');
94
+ }
95
+
96
+ /**
97
+ * Get the last choice made (for context)
98
+ */
99
+ export function getLastChoice(): string | null {
100
+ const ctx = get(storyContext);
101
+ if (ctx.currentSceneIndex > 0) {
102
+ const prevScene = ctx.scenes[ctx.currentSceneIndex - 1];
103
+ // In a real implementation, we'd track which choice was selected
104
+ // For now, we return null and the narrative will contain the context
105
+ return null;
106
+ }
107
+ return null;
108
+ }
109
+
110
+ /**
111
+ * Reset the story
112
+ */
113
+ export function resetStory() {
114
+ storyContext.set({
115
+ scenes: [],
116
+ currentSceneIndex: -1
117
+ });
118
+ isGenerating.set(false);
119
+ generationProgress.set(0);
120
+ generationError.set(null);
121
+ }
122
+
123
+ /**
124
+ * Set generation state
125
+ */
126
+ export function setGenerating(generating: boolean) {
127
+ isGenerating.set(generating);
128
+ if (!generating) {
129
+ generationProgress.set(0);
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Set generation progress
135
+ */
136
+ export function setGenerationProgress(progress: number) {
137
+ generationProgress.set(progress);
138
+ }
139
+
140
+ /**
141
+ * Set generation error
142
+ */
143
+ export function setGenerationError(error: string | null) {
144
+ generationError.set(error);
145
+ }
src/lib/types/index.ts ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // OpenAI API types
2
+ export interface OpenAIConfig {
3
+ apiKey: string;
4
+ }
5
+
6
+ // Story types
7
+ export interface StoryScene {
8
+ id: string;
9
+ narrative: string; // First-person narrative text
10
+ videoUrl?: string; // Generated video URL
11
+ finalFrame?: Blob; // Final frame for continuity
12
+ choices: StoryChoice[];
13
+ timestamp: number;
14
+ }
15
+
16
+ export interface StoryChoice {
17
+ id: string;
18
+ text: string; // The choice text shown to user
19
+ description?: string; // Optional additional context
20
+ }
21
+
22
+ export interface StoryContext {
23
+ scenes: StoryScene[];
24
+ currentSceneIndex: number;
25
+ }
26
+
27
+ // Sora API types
28
+ export interface SoraGenerationParams {
29
+ prompt: string;
30
+ size?: string; // e.g. "1280x720"
31
+ seconds?: number; // 4, 8, or 12
32
+ model?: string; // "sora-2" or "sora-2-pro"
33
+ inputReference?: Blob; // For continuity - final frame from previous video
34
+ }
35
+
36
+ export interface SoraJob {
37
+ id: string;
38
+ status: 'queued' | 'in_progress' | 'completed' | 'failed';
39
+ progress?: number;
40
+ error?: {
41
+ message: string;
42
+ };
43
+ }
44
+
45
+ export interface SoraGenerationResult {
46
+ videoUrl: string;
47
+ jobId: string;
48
+ }
49
+
50
+ // GPT-4 narrative generation types
51
+ export interface NarrativeGenerationParams {
52
+ storyContext: string; // Summary of what happened so far
53
+ userChoice?: string; // The choice the user just made
54
+ isFirstScene: boolean;
55
+ }
56
+
57
+ export interface NarrativeGenerationResult {
58
+ narrative: string; // First-person narrative text
59
+ sceneDescription: string; // Description for Sora video generation
60
+ choices: StoryChoice[];
61
+ }
62
+
63
+ // Upload types
64
+ export interface UploadResult {
65
+ uuid: string;
66
+ url: string;
67
+ share: string;
68
+ }
69
+
70
+ // Component props
71
+ export interface ApiKeyInputProps {
72
+ onApiKeySet?: (apiKey: string) => void;
73
+ }
74
+
75
+ export interface NarrativeDisplayProps {
76
+ narrative: string;
77
+ isVisible: boolean;
78
+ }
79
+
80
+ export interface ChoiceInterfaceProps {
81
+ choices: StoryChoice[];
82
+ onChoiceSelected?: (choice: StoryChoice) => void;
83
+ disabled?: boolean;
84
+ }
85
+
86
+ export interface ContinuousVideoPlayerProps {
87
+ videoUrl?: string;
88
+ onVideoEnd?: (finalFrame: Blob) => void;
89
+ onError?: (error: string) => void;
90
+ }
91
+
92
+ export interface SoraGeneratorProps {
93
+ apiKey: string;
94
+ params: SoraGenerationParams;
95
+ onVideoGenerated?: (result: SoraGenerationResult) => void;
96
+ onProgress?: (progress: number) => void;
97
+ onError?: (error: string) => void;
98
+ }
src/lib/utils/video.ts ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Extract the final frame from a video element as a Blob
3
+ * This is used to create continuity between video segments
4
+ * Similar to the approach in sora-extend, but using browser Canvas API
5
+ */
6
+ export async function extractFinalFrame(
7
+ videoElement: HTMLVideoElement
8
+ ): Promise<Blob> {
9
+ return new Promise((resolve, reject) => {
10
+ try {
11
+ // Create a canvas with the video's dimensions
12
+ const canvas = document.createElement('canvas');
13
+ canvas.width = videoElement.videoWidth;
14
+ canvas.height = videoElement.videoHeight;
15
+
16
+ const ctx = canvas.getContext('2d');
17
+ if (!ctx) {
18
+ reject(new Error('Could not get canvas context'));
19
+ return;
20
+ }
21
+
22
+ // Draw the current frame onto the canvas
23
+ ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
24
+
25
+ // Convert canvas to blob
26
+ canvas.toBlob(
27
+ (blob) => {
28
+ if (blob) {
29
+ resolve(blob);
30
+ } else {
31
+ reject(new Error('Failed to create blob from canvas'));
32
+ }
33
+ },
34
+ 'image/jpeg',
35
+ 0.95 // High quality for better continuity
36
+ );
37
+ } catch (error) {
38
+ reject(error);
39
+ }
40
+ });
41
+ }
42
+
43
+ /**
44
+ * Seek to the last frame of a video and extract it
45
+ * Useful for extracting the final frame before the video ends naturally
46
+ */
47
+ export async function seekAndExtractFinalFrame(
48
+ videoElement: HTMLVideoElement
49
+ ): Promise<Blob> {
50
+ return new Promise((resolve, reject) => {
51
+ const handleSeeked = async () => {
52
+ try {
53
+ const blob = await extractFinalFrame(videoElement);
54
+ videoElement.removeEventListener('seeked', handleSeeked);
55
+ resolve(blob);
56
+ } catch (error) {
57
+ videoElement.removeEventListener('seeked', handleSeeked);
58
+ reject(error);
59
+ }
60
+ };
61
+
62
+ videoElement.addEventListener('seeked', handleSeeked);
63
+
64
+ // Seek to 50ms before the end to ensure we get a valid frame
65
+ const targetTime = Math.max(0, videoElement.duration - 0.05);
66
+ videoElement.currentTime = targetTime;
67
+ });
68
+ }
69
+
70
+ /**
71
+ * Create a preview URL for a blob (useful for debugging)
72
+ */
73
+ export function createBlobPreviewUrl(blob: Blob): string {
74
+ return URL.createObjectURL(blob);
75
+ }
76
+
77
+ /**
78
+ * Revoke a blob URL to free memory
79
+ */
80
+ export function revokeBlobUrl(url: string): void {
81
+ URL.revokeObjectURL(url);
82
+ }
83
+
84
+ /**
85
+ * Download a blob as a file (useful for debugging/testing)
86
+ */
87
+ export function downloadBlob(blob: Blob, filename: string): void {
88
+ const url = createBlobPreviewUrl(blob);
89
+ const a = document.createElement('a');
90
+ a.href = url;
91
+ a.download = filename;
92
+ document.body.appendChild(a);
93
+ a.click();
94
+ document.body.removeChild(a);
95
+ revokeBlobUrl(url);
96
+ }
src/main.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import { mount } from 'svelte'
2
+ import './app.css'
3
+ import App from './App.svelte'
4
+
5
+ const app = mount(App, {
6
+ target: document.getElementById('app')!,
7
+ })
8
+
9
+ export default app
src/vite-env.d.ts ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /// <reference types="vite/client" />
2
+
3
+ declare global {
4
+ interface Window {
5
+ hfAuth: {
6
+ oauthLoginUrl: Function;
7
+ oauthHandleRedirectIfPresent: Function;
8
+ };
9
+ gradioClient: {
10
+ Client: any;
11
+ };
12
+ }
13
+ }
14
+
15
+ export {}
svelte.config.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
2
+
3
+ export default {
4
+ // Consult https://svelte.dev/docs#compile-time-svelte-preprocess
5
+ // for more information about preprocessors
6
+ preprocess: vitePreprocess()
7
+ }
tsconfig.app.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "extends": "@tsconfig/svelte/tsconfig.json",
3
+ "compilerOptions": {
4
+ "target": "ES2020",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "resolveJsonModule": true,
14
+ "isolatedModules": true,
15
+ "noEmit": true,
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "noFallthroughCasesInSwitch": true,
22
+
23
+ "paths": {
24
+ "$lib": ["./src/lib"],
25
+ "$lib/*": ["./src/lib/*"]
26
+ }
27
+ },
28
+ "include": ["src/**/*.ts", "src/**/*.svelte"]
29
+ }
tsconfig.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "extends": "@tsconfig/svelte/tsconfig.json",
3
+ "compilerOptions": {
4
+ "target": "ES2020",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "resolveJsonModule": true,
14
+ "isolatedModules": true,
15
+ "noEmit": true,
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "noFallthroughCasesInSwitch": true,
22
+
23
+ "paths": {
24
+ "$lib": ["./src/lib"],
25
+ "$lib/*": ["./src/lib/*"]
26
+ }
27
+ },
28
+ "files": [],
29
+ "include": [],
30
+ "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
31
+ }
tsconfig.node.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "skipLibCheck": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "allowSyntheticDefaultImports": true,
8
+ "strict": true,
9
+ "noEmit": true
10
+ },
11
+ "include": ["vite.config.ts"]
12
+ }
vite.config.ts ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import { svelte } from '@sveltejs/vite-plugin-svelte'
3
+ import { fileURLToPath, URL } from 'url'
4
+
5
+ // https://vite.dev/config/
6
+ export default defineConfig({
7
+ plugins: [svelte()],
8
+ resolve: {
9
+ alias: {
10
+ '$lib': fileURLToPath(new URL('./src/lib', import.meta.url))
11
+ }
12
+ },
13
+ server: {
14
+ proxy: {
15
+ // Proxy API requests to FastAPI backend
16
+ '/upload': 'http://localhost:8000',
17
+ '/video': 'http://localhost:8000',
18
+ '/spaces': 'http://localhost:8000'
19
+ }
20
+ },
21
+ build: {
22
+ outDir: 'dist' // Build to dist directory for Docker multi-stage build
23
+ }
24
+ })