varg.ai-sdk 0.1.0 → 0.4.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/.claude/settings.local.json +1 -1
  2. package/.env.example +3 -0
  3. package/.github/workflows/ci.yml +23 -0
  4. package/.husky/README.md +102 -0
  5. package/.husky/commit-msg +6 -0
  6. package/.husky/pre-commit +9 -0
  7. package/.husky/pre-push +6 -0
  8. package/.size-limit.json +8 -0
  9. package/.test-hooks.ts +5 -0
  10. package/CLAUDE.md +10 -3
  11. package/CONTRIBUTING.md +150 -0
  12. package/LICENSE.md +53 -0
  13. package/README.md +56 -209
  14. package/SKILLS.md +26 -10
  15. package/biome.json +7 -1
  16. package/bun.lock +1286 -0
  17. package/commitlint.config.js +22 -0
  18. package/docs/index.html +1130 -0
  19. package/docs/prompting.md +326 -0
  20. package/docs/react.md +834 -0
  21. package/docs/sdk.md +812 -0
  22. package/ffmpeg/CLAUDE.md +68 -0
  23. package/package.json +48 -8
  24. package/pipeline/cookbooks/scripts/animate-frames-parallel.ts +84 -0
  25. package/pipeline/cookbooks/scripts/combine-scenes.sh +53 -0
  26. package/pipeline/cookbooks/scripts/generate-frames-parallel.ts +99 -0
  27. package/pipeline/cookbooks/scripts/still-to-video.sh +37 -0
  28. package/pipeline/cookbooks/text-to-tiktok.md +669 -0
  29. package/pipeline/cookbooks/trendwatching.md +156 -0
  30. package/plan.md +281 -0
  31. package/scripts/.gitkeep +0 -0
  32. package/src/ai-sdk/cache.ts +142 -0
  33. package/src/ai-sdk/examples/cached-generation.ts +53 -0
  34. package/src/ai-sdk/examples/duet-scene-4.ts +53 -0
  35. package/src/ai-sdk/examples/duet-scene-5-audio.ts +32 -0
  36. package/src/ai-sdk/examples/duet-video.ts +56 -0
  37. package/src/ai-sdk/examples/editly-composition.ts +63 -0
  38. package/src/ai-sdk/examples/editly-test.ts +57 -0
  39. package/src/ai-sdk/examples/editly-video-test.ts +52 -0
  40. package/src/ai-sdk/examples/fal-lipsync.ts +43 -0
  41. package/src/ai-sdk/examples/higgsfield-image.ts +61 -0
  42. package/src/ai-sdk/examples/music-generation.ts +19 -0
  43. package/src/ai-sdk/examples/openai-sora.ts +34 -0
  44. package/src/ai-sdk/examples/replicate-bg-removal.ts +52 -0
  45. package/src/ai-sdk/examples/simpsons-scene.ts +61 -0
  46. package/src/ai-sdk/examples/talking-lion.ts +55 -0
  47. package/src/ai-sdk/examples/video-generation.ts +39 -0
  48. package/src/ai-sdk/examples/workflow-animated-girl.ts +104 -0
  49. package/src/ai-sdk/examples/workflow-before-after.ts +114 -0
  50. package/src/ai-sdk/examples/workflow-character-grid.ts +112 -0
  51. package/src/ai-sdk/examples/workflow-slideshow.ts +161 -0
  52. package/src/ai-sdk/file-cache.ts +112 -0
  53. package/src/ai-sdk/file.ts +238 -0
  54. package/src/ai-sdk/generate-element.ts +92 -0
  55. package/src/ai-sdk/generate-music.ts +46 -0
  56. package/src/ai-sdk/generate-video.ts +165 -0
  57. package/src/ai-sdk/index.ts +72 -0
  58. package/src/ai-sdk/music-model.ts +110 -0
  59. package/src/ai-sdk/providers/editly/editly.test.ts +1108 -0
  60. package/src/ai-sdk/providers/editly/ffmpeg.ts +60 -0
  61. package/src/ai-sdk/providers/editly/index.ts +817 -0
  62. package/src/ai-sdk/providers/editly/layers.ts +776 -0
  63. package/src/ai-sdk/providers/editly/plan.md +144 -0
  64. package/src/ai-sdk/providers/editly/types.ts +328 -0
  65. package/src/ai-sdk/providers/elevenlabs-provider.ts +255 -0
  66. package/src/ai-sdk/providers/fal-provider.ts +512 -0
  67. package/src/ai-sdk/providers/higgsfield.ts +379 -0
  68. package/src/ai-sdk/providers/openai.ts +251 -0
  69. package/src/ai-sdk/providers/replicate.ts +16 -0
  70. package/src/ai-sdk/video-model.ts +185 -0
  71. package/src/cli/commands/find.tsx +137 -0
  72. package/src/cli/commands/help.tsx +85 -0
  73. package/src/cli/commands/index.ts +6 -0
  74. package/src/cli/commands/list.tsx +238 -0
  75. package/src/cli/commands/render.tsx +71 -0
  76. package/src/cli/commands/run.tsx +511 -0
  77. package/src/cli/commands/which.tsx +253 -0
  78. package/src/cli/index.ts +114 -0
  79. package/src/cli/quiet.ts +44 -0
  80. package/src/cli/types.ts +32 -0
  81. package/src/cli/ui/components/Badge.tsx +29 -0
  82. package/src/cli/ui/components/DataTable.tsx +51 -0
  83. package/src/cli/ui/components/Header.tsx +23 -0
  84. package/src/cli/ui/components/HelpBlock.tsx +44 -0
  85. package/src/cli/ui/components/KeyValue.tsx +33 -0
  86. package/src/cli/ui/components/OptionRow.tsx +81 -0
  87. package/src/cli/ui/components/Separator.tsx +23 -0
  88. package/src/cli/ui/components/StatusBox.tsx +108 -0
  89. package/src/cli/ui/components/VargBox.tsx +51 -0
  90. package/src/cli/ui/components/VargProgress.tsx +36 -0
  91. package/src/cli/ui/components/VargSpinner.tsx +34 -0
  92. package/src/cli/ui/components/VargText.tsx +56 -0
  93. package/src/cli/ui/components/index.ts +19 -0
  94. package/src/cli/ui/index.ts +12 -0
  95. package/src/cli/ui/render.ts +35 -0
  96. package/src/cli/ui/theme.ts +63 -0
  97. package/src/cli/utils.ts +78 -0
  98. package/src/core/executor/executor.ts +201 -0
  99. package/src/core/executor/index.ts +13 -0
  100. package/src/core/executor/job.ts +214 -0
  101. package/src/core/executor/pipeline.ts +222 -0
  102. package/src/core/index.ts +11 -0
  103. package/src/core/registry/index.ts +9 -0
  104. package/src/core/registry/loader.ts +149 -0
  105. package/src/core/registry/registry.ts +221 -0
  106. package/src/core/registry/resolver.ts +206 -0
  107. package/src/core/schema/helpers.ts +134 -0
  108. package/src/core/schema/index.ts +8 -0
  109. package/src/core/schema/shared.ts +102 -0
  110. package/src/core/schema/types.ts +279 -0
  111. package/src/core/schema/validator.ts +92 -0
  112. package/src/definitions/actions/captions.ts +261 -0
  113. package/src/definitions/actions/edit.ts +298 -0
  114. package/src/definitions/actions/image.ts +125 -0
  115. package/src/definitions/actions/index.ts +114 -0
  116. package/src/definitions/actions/music.ts +205 -0
  117. package/src/definitions/actions/sync.ts +128 -0
  118. package/{action/transcribe/index.ts → src/definitions/actions/transcribe.ts} +63 -90
  119. package/src/definitions/actions/upload.ts +111 -0
  120. package/src/definitions/actions/video.ts +163 -0
  121. package/src/definitions/actions/voice.ts +119 -0
  122. package/src/definitions/index.ts +23 -0
  123. package/src/definitions/models/elevenlabs.ts +50 -0
  124. package/src/definitions/models/flux.ts +56 -0
  125. package/src/definitions/models/index.ts +36 -0
  126. package/src/definitions/models/kling.ts +56 -0
  127. package/src/definitions/models/llama.ts +54 -0
  128. package/src/definitions/models/nano-banana-pro.ts +102 -0
  129. package/src/definitions/models/sonauto.ts +68 -0
  130. package/src/definitions/models/soul.ts +65 -0
  131. package/src/definitions/models/wan.ts +54 -0
  132. package/src/definitions/models/whisper.ts +44 -0
  133. package/src/definitions/skills/index.ts +12 -0
  134. package/src/definitions/skills/talking-character.ts +87 -0
  135. package/src/definitions/skills/text-to-tiktok.ts +97 -0
  136. package/src/index.ts +118 -0
  137. package/src/providers/apify.ts +269 -0
  138. package/src/providers/base.ts +264 -0
  139. package/src/providers/elevenlabs.ts +217 -0
  140. package/src/providers/fal.ts +392 -0
  141. package/src/providers/ffmpeg.ts +544 -0
  142. package/src/providers/fireworks.ts +193 -0
  143. package/src/providers/groq.ts +149 -0
  144. package/src/providers/higgsfield.ts +145 -0
  145. package/src/providers/index.ts +143 -0
  146. package/src/providers/replicate.ts +147 -0
  147. package/src/providers/storage.ts +206 -0
  148. package/src/react/cli.ts +52 -0
  149. package/src/react/elements.ts +146 -0
  150. package/src/react/examples/branching.tsx +66 -0
  151. package/src/react/examples/captions-demo.tsx +37 -0
  152. package/src/react/examples/character-video.tsx +84 -0
  153. package/src/react/examples/grid.tsx +53 -0
  154. package/src/react/examples/layouts-demo.tsx +57 -0
  155. package/src/react/examples/madi.tsx +60 -0
  156. package/src/react/examples/music-test.tsx +35 -0
  157. package/src/react/examples/onlyfans-1m/workflow.tsx +88 -0
  158. package/src/react/examples/orange-portrait.tsx +41 -0
  159. package/src/react/examples/split-element-demo.tsx +60 -0
  160. package/src/react/examples/split-layout-demo.tsx +60 -0
  161. package/src/react/examples/split.tsx +41 -0
  162. package/src/react/examples/video-grid.tsx +46 -0
  163. package/src/react/index.ts +43 -0
  164. package/src/react/layouts/grid.tsx +28 -0
  165. package/src/react/layouts/index.ts +2 -0
  166. package/src/react/layouts/split.tsx +20 -0
  167. package/src/react/react.test.ts +309 -0
  168. package/src/react/render.ts +21 -0
  169. package/src/react/renderers/animate.ts +59 -0
  170. package/src/react/renderers/captions.ts +297 -0
  171. package/src/react/renderers/clip.ts +248 -0
  172. package/src/react/renderers/context.ts +17 -0
  173. package/src/react/renderers/image.ts +109 -0
  174. package/src/react/renderers/index.ts +22 -0
  175. package/src/react/renderers/music.ts +60 -0
  176. package/src/react/renderers/packshot.ts +84 -0
  177. package/src/react/renderers/progress.ts +173 -0
  178. package/src/react/renderers/render.ts +243 -0
  179. package/src/react/renderers/slider.ts +69 -0
  180. package/src/react/renderers/speech.ts +53 -0
  181. package/src/react/renderers/split.ts +91 -0
  182. package/src/react/renderers/subtitle.ts +16 -0
  183. package/src/react/renderers/swipe.ts +75 -0
  184. package/src/react/renderers/title.ts +17 -0
  185. package/src/react/renderers/utils.ts +124 -0
  186. package/src/react/renderers/video.ts +127 -0
  187. package/src/react/runtime/jsx-dev-runtime.ts +43 -0
  188. package/src/react/runtime/jsx-runtime.ts +35 -0
  189. package/src/react/types.ts +232 -0
  190. package/src/studio/index.ts +26 -0
  191. package/src/studio/scanner.ts +102 -0
  192. package/src/studio/server.ts +554 -0
  193. package/src/studio/stages.ts +251 -0
  194. package/src/studio/step-renderer.ts +279 -0
  195. package/src/studio/types.ts +60 -0
  196. package/src/studio/ui/cache.html +303 -0
  197. package/src/studio/ui/index.html +1820 -0
  198. package/src/tests/all.test.ts +509 -0
  199. package/src/tests/index.ts +33 -0
  200. package/src/tests/unit.test.ts +403 -0
  201. package/tsconfig.cli.json +8 -0
  202. package/tsconfig.json +21 -3
  203. package/TEST_RESULTS.md +0 -122
  204. package/action/captions/SKILL.md +0 -170
  205. package/action/captions/index.ts +0 -227
  206. package/action/edit/SKILL.md +0 -235
  207. package/action/edit/index.ts +0 -493
  208. package/action/image/SKILL.md +0 -140
  209. package/action/image/index.ts +0 -112
  210. package/action/sync/SKILL.md +0 -136
  211. package/action/sync/index.ts +0 -187
  212. package/action/transcribe/SKILL.md +0 -179
  213. package/action/video/SKILL.md +0 -116
  214. package/action/video/index.ts +0 -135
  215. package/action/voice/SKILL.md +0 -125
  216. package/action/voice/index.ts +0 -201
  217. package/index.ts +0 -38
  218. package/lib/README.md +0 -144
  219. package/lib/ai-sdk/fal.ts +0 -106
  220. package/lib/ai-sdk/replicate.ts +0 -107
  221. package/lib/elevenlabs.ts +0 -382
  222. package/lib/fal.ts +0 -478
  223. package/lib/ffmpeg.ts +0 -467
  224. package/lib/fireworks.ts +0 -235
  225. package/lib/groq.ts +0 -246
  226. package/lib/higgsfield.ts +0 -176
  227. package/lib/remotion/SKILL.md +0 -823
  228. package/lib/remotion/cli.ts +0 -115
  229. package/lib/remotion/functions.ts +0 -283
  230. package/lib/remotion/index.ts +0 -19
  231. package/lib/remotion/templates.ts +0 -73
  232. package/lib/replicate.ts +0 -304
  233. package/output.txt +0 -1
  234. package/test-import.ts +0 -7
  235. package/test-services.ts +0 -97
  236. package/utilities/s3.ts +0 -147
@@ -0,0 +1,303 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>cache viewer - varg studio</title>
7
+ <style>
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+ body {
10
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
11
+ background: #0a0a0a;
12
+ color: #fafafa;
13
+ min-height: 100vh;
14
+ }
15
+ header {
16
+ height: 48px;
17
+ padding: 0 1rem;
18
+ border-bottom: 1px solid #222;
19
+ display: flex;
20
+ align-items: center;
21
+ justify-content: space-between;
22
+ gap: 1rem;
23
+ }
24
+ .logo { font-weight: 600; font-size: 0.875rem; }
25
+ .nav { display: flex; gap: 0.25rem; }
26
+ .nav-btn {
27
+ padding: 0.5rem 0.75rem;
28
+ border-radius: 0.375rem;
29
+ border: none;
30
+ background: transparent;
31
+ color: #888;
32
+ font-size: 0.8rem;
33
+ cursor: pointer;
34
+ text-decoration: none;
35
+ transition: all 0.15s;
36
+ }
37
+ .nav-btn:hover { color: #fff; background: #141414; }
38
+ .nav-btn.active { color: #fff; background: #141414; }
39
+ .header-right { display: flex; align-items: center; gap: 1rem; }
40
+ .stats { color: #666; font-size: 0.8rem; }
41
+ .refresh-btn {
42
+ padding: 0.375rem 0.75rem;
43
+ border-radius: 0.375rem;
44
+ border: 1px solid #333;
45
+ background: transparent;
46
+ color: #888;
47
+ cursor: pointer;
48
+ font-size: 0.8rem;
49
+ }
50
+ .refresh-btn:hover { border-color: #555; color: #fff; }
51
+ .filters {
52
+ padding: 1rem 2rem;
53
+ display: flex;
54
+ gap: 0.5rem;
55
+ }
56
+ .filter-btn {
57
+ padding: 0.5rem 1rem;
58
+ border-radius: 0.5rem;
59
+ border: 1px solid #333;
60
+ background: transparent;
61
+ color: #888;
62
+ cursor: pointer;
63
+ font-size: 0.8rem;
64
+ transition: all 0.2s;
65
+ }
66
+ .filter-btn:hover { border-color: #555; color: #fff; }
67
+ .filter-btn.active { background: #fff; color: #000; border-color: #fff; }
68
+ .grid {
69
+ display: grid;
70
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
71
+ gap: 1rem;
72
+ padding: 1rem 2rem 2rem;
73
+ }
74
+ .card {
75
+ background: #141414;
76
+ border-radius: 0.75rem;
77
+ overflow: hidden;
78
+ border: 1px solid #222;
79
+ transition: border-color 0.2s;
80
+ }
81
+ .card:hover { border-color: #444; }
82
+ .card-media {
83
+ aspect-ratio: 16/9;
84
+ background: #000;
85
+ display: flex;
86
+ align-items: center;
87
+ justify-content: center;
88
+ overflow: hidden;
89
+ cursor: pointer;
90
+ }
91
+ .card-media img, .card-media video {
92
+ width: 100%;
93
+ height: 100%;
94
+ object-fit: contain;
95
+ }
96
+ .card-info { padding: 0.875rem; }
97
+ .card-type {
98
+ display: inline-block;
99
+ padding: 0.25rem 0.5rem;
100
+ border-radius: 0.25rem;
101
+ font-size: 0.7rem;
102
+ font-weight: 500;
103
+ text-transform: uppercase;
104
+ margin-bottom: 0.5rem;
105
+ }
106
+ .card-type.image { background: #1a3a1a; color: #4ade80; }
107
+ .card-type.video { background: #1a1a3a; color: #818cf8; }
108
+ .card-title {
109
+ font-size: 0.75rem;
110
+ color: #999;
111
+ word-break: break-all;
112
+ line-height: 1.4;
113
+ }
114
+ .card-meta {
115
+ display: flex;
116
+ gap: 1rem;
117
+ margin-top: 0.5rem;
118
+ font-size: 0.7rem;
119
+ color: #555;
120
+ }
121
+ .empty {
122
+ text-align: center;
123
+ padding: 4rem;
124
+ color: #666;
125
+ }
126
+ .modal {
127
+ display: none;
128
+ position: fixed;
129
+ inset: 0;
130
+ background: rgba(0,0,0,0.9);
131
+ z-index: 1000;
132
+ align-items: center;
133
+ justify-content: center;
134
+ padding: 2rem;
135
+ }
136
+ .modal.open { display: flex; }
137
+ .modal-content {
138
+ max-width: 90vw;
139
+ max-height: 90vh;
140
+ position: relative;
141
+ }
142
+ .modal-content img, .modal-content video {
143
+ max-width: 90vw;
144
+ max-height: 90vh;
145
+ object-fit: contain;
146
+ border-radius: 0.5rem;
147
+ }
148
+ .modal-close {
149
+ position: absolute;
150
+ top: -2.5rem;
151
+ right: 0;
152
+ background: none;
153
+ border: none;
154
+ color: #888;
155
+ font-size: 1.5rem;
156
+ cursor: pointer;
157
+ }
158
+ .modal-close:hover { color: #fff; }
159
+ .mute-toggle {
160
+ position: absolute;
161
+ bottom: 1rem;
162
+ right: 1rem;
163
+ padding: 0.5rem 0.75rem;
164
+ border-radius: 0.375rem;
165
+ border: 1px solid #444;
166
+ background: rgba(0,0,0,0.7);
167
+ color: #fff;
168
+ font-size: 0.75rem;
169
+ cursor: pointer;
170
+ }
171
+ .mute-toggle:hover { background: rgba(0,0,0,0.9); }
172
+ </style>
173
+ </head>
174
+ <body>
175
+ <header>
176
+ <div class="logo">varg studio</div>
177
+ <nav class="nav">
178
+ <a href="/editor" class="nav-btn">editor</a>
179
+ <a href="/cache" class="nav-btn active">cache</a>
180
+ </nav>
181
+ <div class="header-right">
182
+ <span class="stats" id="stats"></span>
183
+ <button class="refresh-btn" onclick="refresh()">refresh</button>
184
+ </div>
185
+ </header>
186
+
187
+ <div class="filters">
188
+ <button class="filter-btn active" data-filter="all">all</button>
189
+ <button class="filter-btn" data-filter="image">images</button>
190
+ <button class="filter-btn" data-filter="video">videos</button>
191
+ </div>
192
+
193
+ <div id="grid" class="grid"></div>
194
+
195
+ <div id="modal" class="modal" onclick="closeModal(event)">
196
+ <div class="modal-content">
197
+ <button class="modal-close" onclick="closeModal()">&times;</button>
198
+ <div id="modal-media"></div>
199
+ </div>
200
+ </div>
201
+
202
+ <script>
203
+ let items = [];
204
+ let filter = 'all';
205
+
206
+ async function fetchItems() {
207
+ const res = await fetch('/api/items');
208
+ items = await res.json();
209
+ updateStats();
210
+ renderGrid();
211
+ }
212
+
213
+ function updateStats() {
214
+ const images = items.filter(i => i.type === 'image').length;
215
+ const videos = items.filter(i => i.type === 'video').length;
216
+ document.getElementById('stats').textContent =
217
+ `${items.length} items (${images} images, ${videos} videos)`;
218
+ }
219
+
220
+ function formatSize(bytes) {
221
+ if (bytes < 1024) return `${bytes} B`;
222
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
223
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
224
+ }
225
+
226
+ function formatDate(dateStr) {
227
+ const date = new Date(dateStr);
228
+ return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
229
+ }
230
+
231
+ function renderGrid() {
232
+ const grid = document.getElementById('grid');
233
+ const filtered = filter === 'all' ? items : items.filter(i => i.type === filter);
234
+
235
+ if (filtered.length === 0) {
236
+ grid.innerHTML = '<div class="empty">no items found</div>';
237
+ return;
238
+ }
239
+
240
+ grid.innerHTML = filtered.map(item => {
241
+ const mediaUrl = `/api/media/${encodeURIComponent(item.id)}`;
242
+ const mediaHtml = item.type === 'video'
243
+ ? `<video src="${mediaUrl}" muted loop preload="metadata" onmouseenter="this.play()" onmouseleave="this.pause();this.currentTime=0"></video>`
244
+ : `<img src="${mediaUrl}" alt="${item.id}" loading="lazy">`;
245
+
246
+ return '<div class="card">' +
247
+ '<div class="card-media" onclick="openModal(\'' + item.type + '\', \'' + mediaUrl + '\')">' + mediaHtml + '</div>' +
248
+ '<div class="card-info">' +
249
+ '<span class="card-type ' + item.type + '">' + item.type + '</span>' +
250
+ '<div class="card-title">' + item.id + '</div>' +
251
+ '<div class="card-meta">' +
252
+ '<span>' + formatSize(item.size) + '</span>' +
253
+ '<span>' + formatDate(item.createdAt) + '</span>' +
254
+ '</div>' +
255
+ '</div>' +
256
+ '</div>';
257
+ }).join('');
258
+ }
259
+
260
+ function _refresh() { fetchItems(); }
261
+
262
+ document.querySelectorAll('.filter-btn').forEach(btn => {
263
+ btn.addEventListener('click', () => {
264
+ document.querySelectorAll('.filter-btn').forEach(b => { b.classList.remove('active'); });
265
+ btn.classList.add('active');
266
+ filter = btn.dataset.filter;
267
+ renderGrid();
268
+ });
269
+ });
270
+
271
+ function _openModal(type, url) {
272
+ const modal = document.getElementById('modal');
273
+ const mediaContainer = document.getElementById('modal-media');
274
+ if (type === 'video') {
275
+ mediaContainer.innerHTML = '<video id="modal-video" src="' + url + '" controls autoplay loop muted></video>' +
276
+ '<button class="mute-toggle" onclick="toggleMute()"><span id="mute-icon">muted</span></button>';
277
+ } else {
278
+ mediaContainer.innerHTML = `<img src="${url}">`;
279
+ }
280
+ modal.classList.add('open');
281
+ }
282
+
283
+ function _toggleMute() {
284
+ const video = document.getElementById('modal-video');
285
+ if (!video) return;
286
+ video.muted = !video.muted;
287
+ document.getElementById('mute-icon').textContent = video.muted ? 'muted' : 'unmuted';
288
+ }
289
+
290
+ function closeModal(e) {
291
+ if (e && e.target !== e.currentTarget) return;
292
+ document.getElementById('modal').classList.remove('open');
293
+ document.getElementById('modal-media').innerHTML = '';
294
+ }
295
+
296
+ document.addEventListener('keydown', (e) => {
297
+ if (e.key === 'Escape') closeModal();
298
+ });
299
+
300
+ fetchItems();
301
+ </script>
302
+ </body>
303
+ </html>