@mevdragon/vidfarm-devcli 0.18.1 → 0.19.0

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 (42) hide show
  1. package/.agents/skills/editor-capabilities/SKILL.md +166 -0
  2. package/.agents/skills/editor-capabilities/references/re-theme-walkthrough.md +58 -0
  3. package/.agents/skills/vidfarm-media/SKILL.md +2 -0
  4. package/SKILL.director.md +174 -16
  5. package/SKILL.platform.md +7 -3
  6. package/demo/dist/app.css +1 -1
  7. package/demo/dist/app.js +77 -75
  8. package/dist/src/app.js +1513 -145
  9. package/dist/src/cli.js +1365 -105
  10. package/dist/src/config.js +7 -0
  11. package/dist/src/devcli/clips.js +3 -0
  12. package/dist/src/devcli/composition-edit.js +396 -8
  13. package/dist/src/devcli/local-backend.js +123 -0
  14. package/dist/src/devcli/migrate-local.js +140 -0
  15. package/dist/src/devcli/sync.js +311 -0
  16. package/dist/src/devcli/timeline-edit.js +208 -1
  17. package/dist/src/editor-chat.js +33 -17
  18. package/dist/src/frontend/file-directory.js +271 -20
  19. package/dist/src/frontend/homepage-view.js +3 -3
  20. package/dist/src/frontend/template-editor-chat.js +6 -3
  21. package/dist/src/page-shell.js +1 -1
  22. package/dist/src/primitive-registry.js +409 -4
  23. package/dist/src/reskin/chat-page.js +103 -15
  24. package/dist/src/reskin/discover-page.js +247 -10
  25. package/dist/src/reskin/document.js +147 -10
  26. package/dist/src/reskin/inpaint-clipper-page.js +649 -0
  27. package/dist/src/reskin/inpaint-page.js +2459 -452
  28. package/dist/src/reskin/inpaint-video-page.js +1339 -0
  29. package/dist/src/reskin/library-page.js +324 -82
  30. package/dist/src/reskin/theme.js +36 -11
  31. package/dist/src/services/billing.js +4 -0
  32. package/dist/src/services/clip-curation/hunt.js +2 -0
  33. package/dist/src/services/clip-records.js +28 -0
  34. package/dist/src/services/file-directory.js +6 -3
  35. package/dist/src/services/hyperframes.js +283 -3
  36. package/dist/src/services/serverless-records.js +43 -0
  37. package/dist/src/services/storage.js +24 -2
  38. package/package.json +1 -1
  39. package/public/assets/file-directory-app.js +2 -2
  40. package/public/assets/homepage-client-app.js +1 -1
  41. package/public/assets/page-runtime-client-app.js +2 -2
  42. package/public/assets/placeholders/scene-placeholder.png +0 -0
@@ -0,0 +1,1339 @@
1
+ // /tools/video — the "Create Video" twin of the masked image editor. It reuses
2
+ // the same three-panel workbench design as /tools/image, but the center stage
3
+ // is a scrubbable VIDEO with a timeline (MM:SS:mmm timecode + copy), the masks are
4
+ // TIME-SCOPED (each carries a region AND a moment), and generation compiles the
5
+ // whole-video prompt + every timed mask + optional start/end frames into ONE rich
6
+ // instruction for POST /videos/generate (there is no localized video-inpaint API,
7
+ // so the masks/timecodes become precise editing guidance for the AI video model).
8
+ //
9
+ // Data flow:
10
+ // • regenerate → POST /api/v1/primitives/videos/generate (frame_images + prompt)
11
+ // → poll /primitives/video_generate/jobs → durable MP4
12
+ // • text→video → same route from the empty-state composer
13
+ // • Save to Files → POST /api/v1/user/me/attachments/from-url (folder pick)
14
+ //
15
+ // The source video is only ever a browser-side reference to paint/scrub on and to
16
+ // grab start/end frames from — it is never uploaded; only the captured frames are.
17
+ import { reskinDocument } from "./document.js";
18
+ import { renderInpaintModeTabs, INPAINT_TABS_CSS } from "./inpaint-page.js";
19
+ const UPLOAD_ICON = `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 16V4"/><path d="m7 9 5-5 5 5"/><path d="M4 16v2a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-2"/></svg>`;
20
+ const UNDO_ICON = `<svg width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M7.2 7H3.8V3.6"/><path d="M4 7a7 7 0 1 1 1.9 6.6"/></svg>`;
21
+ const REDO_ICON = `<svg width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12.8 7h3.4V3.6"/><path d="M16 7a7 7 0 1 0-1.9 6.6"/></svg>`;
22
+ const CLEAR_ICON = `<svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5l10 10M15 5 5 15"/></svg>`;
23
+ const GEAR_ICON = `<svg width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M8.8 3.4 9.4 2h1.2l.6 1.4 1.2.5 1.4-.6.9.9-.6 1.4.5 1.2 1.4.6v1.2l-1.4.6-.5 1.2.6 1.4-.9.9-1.4-.6-1.2.5-.6 1.4H9.4l-.6-1.4-1.2-.5-1.4.6-.9-.9.6-1.4-.5-1.2L4 8.6V7.4l1.4-.6.5-1.2-.6-1.4.9-.9 1.4.6 1.2-.5Z"/><circle cx="10" cy="8" r="2.4"/></svg>`;
24
+ const REFRESH_ICON = `<svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M16.5 10a6.5 6.5 0 1 1-1.9-4.6"/><path d="M16.5 4.2v4.2h-4.2"/></svg>`;
25
+ const SPARK_ICON = `<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2.5l1.9 5.1 5.1 1.9-5.1 1.9L12 16.5l-1.9-5.1L5 9.5l5.1-1.9z"/></svg>`;
26
+ const DL_ICON = `<svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M10 3v9"/><path d="m6.5 8.5 3.5 3.5 3.5-3.5"/><path d="M4 16h12"/></svg>`;
27
+ const CLIP_ICON = `<svg width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="m8.1 10.7 4.8-4.8a2.5 2.5 0 0 1 3.5 3.5l-6.1 6.1a4 4 0 0 1-5.7-5.7l6.3-6.3"/><path d="m10 8.8-4.1 4.1a1.6 1.6 0 0 0 2.3 2.3l5.6-5.6"/></svg>`;
28
+ const SEND_ICON = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5"/><path d="m5 12 7-7 7 7"/></svg>`;
29
+ const COPY_ICON = `<svg width="13" height="13" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8.4 11.6a3.2 3.2 0 0 0 4.5 0l2.2-2.2a3.2 3.2 0 0 0-4.5-4.5L9.4 6.1"/><path d="M11.6 8.4a3.2 3.2 0 0 0-4.5 0L4.9 10.6a3.2 3.2 0 0 0 4.5 4.5l1.2-1.2"/></svg>`;
30
+ const CLOSE_ICON = `<svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5l10 10"/><path d="M15 5 5 15"/></svg>`;
31
+ const HISTORY_ICON = `<svg width="16" height="16" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3.2 10a6.8 6.8 0 1 1 2 4.8"/><path d="M3.2 14.4v-3.6h3.6"/><path d="M10 6.2V10l2.6 1.6"/></svg>`;
32
+ const PLUS_ICON = `<svg width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 4v12"/><path d="M4 10h12"/></svg>`;
33
+ const PLAY_ICON = `<svg width="17" height="17" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 5.5v13a1 1 0 0 0 1.5.87l11-6.5a1 1 0 0 0 0-1.74l-11-6.5A1 1 0 0 0 7 5.5Z"/></svg>`;
34
+ const PAUSE_ICON = `<svg width="17" height="17" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/></svg>`;
35
+ const CLOCK_ICON = `<svg width="14" height="14" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><circle cx="10" cy="10" r="7"/><path d="M10 6v4l2.6 1.6"/></svg>`;
36
+ export function renderReskinInpaintVideo(input = {}) {
37
+ const boot = { accountId: input.accountId || "", isLoggedIn: Boolean(input.isLoggedIn) };
38
+ const frameSlot = (which, label) => `
39
+ <div class="rk-ipv-frame" data-ipv-frame="${which}">
40
+ <button type="button" class="rk-ipv-frame-thumb" data-ipv-frame-open="${which}" aria-label="Preview ${label} frame">
41
+ <span class="rk-ipv-frame-ph" data-ipv-frame-ph="${which}" aria-hidden="true">${which === "first" ? "◧" : "◨"}</span>
42
+ <img class="rk-ipv-frame-img" data-ipv-frame-img="${which}" alt="" hidden />
43
+ </button>
44
+ <span class="rk-ipv-frame-name">${label}</span>
45
+ <div class="rk-ipv-frame-actions">
46
+ <button type="button" class="rk-ipv-frame-btn" data-ipv-frame-upload="${which}" title="Upload an image for the ${label.toLowerCase()} frame">Replace</button>
47
+ <button type="button" class="rk-ipv-frame-btn" data-ipv-frame-capture="${which}" title="Grab the current playhead frame">Use current</button>
48
+ </div>
49
+ </div>`;
50
+ const body = `
51
+ <main class="rk-container-wide rk-page rk-inpaint-page rk-ipv-page">
52
+ <div class="rk-page-head rk-inpaint-head">
53
+ <div class="rk-inpaint-head-text">
54
+ ${renderInpaintModeTabs("video")}
55
+ <h1 class="rk-h1">Create Video</h1>
56
+ <p class="rk-sub">Load a video (or set a start &amp; end frame), scrub the timeline, and paint <strong>time-scoped</strong> mask regions — each mask carries its own moment (MM:SS:mmm) and instruction. Generate and the masks + timecodes + frames become one precise editing prompt for your AI video model.</p>
57
+ </div>
58
+ <div class="rk-inpaint-toolbar">
59
+ <button type="button" class="rk-btn rk-btn-ghost rk-btn-sm rk-inpaint-startnew" data-ipv-start-new>${PLUS_ICON} Start New</button>
60
+ <button type="button" class="rk-ip-iconbtn" data-ipv-settings-button aria-label="Settings" title="Settings">${GEAR_ICON}</button>
61
+ <button type="button" class="rk-ip-iconbtn" data-ipv-history-toggle aria-label="Past sessions" title="Past sessions">${HISTORY_ICON}</button>
62
+ </div>
63
+ </div>
64
+
65
+ <div class="rk-inpaint-workbench rk-ipv-workbench">
66
+ <!-- LEFT: frames + prompt + brush + timed masks -->
67
+ <section class="rk-inpaint-panel" aria-label="Controls">
68
+ <div class="rk-inpaint-block">
69
+ <label class="rk-label">Start &amp; end frames <span class="rk-ip-setting-note">(for full regenerate)</span></label>
70
+ <div class="rk-ipv-frames">
71
+ ${frameSlot("first", "Start")}
72
+ ${frameSlot("last", "End")}
73
+ </div>
74
+ <input data-ipv-frame-input type="file" accept="image/png,image/jpeg,image/webp,image/gif,image/avif" hidden />
75
+ <span class="rk-hint">Optional. Provide a start and/or end frame to steer the regenerated clip. <strong>Use current</strong> grabs the frame at the playhead.</span>
76
+ </div>
77
+
78
+ <div class="rk-inpaint-block">
79
+ <label class="rk-label">Video prompt</label>
80
+ <div class="rk-ip-promptfield">
81
+ <textarea class="rk-textarea rk-ip-global" data-ipv-global-prompt placeholder="Describe the overall motion, style, and outcome for the whole clip."></textarea>
82
+ <button type="button" class="rk-ip-attach" data-ipv-attach aria-label="Attach reference image" title="Attach reference image">${CLIP_ICON}</button>
83
+ </div>
84
+ <span class="rk-ip-attach-chips" data-ipv-attachments></span>
85
+ <input data-ipv-attachment-input type="file" accept="image/jpeg,image/png,image/webp" multiple hidden />
86
+ <span class="rk-hint">Guidance for the whole video and for consistency across the timed edits below.</span>
87
+ </div>
88
+
89
+ <div class="rk-inpaint-block">
90
+ <div class="rk-inpaint-brushrow">
91
+ <label class="rk-label">Brush <span data-ipv-brush-label>70</span>&nbsp;px</label>
92
+ <div class="rk-ip-toolbtns">
93
+ <button type="button" class="rk-ip-iconbtn" data-ipv-undo aria-label="Undo stroke" title="Undo">${UNDO_ICON}</button>
94
+ <button type="button" class="rk-ip-iconbtn" data-ipv-redo aria-label="Redo stroke" title="Redo">${REDO_ICON}</button>
95
+ <button type="button" class="rk-ip-iconbtn" data-ipv-clear aria-label="Clear active mask" title="Clear active mask">${CLEAR_ICON}</button>
96
+ </div>
97
+ </div>
98
+ <input data-ipv-brush class="rk-inpaint-range" type="range" min="6" max="180" step="1" value="70" />
99
+ </div>
100
+
101
+ <div class="rk-inpaint-block">
102
+ <div class="rk-inpaint-brushrow">
103
+ <label class="rk-label">Timed masks</label>
104
+ <input class="rk-ip-colorpicker" data-ipv-color-picker type="color" value="#ff2d55" aria-label="Next mask color" />
105
+ </div>
106
+ <span class="rk-hint">Each mask marks a region <strong>and</strong> a moment. Paint on the video, set the timecode + instruction, then add another mask for a different spot or time. Click a mask to jump the video to its moment.</span>
107
+ <div class="rk-ipv-masks" data-ipv-masks></div>
108
+ <button type="button" class="rk-btn rk-btn-ghost rk-btn-sm rk-ip-addcolor" data-ipv-add-mask>+ Add timed mask</button>
109
+ </div>
110
+
111
+ <div class="rk-inpaint-block">
112
+ <button type="button" class="rk-btn rk-btn-gold rk-inpaint-generate" data-ipv-submit>${SPARK_ICON} <span data-ipv-submit-label>Generate video</span></button>
113
+ <p class="rk-inpaint-status" data-ipv-status data-tone="" aria-live="polite"></p>
114
+ </div>
115
+ </section>
116
+
117
+ <!-- CENTER: scrubbable video stage -->
118
+ <section class="rk-inpaint-stage rk-ipv-stage" aria-label="Video canvas">
119
+ <div class="rk-inpaint-empty" data-ipv-empty>
120
+ <span class="rk-inpaint-empty-ic" aria-hidden="true">${UPLOAD_ICON}</span>
121
+ <p class="rk-inpaint-empty-title">Load a video to edit</p>
122
+ <p class="rk-inpaint-empty-sub">Drop a video here, or set a start &amp; end frame and a prompt to generate one.</p>
123
+ <button type="button" class="rk-btn rk-btn-ink rk-btn-sm rk-ipv-upload" data-ipv-upload>${UPLOAD_ICON} Upload video</button>
124
+ <input data-ipv-file type="file" accept="video/mp4,video/webm,video/quicktime,video/*" hidden />
125
+
126
+ <div class="rk-ip-gen" id="rk-ipv-gen">
127
+ <span class="rk-ip-gen-or">or</span>
128
+ <div class="rk-ip-gen-box">
129
+ <textarea id="rk-ipv-genprompt" class="rk-ip-gen-input" rows="2" placeholder="Or describe the video you want"></textarea>
130
+ <div class="rk-ip-gen-refs" id="rk-ipv-genrefs" hidden></div>
131
+ <div class="rk-ip-gen-bar">
132
+ <button type="button" class="rk-ip-gen-clip" id="rk-ipv-genclip" aria-label="Attach reference images from your files" title="Attach reference images from your files">${CLIP_ICON}</button>
133
+ <span class="rk-ip-gen-hint">Generate a brand-new clip from a prompt (+ optional frames).</span>
134
+ <button type="button" class="rk-ip-gen-send" id="rk-ipv-gensend" aria-label="Generate video">${SEND_ICON}</button>
135
+ </div>
136
+ </div>
137
+ <p class="rk-ip-gen-status" id="rk-ipv-genstatus" data-tone="" aria-live="polite"></p>
138
+ </div>
139
+ </div>
140
+
141
+ <div class="rk-ipv-player" data-ipv-player hidden>
142
+ <div class="rk-ipv-canvaswrap" data-ipv-canvaswrap>
143
+ <video data-ipv-video class="rk-ipv-video" playsinline></video>
144
+ <canvas data-ipv-canvas class="rk-ipv-canvas"></canvas>
145
+ <div class="editor-inpaint-brush-cursor" data-ipv-brush-cursor aria-hidden="true"></div>
146
+ </div>
147
+ <div class="rk-ipv-controls">
148
+ <button type="button" class="rk-ipv-playpause" data-ipv-playpause aria-label="Play/pause">${PLAY_ICON}</button>
149
+ <input type="range" class="rk-ipv-scrub" data-ipv-scrub min="0" max="1000" step="1" value="0" aria-label="Seek" />
150
+ <div class="rk-ipv-timecode">
151
+ <input class="rk-ipv-timecode-input" data-ipv-timecode-input type="text" inputmode="numeric" spellcheck="false" value="00:00:000" aria-label="Current time MM:SS:mmm" />
152
+ <button type="button" class="rk-ip-iconbtn rk-ipv-timecode-copy" data-ipv-timecode-copy aria-label="Copy current timecode" title="Copy timecode">${COPY_ICON}</button>
153
+ <span class="rk-ipv-duration" data-ipv-duration>/ 00:00:000</span>
154
+ </div>
155
+ </div>
156
+ </div>
157
+ </section>
158
+
159
+ <!-- RIGHT: result + tracer-grouped history -->
160
+ <section class="rk-inpaint-output" aria-label="Result and history">
161
+ <div class="rk-inpaint-result" data-ipv-output hidden>
162
+ <div class="rk-inpaint-result-head">
163
+ <h2 class="rk-inpaint-h2">Result</h2>
164
+ <button type="button" class="rk-btn rk-btn-ghost rk-btn-sm" data-ipv-save>Save to Files</button>
165
+ </div>
166
+ <video class="rk-ipv-result-video" data-ipv-result controls playsinline></video>
167
+ </div>
168
+
169
+ <div class="rk-inpaint-history">
170
+ <div class="rk-inpaint-history-head">
171
+ <h2 class="rk-inpaint-h2">This session</h2>
172
+ <div class="rk-ip-histtools">
173
+ <button type="button" class="rk-ip-iconbtn" data-ipv-history-refresh aria-label="Refresh history" title="Refresh history">${REFRESH_ICON}</button>
174
+ </div>
175
+ </div>
176
+ <div class="rk-inpaint-historylist" data-ipv-history-list>
177
+ <div class="custom-inpaint-history-empty">Loading history…</div>
178
+ </div>
179
+ </div>
180
+ </section>
181
+ </div>
182
+
183
+ <!-- settings modal (provider + duration + aspect + audio + tracer) -->
184
+ <div class="rk-ip-settingsmodal" data-ipv-settings hidden>
185
+ <div class="rk-ip-settingsmodal-panel" role="dialog" aria-modal="true" aria-label="Video generation settings">
186
+ <div class="rk-ip-settingsmodal-head">
187
+ <span class="rk-ip-settingsmodal-title">Settings</span>
188
+ <button type="button" class="rk-ip-iconbtn" data-ipv-settings-close aria-label="Close settings" title="Close">${CLOSE_ICON}</button>
189
+ </div>
190
+ <div class="rk-ip-settingsmodal-body">
191
+ <label class="rk-ip-setting rk-ip-provider" data-ipv-provider-wrap>
192
+ <span class="rk-label">AI model</span>
193
+ <select data-ipv-provider class="rk-select rk-inpaint-select"></select>
194
+ </label>
195
+ <label class="rk-ip-setting">
196
+ <span class="rk-label">Duration</span>
197
+ <select data-ipv-duration class="rk-select rk-inpaint-select">
198
+ <option value="">Auto</option>
199
+ <option value="4">4 seconds</option>
200
+ <option value="6">6 seconds</option>
201
+ <option value="8">8 seconds</option>
202
+ <option value="10">10 seconds</option>
203
+ </select>
204
+ </label>
205
+ <label class="rk-ip-setting rk-ipv-audio-row">
206
+ <span class="rk-label">Generate audio</span>
207
+ <input type="checkbox" data-ipv-audio class="rk-ipv-audio" />
208
+ </label>
209
+ <label class="rk-ip-setting">
210
+ <span class="rk-label">Session name <span class="rk-ip-setting-note">(tracer — groups this session's History)</span></span>
211
+ <input data-ipv-tracer class="rk-ip-input" type="text" autocomplete="off" spellcheck="false" placeholder="Auto-named from your first prompt" />
212
+ </label>
213
+ </div>
214
+ </div>
215
+ </div>
216
+
217
+ <!-- past-sessions left slide-in panel -->
218
+ <aside class="rk-ip-sessions" data-ipv-sessions aria-label="Past sessions">
219
+ <div class="rk-ip-sessions-head">
220
+ <span class="rk-ip-sessions-title">Past sessions</span>
221
+ <button type="button" class="rk-ip-iconbtn" data-ipv-sessions-close aria-label="Close past sessions" title="Close">${CLOSE_ICON}</button>
222
+ </div>
223
+ <div class="rk-ip-sessions-list" data-ipv-sessions-list>
224
+ <div class="rk-ip-sessions-empty">Loading…</div>
225
+ </div>
226
+ </aside>
227
+ <div class="rk-ip-sessions-backdrop" data-ipv-sessions-backdrop hidden></div>
228
+
229
+ <!-- video preview lightbox (result + history) -->
230
+ <div class="rk-ip-lightbox rk-ipv-lightbox" data-ipv-lightbox data-open="false" hidden>
231
+ <div class="rk-ipv-lightbox-dialog" role="dialog" aria-modal="true" aria-label="Video output">
232
+ <div class="custom-inpaint-lightbox-head">
233
+ <p class="custom-inpaint-lightbox-title" data-ipv-lightbox-title>Video output</p>
234
+ <div class="custom-inpaint-lightbox-actions">
235
+ <button type="button" class="rk-ip-iconbtn" data-ipv-lightbox-copy aria-label="Copy video URL" title="Copy URL">${COPY_ICON}</button>
236
+ <a class="rk-btn rk-btn-ink rk-btn-sm" data-ipv-lightbox-download href="#" target="_blank" rel="noopener noreferrer" download>${DL_ICON} Download</a>
237
+ <button type="button" class="rk-ip-iconbtn" data-ipv-lightbox-close aria-label="Close preview" title="Close">${CLOSE_ICON}</button>
238
+ </div>
239
+ </div>
240
+ <div class="rk-ipv-lightbox-stage">
241
+ <video data-ipv-lightbox-video controls playsinline></video>
242
+ </div>
243
+ </div>
244
+ </div>
245
+
246
+ <script type="application/json" data-rk-inpaint-video-boot>${JSON.stringify(boot).replace(/</g, "\\u003c")}</script>
247
+ </main>`;
248
+ const pageCss = `
249
+ ${INPAINT_TABS_CSS}
250
+ .rk-inpaint-page{padding-left:var(--rk-gutter);padding-right:var(--rk-gutter)}
251
+ .rk-inpaint-workbench{display:grid;grid-template-columns:320px minmax(0,1fr) 320px;gap:20px;align-items:start}
252
+ @media(max-width:1200px){.rk-inpaint-workbench{grid-template-columns:320px minmax(0,1fr)}.rk-inpaint-output{grid-column:1 / -1;grid-row:2}}
253
+ @media(max-width:820px){.rk-inpaint-workbench{grid-template-columns:1fr}.rk-inpaint-output,.rk-inpaint-stage,.rk-inpaint-panel{grid-column:auto}.rk-inpaint-stage{order:-1}}
254
+ .rk-inpaint-panel,.rk-inpaint-stage,.rk-inpaint-output{background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);box-shadow:var(--rk-shadow-card)}
255
+ .rk-inpaint-panel{padding:18px;display:grid;gap:16px;align-content:start}
256
+ .rk-inpaint-block{display:grid;gap:9px}
257
+ .rk-inpaint-h2{font-family:var(--rk-font-display);font-size:var(--rk-text-lg);font-weight:800;color:var(--rk-ink);letter-spacing:-.01em}
258
+ .rk-inpaint-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;flex-wrap:wrap}
259
+ .rk-inpaint-head-text{min-width:0;flex:1 1 320px}
260
+ .rk-inpaint-toolbar{flex:none;display:flex;align-items:center;gap:8px;padding-top:2px}
261
+ .rk-inpaint-startnew{gap:6px}
262
+ .rk-ip-iconbtn{display:grid;place-items:center;width:34px;height:34px;border-radius:50%;border:1px solid var(--rk-border);background:#fff;color:var(--rk-n-700);cursor:pointer;transition:background var(--rk-dur) var(--rk-ease),color var(--rk-dur) var(--rk-ease),border-color var(--rk-dur) var(--rk-ease)}
263
+ .rk-ip-iconbtn:hover{background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
264
+ .rk-ip-toolbtns{display:flex;align-items:center;gap:6px}
265
+
266
+ /* start/end frames */
267
+ .rk-ipv-frames{display:grid;grid-template-columns:1fr 1fr;gap:10px}
268
+ .rk-ipv-frame{display:grid;gap:6px;justify-items:center}
269
+ .rk-ipv-frame-thumb{position:relative;width:100%;aspect-ratio:16/10;padding:0;border:1.5px dashed var(--rk-border-strong);border-radius:var(--rk-r-xl);background:var(--rk-n-50);color:var(--rk-text-faint);cursor:pointer;overflow:hidden;display:grid;place-items:center}
270
+ .rk-ipv-frame-thumb:hover{border-color:var(--rk-gold-600)}
271
+ .rk-ipv-frame-ph{font-size:24px;line-height:1}
272
+ .rk-ipv-frame-img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
273
+ .rk-ipv-frame-name{font-size:12px;font-weight:700;color:var(--rk-ink)}
274
+ .rk-ipv-frame-actions{display:flex;gap:6px}
275
+ .rk-ipv-frame-btn{padding:3px 8px;font:inherit;font-size:11px;font-weight:700;color:var(--rk-n-700);background:#fff;border:1px solid var(--rk-border);border-radius:999px;cursor:pointer}
276
+ .rk-ipv-frame-btn:hover{background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
277
+
278
+ /* prompt field + attachments */
279
+ .rk-ip-promptfield{position:relative}
280
+ .rk-ip-global{min-height:74px;width:100%;padding-right:44px}
281
+ .rk-ip-attach{position:absolute;right:7px;bottom:7px;display:grid;place-items:center;width:30px;height:30px;border-radius:50%;cursor:pointer;border:1px solid var(--rk-border);background:#fff;color:var(--rk-n-600)}
282
+ .rk-ip-attach:hover{background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
283
+ .rk-ip-attach-chips{display:flex;flex-wrap:wrap;gap:6px}
284
+ .editor-inpaint-attachment-chip{display:inline-flex;align-items:center;gap:6px;padding:3px 4px 3px 6px;border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);background:var(--rk-n-50);font-size:11.5px;color:var(--rk-n-700);max-width:170px}
285
+ .editor-inpaint-attachment-chip img{width:22px;height:22px;border-radius:4px;object-fit:cover;flex:none}
286
+ .editor-inpaint-attachment-chip span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
287
+ .editor-inpaint-attachment-chip button{flex:none;border:0;background:transparent;cursor:pointer;color:var(--rk-text-faint);font-size:15px;line-height:1;padding:0 2px}
288
+ .rk-ip-setting-note{font-weight:600;color:var(--rk-text-faint)}
289
+
290
+ /* mask color picker + timed mask rows */
291
+ .rk-inpaint-brushrow{display:flex;align-items:center;justify-content:space-between;gap:10px}
292
+ .rk-ip-colorpicker{width:42px;height:32px;padding:2px;border:1px solid var(--rk-border-strong);border-radius:var(--rk-r-lg);background:#fff;cursor:pointer}
293
+ .rk-inpaint-range{width:100%}
294
+ .rk-ipv-masks{display:grid;gap:8px}
295
+ .rk-ipv-mask{position:relative;display:grid;grid-template-columns:28px minmax(0,1fr);gap:9px;padding:8px;border:1px solid var(--rk-border);border-radius:var(--rk-r-xl);background:var(--rk-n-50)}
296
+ .rk-ipv-mask[data-active="true"]{border-color:var(--rk-gold-600);background:var(--rk-gold-tint)}
297
+ .rk-ipv-mask-dot{width:24px;height:24px;padding:0;border:2px solid #fff;border-radius:50%;cursor:pointer;background:var(--ipv-c,#ff2d55);box-shadow:0 0 0 1px var(--rk-border-strong)}
298
+ .rk-ipv-mask[data-active="true"] .rk-ipv-mask-dot{box-shadow:0 0 0 2px var(--rk-gold-700)}
299
+ .rk-ipv-mask-body{min-width:0;display:grid;gap:6px}
300
+ .rk-ipv-mask-row{display:flex;align-items:center;gap:6px}
301
+ .rk-ipv-mask-time{flex:1;min-width:0;display:flex;align-items:center;gap:6px;padding:4px 9px;font:inherit;font-size:12px;font-weight:800;font-variant-numeric:tabular-nums;color:var(--rk-ink);background:#fff;border:1px solid var(--rk-border);border-radius:999px;cursor:pointer}
302
+ .rk-ipv-mask-time:hover{border-color:var(--rk-gold-600);color:var(--rk-gold-700)}
303
+ .rk-ipv-mask-time svg{flex:none;color:var(--rk-text-muted)}
304
+ .rk-ipv-mask-mini{display:grid;place-items:center;width:24px;height:24px;flex:none;border:1px solid var(--rk-border);border-radius:50%;background:#fff;color:var(--rk-n-600);cursor:pointer}
305
+ .rk-ipv-mask-mini:hover{background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
306
+ .rk-ipv-mask-mini.is-danger:hover{color:#b42318;border-color:#e6a8a0;background:#fff}
307
+ .rk-ipv-mask-prompt{width:100%;min-height:44px;resize:vertical;padding:8px 10px;font:inherit;font-size:12.5px;line-height:1.4;color:var(--rk-ink);border:1px solid var(--rk-border-strong);border-radius:var(--rk-r-lg);background:#fff;outline:0}
308
+ .rk-ipv-mask-prompt:focus{border-color:var(--rk-gold-600);box-shadow:0 0 0 3px var(--rk-gold-tint)}
309
+ .rk-ipv-masks-empty{font-size:12px;color:var(--rk-text-faint);padding:8px 2px;line-height:1.4}
310
+ .rk-ip-addcolor{justify-self:start}
311
+
312
+ /* provider select */
313
+ .rk-ip-provider{display:none;gap:6px}
314
+ .rk-ip-provider[data-visible="true"]{display:grid}
315
+ .rk-ip-setting{display:grid;gap:6px}
316
+ .rk-ipv-audio-row{grid-template-columns:1fr auto;align-items:center}
317
+ .rk-inpaint-select{width:100%}
318
+ .rk-ip-input{width:100%;padding:8px 10px;font:inherit;font-size:13px;color:var(--rk-ink);border:1px solid var(--rk-border-strong);border-radius:var(--rk-r-lg);background:#fff;outline:0}
319
+ .rk-ip-input:focus{border-color:var(--rk-gold-600);box-shadow:0 0 0 3px var(--rk-gold-tint)}
320
+
321
+ /* generate + status */
322
+ .rk-inpaint-generate{width:100%;justify-content:center;gap:8px;padding:13px}
323
+ .rk-inpaint-generate[data-loading="true"]{opacity:.7;cursor:wait}
324
+ .rk-inpaint-status{font-size:12.5px;font-weight:600;line-height:1.5;min-height:18px;color:var(--rk-text-muted)}
325
+ .rk-inpaint-status[data-tone="error"]{color:#b42318}
326
+ .rk-inpaint-status[data-tone="success"]{color:#1f7a44}
327
+
328
+ /* center video stage */
329
+ .rk-inpaint-stage{min-height:420px;display:grid;place-items:center;padding:20px;overflow:hidden}
330
+ .rk-ipv-stage{align-content:center}
331
+ .rk-inpaint-empty{display:grid;justify-items:center;gap:8px;text-align:center;color:var(--rk-text-muted);padding:30px 20px}
332
+ .rk-inpaint-empty[hidden]{display:none}
333
+ .rk-inpaint-empty-ic{color:var(--rk-n-400)}
334
+ .rk-inpaint-empty-title{font-family:var(--rk-font-display);font-weight:800;font-size:var(--rk-text-lg);color:var(--rk-ink)}
335
+ .rk-inpaint-empty-sub{font-size:13px}
336
+ .rk-ipv-upload{margin-top:6px;gap:6px}
337
+ .rk-ipv-player{width:100%;display:grid;gap:12px;justify-items:center}
338
+ .rk-ipv-player[hidden]{display:none}
339
+ .rk-ipv-canvaswrap{position:relative;display:inline-block;line-height:0;max-width:100%;border-radius:var(--rk-r-2xl);overflow:hidden;box-shadow:var(--rk-shadow-md);
340
+ --inpaint-brush-cursor-size:70px;--inpaint-brush-cursor-x:0px;--inpaint-brush-cursor-y:0px;background:#000}
341
+ .rk-ipv-video{display:block;max-width:100%;max-height:min(58vh,560px);width:auto;height:auto;object-fit:contain;user-select:none}
342
+ .rk-ipv-canvas{position:absolute;inset:0;width:100%;height:100%;opacity:.82;cursor:crosshair;touch-action:none}
343
+ .editor-inpaint-brush-cursor{position:absolute;z-index:2;left:0;top:0;width:var(--inpaint-brush-cursor-size);height:var(--inpaint-brush-cursor-size);
344
+ border:2px solid rgba(255,255,255,.95);border-radius:50%;box-shadow:0 0 0 1px rgba(28,25,23,.7),0 0 18px rgba(28,25,23,.25);opacity:0;pointer-events:none;
345
+ transform:translate(var(--inpaint-brush-cursor-x),var(--inpaint-brush-cursor-y)) translate(-50%,-50%)}
346
+ .editor-inpaint-brush-cursor[data-visible="true"]{opacity:1}
347
+ .rk-ipv-controls{width:100%;max-width:640px;display:flex;align-items:center;gap:10px}
348
+ .rk-ipv-playpause{flex:none;display:grid;place-items:center;width:38px;height:38px;border-radius:50%;border:0;background:var(--rk-ink);color:#fff;cursor:pointer}
349
+ .rk-ipv-playpause:hover{background:var(--rk-n-800,#333)}
350
+ .rk-ipv-scrub{flex:1;min-width:0;accent-color:var(--rk-gold-600)}
351
+ .rk-ipv-timecode{flex:none;display:flex;align-items:center;gap:6px}
352
+ .rk-ipv-timecode-input{width:96px;padding:5px 8px;font:inherit;font-size:12.5px;font-weight:800;font-variant-numeric:tabular-nums;text-align:center;color:var(--rk-ink);border:1px solid var(--rk-border-strong);border-radius:var(--rk-r-lg);background:#fff;outline:0}
353
+ .rk-ipv-timecode-input:focus{border-color:var(--rk-gold-600);box-shadow:0 0 0 3px var(--rk-gold-tint)}
354
+ .rk-ipv-timecode-copy{width:30px;height:30px}
355
+ .rk-ipv-duration{font-size:11.5px;font-weight:700;color:var(--rk-text-faint);font-variant-numeric:tabular-nums;white-space:nowrap}
356
+
357
+ /* text-to-video composer (empty state) */
358
+ .rk-ip-gen{width:100%;max-width:460px;margin-top:16px;display:grid;gap:10px;justify-items:stretch}
359
+ .rk-ip-gen-or{position:relative;font-size:11.5px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--rk-text-faint)}
360
+ .rk-ip-gen-or::before,.rk-ip-gen-or::after{content:"";position:absolute;top:50%;width:calc(50% - 26px);height:1px;background:var(--rk-border)}
361
+ .rk-ip-gen-or::before{left:0}.rk-ip-gen-or::after{right:0}
362
+ .rk-ip-gen-box{background:#fff;border:1.5px solid var(--rk-border-strong);border-radius:var(--rk-r-2xl);padding:10px 10px 8px;display:grid;gap:8px;box-shadow:var(--rk-shadow-sm);text-align:left}
363
+ .rk-ip-gen-box:focus-within{border-color:var(--rk-gold-600);box-shadow:0 0 0 3px var(--rk-gold-tint)}
364
+ .rk-ip-gen.is-busy .rk-ip-gen-box{opacity:.75}
365
+ .rk-ip-gen-input{width:100%;border:0;outline:0;resize:none;background:transparent;font:inherit;font-size:14px;line-height:1.45;color:var(--rk-ink);min-height:44px;padding:2px 4px}
366
+ .rk-ip-gen-input::placeholder{color:var(--rk-text-faint)}
367
+ .rk-ip-gen-refs{display:flex;flex-wrap:wrap;gap:6px}
368
+ .rk-ip-gen-refs[hidden]{display:none}
369
+ .rk-ip-gen-ref{display:inline-flex;align-items:center;gap:6px;padding:3px 4px 3px 6px;border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);background:var(--rk-n-50);font-size:11.5px;color:var(--rk-n-700);max-width:180px}
370
+ .rk-ip-gen-ref img{width:22px;height:22px;border-radius:4px;object-fit:cover;flex:none}
371
+ .rk-ip-gen-ref span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
372
+ .rk-ip-gen-ref button{flex:none;border:0;background:transparent;cursor:pointer;color:var(--rk-text-faint);font-size:15px;line-height:1;padding:0 2px}
373
+ .rk-ip-gen-bar{display:flex;align-items:center;gap:8px}
374
+ .rk-ip-gen-hint{flex:1;font-size:11.5px;color:var(--rk-text-faint);line-height:1.35}
375
+ .rk-ip-gen-clip,.rk-ip-gen-send{flex:none;display:grid;place-items:center;width:34px;height:34px;border-radius:50%;cursor:pointer}
376
+ .rk-ip-gen-clip{border:1px solid var(--rk-border);background:#fff;color:var(--rk-n-600)}
377
+ .rk-ip-gen-clip:hover{background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
378
+ .rk-ip-gen-send{border:0;background:var(--rk-gold-500,var(--rk-gold-600));color:var(--rk-ink)}
379
+ .rk-ip-gen-send:hover:not(:disabled){background:var(--rk-gold-600)}
380
+ .rk-ip-gen-send:disabled{opacity:.5;cursor:default}
381
+ .rk-ip-gen-send.is-busy{background:var(--rk-gold-tint);pointer-events:none}
382
+ .rk-ip-gen-spin{width:16px;height:16px;border-radius:50%;border:2.5px solid var(--rk-gold-tint2,rgba(252,185,0,.35));border-top-color:var(--rk-gold-700);animation:rk-ip-spin .8s linear infinite}
383
+ .rk-ip-gen-status{font-size:12px;font-weight:600;min-height:16px;color:var(--rk-text-muted);text-align:left;padding-left:2px}
384
+ .rk-ip-gen-status[data-tone="error"]{color:#b42318}
385
+ .rk-ip-gen-status[data-tone="success"]{color:#1f7a44}
386
+
387
+ /* file-directory pop panel (reference picker) */
388
+ .rk-ip-filemodal{position:fixed;inset:0;z-index:60;background:rgba(28,25,23,.44);backdrop-filter:blur(2px);display:grid;place-items:center;padding:20px}
389
+ .rk-ip-filemodal-panel{width:min(560px,100%);max-height:min(80vh,680px);display:flex;flex-direction:column;background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);box-shadow:var(--rk-shadow-lg,var(--rk-shadow-card));overflow:hidden}
390
+ .rk-ip-filemodal-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:14px 18px;border-bottom:1px solid var(--rk-border)}
391
+ .rk-ip-filemodal-title{font-family:var(--rk-font-display);font-weight:800;font-size:var(--rk-text-md,15px);color:var(--rk-ink)}
392
+ .rk-ip-filemodal-close{border:0;background:transparent;font-size:22px;line-height:1;color:var(--rk-text-muted);cursor:pointer;padding:0 4px}
393
+ .rk-ip-filemodal-mount{padding:12px 14px;overflow:auto;flex:1;min-height:0}
394
+
395
+ /* right: result + history */
396
+ .rk-inpaint-output{padding:18px;display:grid;gap:18px;align-content:start}
397
+ .rk-inpaint-result[hidden]{display:none}
398
+ .rk-inpaint-result-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
399
+ .rk-ipv-result-video{display:block;width:100%;border-radius:var(--rk-r-2xl);background:#000;box-shadow:var(--rk-shadow-sm)}
400
+ .rk-inpaint-history-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
401
+ .rk-ip-histtools{display:flex;align-items:center;gap:8px}
402
+ .rk-inpaint-historylist{display:grid;gap:10px}
403
+ .custom-inpaint-history-empty{font-size:12.5px;color:var(--rk-text-faint);padding:14px 4px;text-align:center;line-height:1.4}
404
+ .custom-inpaint-history-item{position:relative;display:grid;grid-template-columns:54px minmax(0,1fr);align-items:center;gap:10px;padding:8px;border:1px solid var(--rk-border);border-radius:var(--rk-r-xl);background:var(--rk-n-50)}
405
+ .custom-inpaint-history-thumb-button{position:relative;width:54px;height:54px;display:grid;place-items:center;padding:0;border:0;border-radius:var(--rk-r-lg);background:var(--rk-n-200);cursor:zoom-in;overflow:hidden}
406
+ .custom-inpaint-history-thumb{width:54px;height:54px;border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);object-fit:cover;background:#000}
407
+ .custom-inpaint-history-thumb-badge{position:absolute;inset:0;display:grid;place-items:center;color:#fff;text-shadow:0 1px 4px rgba(0,0,0,.6);pointer-events:none}
408
+ .custom-inpaint-history-generating{width:54px;height:54px;display:grid;place-items:center;border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);background:var(--rk-n-100)}
409
+ .custom-inpaint-history-generating::before{content:"";width:22px;height:22px;border-radius:50%;border:2.5px solid var(--rk-gold-tint2,rgba(252,185,0,.35));border-top-color:var(--rk-gold-700);animation:rk-ip-spin .8s linear infinite}
410
+ .custom-inpaint-history-meta{min-width:0;display:grid;gap:5px}
411
+ .custom-inpaint-history-title{min-width:0;color:var(--rk-ink);font-size:12.5px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
412
+ .custom-inpaint-history-subtitle{min-width:0;color:var(--rk-text-muted);font-size:11.5px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
413
+ .custom-inpaint-history-actions{display:flex;align-items:center;gap:5px}
414
+ .custom-inpaint-history-icon{width:26px;height:26px;display:inline-grid;place-items:center;padding:0;border:1px solid var(--rk-border);border-radius:50%;background:#fff;color:var(--rk-n-700);cursor:pointer;text-decoration:none}
415
+ .custom-inpaint-history-icon:hover{background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
416
+ .custom-inpaint-history-icon svg{width:13px;height:13px}
417
+ .custom-inpaint-history-check{min-height:26px;padding:0 10px;border:1px solid var(--rk-border);border-radius:999px;background:#fff;color:var(--rk-ink);font:inherit;font-size:11.5px;font-weight:800;cursor:pointer}
418
+ .custom-inpaint-history-check[disabled]{cursor:wait;opacity:.6}
419
+ .custom-inpaint-history-popconfirm{position:absolute;right:6px;top:6px;z-index:6;width:160px;display:grid;gap:7px;padding:9px;border:1px solid var(--rk-border);border-radius:var(--rk-r-lg);background:var(--rk-surface);box-shadow:var(--rk-shadow-lg,var(--rk-shadow-card))}
420
+ .custom-inpaint-history-popconfirm[hidden]{display:none}
421
+ .custom-inpaint-history-popconfirm p{margin:0;color:var(--rk-ink);font-size:12px;font-weight:700;line-height:1.3}
422
+ .custom-inpaint-history-popconfirm-actions{display:flex;gap:6px}
423
+ .custom-inpaint-history-popconfirm-actions button{flex:1 1 0;min-height:26px;padding:0 8px;border-radius:var(--rk-r-md,8px);border:1px solid var(--rk-border);background:#fff;color:var(--rk-ink);font:inherit;font-size:11.5px;font-weight:700;cursor:pointer}
424
+ .custom-inpaint-history-popconfirm-actions button[data-danger="true"]{border-color:#e6a8a0;color:#b42318}
425
+
426
+ /* lightbox */
427
+ .rk-ip-lightbox{position:fixed;inset:0;z-index:80;display:none;place-items:center;padding:22px;background:rgba(28,25,23,.66);backdrop-filter:blur(8px)}
428
+ .rk-ip-lightbox[data-open="true"]{display:grid}
429
+ .rk-ip-lightbox[hidden]{display:none}
430
+ .rk-ipv-lightbox-dialog{width:min(1100px,calc(100vw - 44px));max-height:94svh;display:grid;grid-template-rows:auto minmax(0,1fr);gap:12px;padding:14px;border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);background:var(--rk-surface);box-shadow:var(--rk-shadow-lg,var(--rk-shadow-card))}
431
+ .custom-inpaint-lightbox-head{display:flex;align-items:center;justify-content:space-between;gap:10px}
432
+ .custom-inpaint-lightbox-title{min-width:0;margin:0;color:var(--rk-ink);font-size:14px;font-weight:800;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
433
+ .custom-inpaint-lightbox-actions{display:flex;align-items:center;gap:8px}
434
+ .rk-ipv-lightbox-stage{min-height:0;display:grid;place-items:center;overflow:auto}
435
+ .rk-ipv-lightbox-stage video{max-width:100%;max-height:calc(94svh - 92px);border-radius:var(--rk-r-xl);background:#000}
436
+
437
+ @keyframes rk-ip-spin{to{transform:rotate(360deg)}}
438
+
439
+ /* settings modal */
440
+ .rk-ip-settingsmodal{position:fixed;inset:0;z-index:70;background:rgba(28,25,23,.44);backdrop-filter:blur(2px);display:grid;place-items:center;padding:20px}
441
+ .rk-ip-settingsmodal[hidden]{display:none}
442
+ .rk-ip-settingsmodal-panel{width:min(440px,100%);display:flex;flex-direction:column;background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);box-shadow:var(--rk-shadow-lg,var(--rk-shadow-card));overflow:hidden}
443
+ .rk-ip-settingsmodal-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:14px 18px;border-bottom:1px solid var(--rk-border)}
444
+ .rk-ip-settingsmodal-title{font-family:var(--rk-font-display);font-weight:800;font-size:var(--rk-text-md,15px);color:var(--rk-ink)}
445
+ .rk-ip-settingsmodal-body{padding:16px 18px;display:grid;gap:14px}
446
+
447
+ /* past-sessions panel */
448
+ .rk-ip-sessions{position:fixed;left:0;top:0;bottom:0;z-index:96;width:min(320px,86vw);display:flex;flex-direction:column;background:var(--rk-surface);border-right:1px solid var(--rk-border);box-shadow:var(--rk-shadow-xl,var(--rk-shadow-card));transform:translateX(-100%);transition:transform var(--rk-dur) var(--rk-ease)}
449
+ .rk-ip-sessions.is-open{transform:none}
450
+ .rk-ip-sessions-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:16px 14px 10px;border-bottom:1px solid var(--rk-border)}
451
+ .rk-ip-sessions-title{font-family:var(--rk-font-display);font-weight:800;font-size:14px;color:var(--rk-ink)}
452
+ .rk-ip-sessions-list{flex:1;min-height:0;overflow-y:auto;padding:8px;display:grid;gap:4px;align-content:start}
453
+ .rk-ip-sessions-empty{font-size:12.5px;color:var(--rk-text-faint);padding:16px 8px;text-align:center;line-height:1.4}
454
+ .rk-ip-session{display:grid;grid-template-columns:44px minmax(0,1fr);align-items:center;gap:10px;padding:7px;border:1px solid transparent;border-radius:var(--rk-r-lg);background:transparent;cursor:pointer;text-align:left;font:inherit;width:100%}
455
+ .rk-ip-session:hover{background:var(--rk-n-50)}
456
+ .rk-ip-session.is-active{background:var(--rk-gold-tint);border-color:var(--rk-gold-600)}
457
+ .rk-ip-session-thumb{width:44px;height:44px;border:1px solid var(--rk-border);border-radius:var(--rk-r-md,8px);object-fit:cover;background:var(--rk-n-200)}
458
+ .rk-ip-session-thumb.is-empty{display:grid;place-items:center;color:var(--rk-text-faint)}
459
+ .rk-ip-session-meta{min-width:0;display:grid;gap:3px}
460
+ .rk-ip-session-title{min-width:0;color:var(--rk-ink);font-size:12.5px;font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
461
+ .rk-ip-session-sub{min-width:0;color:var(--rk-text-muted);font-size:11px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
462
+ .rk-ip-sessions-backdrop{position:fixed;inset:0;z-index:95;background:rgba(28,25,23,.34)}
463
+ .rk-ip-sessions-backdrop[hidden]{display:none}
464
+ `;
465
+ const script = INPAINT_VIDEO_SCRIPT;
466
+ return reskinDocument({
467
+ title: "vidfarm reskin — Create Video",
468
+ description: "Reskinned vidfarm video editor — scrub the timeline, paint time-scoped mask edits, set start/end frames, and generate on your own AI keys.",
469
+ activeSlug: "inpaint",
470
+ pageCss,
471
+ body,
472
+ script
473
+ });
474
+ }
475
+ // The whole client runtime lives here as a plain string so the render function
476
+ // above stays readable. It is a self-contained IIFE (no imports at runtime).
477
+ const INPAINT_VIDEO_SCRIPT = `
478
+ (function(){
479
+ var videoEl = document.querySelector("[data-ipv-video]");
480
+ var canvas = document.querySelector("[data-ipv-canvas]");
481
+ var brushCursor = document.querySelector("[data-ipv-brush-cursor]");
482
+ var player = document.querySelector("[data-ipv-player]");
483
+ var emptyState = document.querySelector("[data-ipv-empty]");
484
+ var canvasWrap = document.querySelector("[data-ipv-canvaswrap]");
485
+ var uploadBtn = document.querySelector("[data-ipv-upload]");
486
+ var fileInput = document.querySelector("[data-ipv-file]");
487
+ var startNewBtn = document.querySelector("[data-ipv-start-new]");
488
+ var statusEl = document.querySelector("[data-ipv-status]");
489
+ var submitBtn = document.querySelector("[data-ipv-submit]");
490
+ var submitLabel = document.querySelector("[data-ipv-submit-label]");
491
+ var globalPrompt = document.querySelector("[data-ipv-global-prompt]");
492
+ var attachBtn = document.querySelector("[data-ipv-attach]");
493
+ var attachChips = document.querySelector("[data-ipv-attachments]");
494
+ var attachInput = document.querySelector("[data-ipv-attachment-input]");
495
+ var brushInput = document.querySelector("[data-ipv-brush]");
496
+ var brushLabel = document.querySelector("[data-ipv-brush-label]");
497
+ var colorPicker = document.querySelector("[data-ipv-color-picker]");
498
+ var addMaskBtn = document.querySelector("[data-ipv-add-mask]");
499
+ var masksList = document.querySelector("[data-ipv-masks]");
500
+ var undoBtn = document.querySelector("[data-ipv-undo]");
501
+ var redoBtn = document.querySelector("[data-ipv-redo]");
502
+ var clearBtn = document.querySelector("[data-ipv-clear]");
503
+ var frameInput = document.querySelector("[data-ipv-frame-input]");
504
+ var playpause = document.querySelector("[data-ipv-playpause]");
505
+ var scrub = document.querySelector("[data-ipv-scrub]");
506
+ var timecodeInput = document.querySelector("[data-ipv-timecode-input]");
507
+ var timecodeCopy = document.querySelector("[data-ipv-timecode-copy]");
508
+ var durationEl = document.querySelector("[data-ipv-duration]");
509
+ var outputPanel = document.querySelector("[data-ipv-output]");
510
+ var resultVideo = document.querySelector("[data-ipv-result]");
511
+ var saveBtn = document.querySelector("[data-ipv-save]");
512
+ var historyList = document.querySelector("[data-ipv-history-list]");
513
+ var historyRefresh = document.querySelector("[data-ipv-history-refresh]");
514
+ var settingsBtn = document.querySelector("[data-ipv-settings-button]");
515
+ var settingsModal = document.querySelector("[data-ipv-settings]");
516
+ var settingsClose = document.querySelector("[data-ipv-settings-close]");
517
+ var providerWrap = document.querySelector("[data-ipv-provider-wrap]");
518
+ var providerSelect = document.querySelector("[data-ipv-provider]");
519
+ var durationSelect = document.querySelector("[data-ipv-duration]");
520
+ var audioToggle = document.querySelector("[data-ipv-audio]");
521
+ var tracerInput = document.querySelector("[data-ipv-tracer]");
522
+ var sessionsToggle = document.querySelector("[data-ipv-history-toggle]");
523
+ var sessionsPanel = document.querySelector("[data-ipv-sessions]");
524
+ var sessionsList = document.querySelector("[data-ipv-sessions-list]");
525
+ var sessionsClose = document.querySelector("[data-ipv-sessions-close]");
526
+ var sessionsBackdrop = document.querySelector("[data-ipv-sessions-backdrop]");
527
+ var lightbox = document.querySelector("[data-ipv-lightbox]");
528
+ var lightboxTitle = document.querySelector("[data-ipv-lightbox-title]");
529
+ var lightboxVideo = document.querySelector("[data-ipv-lightbox-video]");
530
+ var lightboxCopy = document.querySelector("[data-ipv-lightbox-copy]");
531
+ var lightboxDownload = document.querySelector("[data-ipv-lightbox-download]");
532
+ var lightboxClose = document.querySelector("[data-ipv-lightbox-close]");
533
+ if(!videoEl || !canvas || !masksList || !submitBtn) return;
534
+
535
+ var PLAY_SVG = ${JSON.stringify(PLAY_ICON)};
536
+ var PAUSE_SVG = ${JSON.stringify(PAUSE_ICON)};
537
+ var CLOCK_SVG = ${JSON.stringify(CLOCK_ICON)};
538
+
539
+ // ===== state =====
540
+ var sourceVideoUrl = ""; // playable URL (may be a blob: object URL for uploads)
541
+ var sourceVideoName = "";
542
+ var sourceObjectUrl = ""; // revoke on reset
543
+ var masks = []; // { id, color, prompt, timeSec, strokes:[], redo:[] }
544
+ var activeMaskId = "";
545
+ var isPainting = false;
546
+ var frames = { first: null, last: null }; // { url, name }
547
+ var attachments = []; // { id, fileName, url }
548
+ var maxAttachments = 8;
549
+ var allowedAttachTypes = { "image/jpeg":1, "image/png":1, "image/webp":1 };
550
+ var historyItems = [];
551
+ var hiddenHistoryKeys = {};
552
+ var sourceHistoryItems = [];
553
+ var sessionTracer = "";
554
+ var tracerAutoPending = true;
555
+ var maskSeq = 0;
556
+ var isSeekingFor = null; // pending frame-capture continuation
557
+
558
+ // ===== small helpers =====
559
+ function pad(n, w){ n = String(Math.max(0, Math.floor(n))); while(n.length < w) n = "0" + n; return n; }
560
+ function formatTimecode(sec){
561
+ var s = Math.max(0, Number(sec) || 0);
562
+ var mm = Math.floor(s / 60);
563
+ var ss = Math.floor(s % 60);
564
+ var ms = Math.round((s - Math.floor(s)) * 1000);
565
+ if(ms >= 1000){ ms -= 1000; ss += 1; }
566
+ if(ss >= 60){ ss -= 60; mm += 1; }
567
+ return pad(mm, 2) + ":" + pad(ss, 2) + ":" + pad(ms, 3);
568
+ }
569
+ function parseTimecode(str){
570
+ var parts = String(str || "").trim().split(":");
571
+ if(!parts.length) return NaN;
572
+ var mm = 0, ss = 0, ms = 0;
573
+ if(parts.length >= 3){ mm = parseInt(parts[0], 10) || 0; ss = parseInt(parts[1], 10) || 0; ms = parseInt(parts[2], 10) || 0; }
574
+ else if(parts.length === 2){ mm = parseInt(parts[0], 10) || 0; ss = parseInt(parts[1], 10) || 0; }
575
+ else { ss = parseInt(parts[0], 10) || 0; }
576
+ return mm * 60 + ss + ms / 1000;
577
+ }
578
+ function absoluteUrl(value){ return new URL(value, window.location.href).toString(); }
579
+ function parseJsonResponse(response){ return response.json().catch(function(){ return {}; }); }
580
+ function setStatus(message, tone){ if(!statusEl) return; statusEl.textContent = message || ""; statusEl.setAttribute("data-tone", tone || ""); }
581
+ function nowIso(){ return new Date().toISOString(); }
582
+ function uid(prefix){
583
+ if(globalThis.crypto && globalThis.crypto.randomUUID) return prefix + globalThis.crypto.randomUUID().replace(/-/g, "").slice(0, 8);
584
+ return prefix + Math.random().toString(16).slice(2, 10);
585
+ }
586
+
587
+ // ===== tracer =====
588
+ function tracerSuffix(){
589
+ return (globalThis.crypto && globalThis.crypto.randomUUID)
590
+ ? globalThis.crypto.randomUUID().replace(/-/g, "").slice(0, 6)
591
+ : Math.random().toString(16).slice(2, 8);
592
+ }
593
+ function slugifyTracer(value){
594
+ var normalized = String(value || "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32);
595
+ return normalized || "video-edit";
596
+ }
597
+ function nameTracerFromSeed(seed){
598
+ var value = "video-edit-" + slugifyTracer(seed) + "-" + tracerSuffix();
599
+ sessionTracer = value; tracerAutoPending = false; if(tracerInput) tracerInput.value = value; return value;
600
+ }
601
+ function resolveTracer(){ if(sessionTracer) return sessionTracer; var v = tracerInput ? tracerInput.value.trim() : ""; return v; }
602
+ function getTracerValue(){ if(sessionTracer) return sessionTracer; var v = tracerInput ? tracerInput.value.trim() : ""; if(v){ sessionTracer = v; tracerAutoPending = false; return v; } return nameTracerFromSeed("video-edit"); }
603
+
604
+ // ===== provider list =====
605
+ function loadProviders(){
606
+ fetch("/api/v1/user/me/provider-keys", { credentials: "same-origin" }).then(parseJsonResponse).then(function(payload){
607
+ var supported = { openai:1, gemini:1, openrouter:1 };
608
+ var providers = []; var seen = {};
609
+ var keys = (payload && payload.provider_keys) || [];
610
+ for(var i = 0; i < keys.length; i++){
611
+ var key = keys[i]; var provider = String(key.provider || ""); var stat = String(key.status || "active");
612
+ var cd = key.cooldown_until ? Date.parse(String(key.cooldown_until)) : 0;
613
+ if(!supported[provider] || stat !== "active" || (cd && cd > Date.now()) || seen[provider]) continue;
614
+ seen[provider] = 1; providers.push(provider);
615
+ }
616
+ if(providerSelect){
617
+ providerSelect.innerHTML = '<option value="">Auto</option>' + providers.map(function(p){ return '<option value="' + p + '">' + p.charAt(0).toUpperCase() + p.slice(1) + '</option>'; }).join("");
618
+ }
619
+ if(providerWrap) providerWrap.setAttribute("data-visible", providers.length > 1 ? "true" : "false");
620
+ }).catch(function(){});
621
+ }
622
+
623
+ // ===== upload / save =====
624
+ function uploadTemporaryBlob(blob, fileName, contentType, folderPath){
625
+ var folder = folderPath || "inpaints/_inputs";
626
+ return fetch("/api/v1/user/me/temporary-files/presign", {
627
+ method: "POST", credentials: "same-origin", headers: { "content-type": "application/json" },
628
+ body: JSON.stringify({ file_name: fileName, content_type: contentType, size_bytes: blob.size, folder_path: folder })
629
+ }).then(function(r){ return parseJsonResponse(r).then(function(p){ return { ok: r.ok, p: p }; }); }).then(function(res){
630
+ var presign = res.p;
631
+ if(!res.ok || !presign || !presign.transport) throw new Error((presign && presign.error) || "Unable to prepare upload.");
632
+ if(presign.transport === "server"){
633
+ var formData = new FormData();
634
+ formData.append((presign.upload && presign.upload.form_field) || "file", new File([blob], fileName, { type: contentType }));
635
+ formData.append("folder_path", folder);
636
+ return fetch((presign.upload && presign.upload.url) || "/api/v1/user/me/temporary-files/upload", { method: (presign.upload && presign.upload.method) || "POST", credentials: "same-origin", body: formData })
637
+ .then(function(r){ return parseJsonResponse(r).then(function(p){ return { ok: r.ok, p: p }; }); })
638
+ .then(function(u){ if(!u.ok || !u.p.file) throw new Error(u.p.error || "Unable to upload file."); return u.p.file.viewUrl || u.p.file.view_url || u.p.file.s3Url || u.p.file.s3_url; });
639
+ }
640
+ return fetch(presign.upload.url, { method: presign.upload.method, headers: presign.upload.headers, body: blob }).then(function(r){
641
+ if(!r.ok) throw new Error("Upload failed with status " + r.status + ".");
642
+ return fetch("/api/v1/user/me/temporary-files", { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json" },
643
+ body: JSON.stringify({ file_id: presign.file_id, file_name: presign.file_name, content_type: presign.content_type, size_bytes: blob.size, storage_key: presign.storage_key, folder_path: presign.folder_path || folder }) })
644
+ .then(function(r2){ return parseJsonResponse(r2).then(function(p){ return { ok: r2.ok, p: p }; }); })
645
+ .then(function(f){ if(!f.ok || !f.p.file) throw new Error(f.p.error || "Unable to save file."); return f.p.file.viewUrl || f.p.file.view_url || f.p.file.s3Url || f.p.file.s3_url; });
646
+ });
647
+ });
648
+ }
649
+ function saveMediaToFiles(mediaUrl, defaultName){
650
+ if(!mediaUrl) return;
651
+ var dir = window.__vidfarmDirectory;
652
+ if(!dir || !dir.pick){ setStatus("File browser isn't available right now.", "error"); return; }
653
+ dir.pick({ select: "folder", roots: ["files"], initialPath: "/files", title: "Save to My Files", hint: "Open a folder in your Files, then choose it to save this video there.", confirmLabel: "Save here",
654
+ onPick: function(folder){
655
+ setStatus("Saving to " + (folder && folder.path ? folder.path : "My Files") + "\\u2026", "");
656
+ fetch("/api/v1/user/me/attachments/from-url", { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json" },
657
+ body: JSON.stringify({ source_url: mediaUrl, folder_path: (folder && folder.folderPath) || "", file_name: defaultName || "" }) })
658
+ .then(function(r){ return r.json().then(function(j){ return { ok: r.ok, j: j }; }); })
659
+ .then(function(res){ if(!res.ok || !res.j || res.j.error) throw new Error((res.j && res.j.error) || "Couldn't save to My Files."); setStatus("Saved to " + (folder && folder.path ? folder.path : "My Files") + ".", "success"); })
660
+ .catch(function(err){ setStatus(err && err.message ? err.message : "Couldn't save to My Files.", "error"); });
661
+ } });
662
+ }
663
+ function copyText(value){
664
+ if(navigator.clipboard && window.isSecureContext){ return navigator.clipboard.writeText(value).then(function(){ return true; }).catch(function(){ return fallbackCopy(value); }); }
665
+ return Promise.resolve(fallbackCopy(value));
666
+ }
667
+ function fallbackCopy(value){
668
+ var field = document.createElement("textarea"); field.value = value; field.setAttribute("readonly", ""); field.style.position = "fixed"; field.style.opacity = "0";
669
+ document.body.appendChild(field); field.select(); var ok = false; try { ok = document.execCommand("copy"); } catch(_){} field.remove(); return ok;
670
+ }
671
+
672
+ // ===== masks =====
673
+ function activeMask(){ for(var i = 0; i < masks.length; i++){ if(masks[i].id === activeMaskId) return masks[i]; } return null; }
674
+ function normalizeHex(v){ v = String(v || "").trim(); if(/^#[0-9a-fA-F]{6}$/.test(v)) return v.toLowerCase(); if(/^#[0-9a-fA-F]{3}$/.test(v)){ return ("#" + v[1] + v[1] + v[2] + v[2] + v[3] + v[3]).toLowerCase(); } return "#ff2d55"; }
675
+ var PALETTE = ["#ff2d55","#0a84ff","#34c759","#ff9f0a","#bf5af2","#ff375f","#5ac8fa","#ffd60a"];
676
+ function nextColor(){ var used = {}; masks.forEach(function(m){ used[m.color] = 1; }); for(var i = 0; i < PALETTE.length; i++){ if(!used[PALETTE[i]]) return PALETTE[i]; } return PALETTE[masks.length % PALETTE.length]; }
677
+ function addMask(atTime, color){
678
+ var m = { id: uid("mask_"), color: normalizeHex(color || nextColor()), prompt: "", timeSec: (atTime != null ? atTime : (videoEl.currentTime || 0)), strokes: [], redo: [] };
679
+ masks.push(m); activeMaskId = m.id; if(colorPicker) colorPicker.value = m.color; renderMasks(); return m;
680
+ }
681
+ function removeMask(id){
682
+ masks = masks.filter(function(m){ return m.id !== id; });
683
+ if(activeMaskId === id) activeMaskId = masks.length ? masks[masks.length - 1].id : "";
684
+ renderMasks(); redraw();
685
+ }
686
+ function renderMasks(){
687
+ masksList.innerHTML = "";
688
+ if(!masks.length){
689
+ var empty = document.createElement("div"); empty.className = "rk-ipv-masks-empty";
690
+ empty.textContent = "No masks yet. Paint on the video, or use + Add timed mask, to mark a region at a moment.";
691
+ masksList.appendChild(empty); syncUndoRedo(); return;
692
+ }
693
+ masks.forEach(function(m){
694
+ var row = document.createElement("div"); row.className = "rk-ipv-mask"; row.dataset.maskId = m.id; row.setAttribute("data-active", m.id === activeMaskId ? "true" : "false");
695
+ var dot = document.createElement("button"); dot.type = "button"; dot.className = "rk-ipv-mask-dot"; dot.style.setProperty("--ipv-c", m.color); dot.dataset.maskSelect = m.id; dot.title = "Select this mask to paint";
696
+ var bodyEl = document.createElement("div"); bodyEl.className = "rk-ipv-mask-body";
697
+ var rowTop = document.createElement("div"); rowTop.className = "rk-ipv-mask-row";
698
+ var timeBtn = document.createElement("button"); timeBtn.type = "button"; timeBtn.className = "rk-ipv-mask-time"; timeBtn.dataset.maskSeek = m.id; timeBtn.title = "Jump the video to this moment"; timeBtn.innerHTML = CLOCK_SVG + "<span>" + formatTimecode(m.timeSec) + "</span>";
699
+ var setBtn = document.createElement("button"); setBtn.type = "button"; setBtn.className = "rk-ipv-mask-mini"; setBtn.dataset.maskSettime = m.id; setBtn.title = "Set this mask to the current playhead"; setBtn.innerHTML = "<svg width='13' height='13' viewBox='0 0 20 20' fill='none' stroke='currentColor' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'><path d='M10 4v12'/><path d='m6 8 4-4 4 4'/></svg>";
700
+ var delBtn = document.createElement("button"); delBtn.type = "button"; delBtn.className = "rk-ipv-mask-mini is-danger"; delBtn.dataset.maskRemove = m.id; delBtn.title = "Remove this mask"; delBtn.innerHTML = "<svg width='12' height='12' viewBox='0 0 20 20' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M5 5l10 10M15 5 5 15'/></svg>";
701
+ rowTop.appendChild(timeBtn); rowTop.appendChild(setBtn); rowTop.appendChild(delBtn);
702
+ var ta = document.createElement("textarea"); ta.className = "rk-ipv-mask-prompt"; ta.dataset.maskPrompt = m.id; ta.placeholder = "What changes in this region at this moment?"; ta.value = m.prompt || "";
703
+ bodyEl.appendChild(rowTop); bodyEl.appendChild(ta);
704
+ row.appendChild(dot); row.appendChild(bodyEl); masksList.appendChild(row);
705
+ });
706
+ syncUndoRedo();
707
+ }
708
+ function selectMask(id){ activeMaskId = id; var m = activeMask(); if(m && colorPicker) colorPicker.value = m.color; renderMasks(); redraw(); }
709
+ function syncUndoRedo(){ var m = activeMask(); if(undoBtn) undoBtn.disabled = !m || !m.strokes.length; if(redoBtn) redoBtn.disabled = !m || !m.redo.length; }
710
+
711
+ // ===== canvas / painting =====
712
+ function syncCanvasSize(){
713
+ var w = Math.round(videoEl.clientWidth), h = Math.round(videoEl.clientHeight);
714
+ if(w > 0 && h > 0 && (canvas.width !== w || canvas.height !== h)){ canvas.width = w; canvas.height = h; }
715
+ updateBrushCursorSize();
716
+ }
717
+ function updateBrushCursorSize(){
718
+ if(!brushCursor) return; var size = Number(brushInput ? brushInput.value : 70) || 70;
719
+ var rect = canvas.getBoundingClientRect(); var scale = rect.width && canvas.width ? rect.width / canvas.width : 1;
720
+ canvasWrap.style.setProperty("--inpaint-brush-cursor-size", (size * scale) + "px");
721
+ }
722
+ function updateBrushCursorPos(event){
723
+ if(!brushCursor) return; var rect = canvasWrap.getBoundingClientRect();
724
+ canvasWrap.style.setProperty("--inpaint-brush-cursor-x", (event.clientX - rect.left) + "px");
725
+ canvasWrap.style.setProperty("--inpaint-brush-cursor-y", (event.clientY - rect.top) + "px");
726
+ brushCursor.setAttribute("data-visible", "true");
727
+ }
728
+ function hideBrushCursor(){ if(brushCursor) brushCursor.setAttribute("data-visible", "false"); }
729
+ function getPoint(event){
730
+ var rect = canvas.getBoundingClientRect();
731
+ return { x: Math.max(0, Math.min(1, (event.clientX - rect.left) / (rect.width || 1))), y: Math.max(0, Math.min(1, (event.clientY - rect.top) / (rect.height || 1))) };
732
+ }
733
+ var VISIBILITY_WINDOW = 0.4; // seconds either side of a mask's moment
734
+ function drawStrokes(strokes, color, alpha){
735
+ var ctx = canvas.getContext("2d"); if(!ctx) return;
736
+ ctx.lineCap = "round"; ctx.lineJoin = "round"; ctx.globalAlpha = alpha; ctx.strokeStyle = color;
737
+ for(var s = 0; s < strokes.length; s++){
738
+ var stroke = strokes[s]; if(!stroke.points || !stroke.points.length) continue;
739
+ ctx.lineWidth = stroke.size; ctx.beginPath();
740
+ ctx.moveTo(stroke.points[0].x * canvas.width, stroke.points[0].y * canvas.height);
741
+ if(stroke.points.length === 1) ctx.lineTo(stroke.points[0].x * canvas.width + 0.01, stroke.points[0].y * canvas.height + 0.01);
742
+ else for(var i = 1; i < stroke.points.length; i++) ctx.lineTo(stroke.points[i].x * canvas.width, stroke.points[i].y * canvas.height);
743
+ ctx.stroke();
744
+ }
745
+ ctx.globalAlpha = 1;
746
+ }
747
+ function redraw(){
748
+ var ctx = canvas.getContext("2d"); if(!ctx) return;
749
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
750
+ var t = videoEl.currentTime || 0;
751
+ for(var i = 0; i < masks.length; i++){
752
+ var m = masks[i]; if(!m.strokes.length) continue;
753
+ var isActive = m.id === activeMaskId;
754
+ var inWindow = Math.abs(m.timeSec - t) <= VISIBILITY_WINDOW;
755
+ if(isActive || inWindow) drawStrokes(m.strokes, m.color, isActive ? 0.9 : 0.55);
756
+ }
757
+ }
758
+ function beginStroke(event){
759
+ if(!sourceVideoUrl || submitBtn.disabled) return;
760
+ event.preventDefault();
761
+ if(!videoEl.paused) videoEl.pause();
762
+ var m = activeMask();
763
+ if(!m) m = addMask(videoEl.currentTime || 0);
764
+ try { canvas.setPointerCapture(event.pointerId); } catch(_){}
765
+ isPainting = true;
766
+ var stroke = { size: Number(brushInput ? brushInput.value : 70) || 70, points: [getPoint(event)] };
767
+ m.strokes.push(stroke); m.redo = []; redraw(); syncUndoRedo();
768
+ }
769
+ function continueStroke(event){
770
+ updateBrushCursorPos(event);
771
+ if(!isPainting) return; event.preventDefault();
772
+ var m = activeMask(); if(!m || !m.strokes.length) return;
773
+ m.strokes[m.strokes.length - 1].points.push(getPoint(event)); redraw();
774
+ }
775
+ function endStroke(event){ if(!isPainting) return; isPainting = false; try { canvas.releasePointerCapture(event.pointerId); } catch(_){} }
776
+
777
+ // ===== source video =====
778
+ function clearSourceObjectUrl(){ if(sourceObjectUrl){ try { URL.revokeObjectURL(sourceObjectUrl); } catch(_){} sourceObjectUrl = ""; } }
779
+ function sourceNameFromUrl(value){ try { var p = new URL(value, window.location.href).pathname; return decodeURIComponent((p.split("/").filter(Boolean).pop()) || "") || "Source video"; } catch(_){ return "Source video"; } }
780
+ var crossOriginRetried = false;
781
+ function loadSourceUrl(value, name){
782
+ if(!value) return;
783
+ var url; try { url = absoluteUrl(value); } catch(_){ setStatus("That video URL is invalid.", "error"); return; }
784
+ clearSourceObjectUrl();
785
+ sourceVideoUrl = url; sourceVideoName = name || sourceNameFromUrl(url);
786
+ emptyState.hidden = true; if(player) player.hidden = false; outputPanel.hidden = true;
787
+ crossOriginRetried = false;
788
+ // A cross-origin source is opened with crossorigin=anonymous so "Use current"
789
+ // frame-capture can read pixels when the host sends CORS headers; if the load
790
+ // errors (no CORS), we retry once WITHOUT it so playback still works (frame
791
+ // capture then falls back to a manual upload).
792
+ var sameOrigin = false; try { sameOrigin = new URL(url).origin === window.location.origin; } catch(_){}
793
+ if(sameOrigin) videoEl.removeAttribute("crossorigin"); else videoEl.setAttribute("crossorigin", "anonymous");
794
+ videoEl.addEventListener("loadedmetadata", function(){ syncCanvasSize(); if(durationEl) durationEl.textContent = "/ " + formatTimecode(videoEl.duration || 0); redraw(); }, { once: true });
795
+ videoEl.onerror = function(){ if(!crossOriginRetried && videoEl.getAttribute("crossorigin")){ crossOriginRetried = true; videoEl.removeAttribute("crossorigin"); videoEl.src = url; videoEl.load(); } };
796
+ videoEl.src = url;
797
+ videoEl.load();
798
+ setStatus("Video ready. Scrub the timeline and paint timed masks.", "success");
799
+ }
800
+ function loadSourceFile(file){
801
+ if(!file) return;
802
+ clearSourceObjectUrl();
803
+ sourceObjectUrl = URL.createObjectURL(file);
804
+ sourceVideoUrl = sourceObjectUrl; sourceVideoName = file.name || "Uploaded video";
805
+ emptyState.hidden = true; if(player) player.hidden = false; outputPanel.hidden = true;
806
+ videoEl.removeAttribute("crossorigin"); // local blob doesn't need CORS and avoids taint
807
+ videoEl.src = sourceObjectUrl;
808
+ videoEl.addEventListener("loadedmetadata", function(){ syncCanvasSize(); if(durationEl) durationEl.textContent = "/ " + formatTimecode(videoEl.duration || 0); redraw(); }, { once: true });
809
+ videoEl.load();
810
+ setStatus("Video loaded. Scrub the timeline and paint timed masks.", "success");
811
+ }
812
+
813
+ // ===== start/end frame slots =====
814
+ var pendingFrameSlot = null;
815
+ function setFrame(which, url, name){
816
+ frames[which] = { url: url, name: name || "" };
817
+ var img = document.querySelector('[data-ipv-frame-img="' + which + '"]');
818
+ var ph = document.querySelector('[data-ipv-frame-ph="' + which + '"]');
819
+ if(img){ img.src = url; img.hidden = false; }
820
+ if(ph) ph.style.display = "none";
821
+ }
822
+ function captureCurrentFrame(which){
823
+ if(!sourceVideoUrl){ setStatus("Load a video first, then grab a frame.", "error"); return; }
824
+ var vw = videoEl.videoWidth, vh = videoEl.videoHeight;
825
+ if(!vw || !vh){ setStatus("The video isn't ready yet — try again in a moment.", "error"); return; }
826
+ var off = document.createElement("canvas"); off.width = vw; off.height = vh;
827
+ var ctx = off.getContext("2d"); if(!ctx) return;
828
+ try { ctx.drawImage(videoEl, 0, 0, vw, vh); } catch(err){ setStatus("Couldn't read this frame (the video blocks cross-origin capture). Upload a frame instead.", "error"); return; }
829
+ setStatus("Saving " + which + " frame\\u2026", "");
830
+ off.toBlob(function(blob){
831
+ if(!blob){ setStatus("Couldn't capture that frame — upload a frame instead.", "error"); return; }
832
+ uploadTemporaryBlob(blob, which + "-frame-" + Date.now() + ".png", "image/png").then(function(uploaded){
833
+ setFrame(which, absoluteUrl(uploaded), which + " frame"); setStatus((which === "first" ? "Start" : "End") + " frame set from the current playhead.", "success");
834
+ }).catch(function(err){ setStatus(err && err.message ? err.message : "Couldn't save the frame.", "error"); });
835
+ }, "image/png");
836
+ }
837
+ function uploadFrameFile(which, file){
838
+ if(!file) return;
839
+ setStatus("Uploading " + which + " frame\\u2026", "");
840
+ uploadTemporaryBlob(file, file.name || (which + "-frame.png"), file.type || "image/png").then(function(uploaded){
841
+ setFrame(which, absoluteUrl(uploaded), file.name || (which + " frame")); setStatus((which === "first" ? "Start" : "End") + " frame set.", "success");
842
+ }).catch(function(err){ setStatus(err && err.message ? err.message : "Couldn't upload the frame.", "error"); });
843
+ }
844
+
845
+ // ===== attachments (global prompt refs) =====
846
+ function renderAttachChips(){
847
+ if(!attachChips) return; attachChips.innerHTML = "";
848
+ attachments.forEach(function(att){
849
+ var chip = document.createElement("span"); chip.className = "editor-inpaint-attachment-chip";
850
+ var img = document.createElement("img"); img.src = att.url; img.alt = ""; chip.appendChild(img);
851
+ var label = document.createElement("span"); label.textContent = att.fileName || "reference"; chip.appendChild(label);
852
+ var x = document.createElement("button"); x.type = "button"; x.textContent = "\\u00d7"; x.setAttribute("aria-label", "Remove reference"); x.addEventListener("click", function(){ attachments = attachments.filter(function(a){ return a.id !== att.id; }); renderAttachChips(); });
853
+ chip.appendChild(x); attachChips.appendChild(chip);
854
+ });
855
+ }
856
+ function addAttachmentFiles(files){
857
+ var selected = Array.prototype.slice.call(files || []); if(!selected.length) return;
858
+ if(attachments.length + selected.length > maxAttachments){ setStatus("Use up to " + maxAttachments + " reference images.", "error"); return; }
859
+ setStatus("Uploading reference image\\u2026", "");
860
+ var chain = Promise.resolve();
861
+ selected.forEach(function(file){
862
+ chain = chain.then(function(){
863
+ if(!allowedAttachTypes[file.type]) throw new Error("Reference attachments must be JPEG, PNG, or WebP.");
864
+ return uploadTemporaryBlob(file, file.name || "reference.png", file.type || "image/png").then(function(uploaded){
865
+ attachments.push({ id: uid("att_"), fileName: file.name || "reference.png", url: absoluteUrl(uploaded) });
866
+ });
867
+ });
868
+ });
869
+ chain.then(function(){ renderAttachChips(); setStatus("Reference image ready.", "success"); }).catch(function(err){ setStatus(err && err.message ? err.message : "Unable to upload reference image.", "error"); });
870
+ }
871
+
872
+ // ===== instruction compiler =====
873
+ function regionLabel(mask){
874
+ if(!mask.strokes.length) return "";
875
+ var minX = 1, minY = 1, maxX = 0, maxY = 0, any = false;
876
+ mask.strokes.forEach(function(s){ s.points.forEach(function(p){ any = true; if(p.x < minX) minX = p.x; if(p.y < minY) minY = p.y; if(p.x > maxX) maxX = p.x; if(p.y > maxY) maxY = p.y; }); });
877
+ if(!any) return "";
878
+ var cx = (minX + maxX) / 2, cy = (minY + maxY) / 2;
879
+ var h = cx < 0.34 ? "left" : (cx > 0.66 ? "right" : "center");
880
+ var v = cy < 0.34 ? "top" : (cy > 0.66 ? "bottom" : "middle");
881
+ if(v === "middle" && h === "center") return "center";
882
+ if(v === "middle") return h;
883
+ if(h === "center") return v;
884
+ return v + "-" + h;
885
+ }
886
+ function buildVideoInstruction(){
887
+ var whole = globalPrompt ? globalPrompt.value.trim() : "";
888
+ var timed = masks.map(function(m){ return { time: m.timeSec, region: regionLabel(m), prompt: String(m.prompt || "").trim() }; }).filter(function(x){ return x.prompt; });
889
+ if(!whole && !timed.length) return "";
890
+ var attNotes = attachments.map(function(a, i){ return "reference image " + (i + 1) + " (" + (a.fileName || "attachment") + ")"; });
891
+ var lines = [
892
+ "VIDEO EDITING BRIEF:",
893
+ "You are producing an edited version of an existing clip. Preserve the overall subject, framing, style, pacing, and everything not explicitly changed below; apply only the described edits.",
894
+ whole ? ("Whole clip: " + whole + ".") : "",
895
+ timed.length ? ("Timed region edits: " + timed.map(function(x){ return "at " + formatTimecode(x.time) + (x.region ? (" in the " + x.region + " of the frame") : "") + ", " + x.prompt; }).join("; ") + ".") : "",
896
+ "Match the surrounding lighting, motion, grain, and perspective so each edit blends seamlessly across the given timecodes.",
897
+ attNotes.length ? ("Reference images: " + attNotes.join("; ") + ". Use each only as visual guidance.") : ""
898
+ ];
899
+ return lines.filter(Boolean).join(" ");
900
+ }
901
+ function getAspectRatio(){
902
+ var w = videoEl.videoWidth || 0, h = videoEl.videoHeight || 0;
903
+ if(!w || !h){
904
+ // fall back to a frame image's ratio isn't trivial; default to 16:9
905
+ return "16:9";
906
+ }
907
+ var r = w / h;
908
+ if(Math.abs(r - 1) < 0.08) return "1:1";
909
+ if(r > 1) return r >= 1.55 ? "16:9" : "4:3";
910
+ return r <= 0.65 ? "9:16" : "3:4";
911
+ }
912
+
913
+ // ===== output extraction / history =====
914
+ function extractVideoUrl(payload){
915
+ var result = (payload && payload.result) || {};
916
+ var out = result.output || {};
917
+ var candidate = (out.video && out.video.file_url) || out.primary_file_url || (result.video && result.video.file_url) || result.primary_file_url || (Array.isArray(out.files) ? out.files[0] : "") || (Array.isArray(result.files) ? result.files[0] : "");
918
+ return candidate || "";
919
+ }
920
+ function isPendingStatus(status){ return ["queued","running","waiting_for_provider","waiting_for_child","waiting_for_human"].indexOf(String(status || "")) >= 0; }
921
+ function pendingKey(jobId){ return "job:" + String(jobId || ""); }
922
+ function makePending(input){
923
+ var jobId = String((input && (input.jobId || input.job_id)) || ""); if(!jobId) return null;
924
+ return { key: pendingKey(jobId), kind: "pending", jobId: jobId, status: String((input && input.status) || "queued"), progress: Number((input && input.progress) || 0), createdAt: String((input && (input.createdAt || input.created_at)) || nowIso()) };
925
+ }
926
+ function upsertHistory(item){
927
+ if(!item || !item.key) return;
928
+ historyItems = historyItems.filter(function(e){ return e.key !== item.key && (!item.jobId || e.jobId !== item.jobId); });
929
+ historyItems.unshift(item);
930
+ historyItems.sort(function(a, b){ return Date.parse(b.createdAt || 0) - Date.parse(a.createdAt || 0); });
931
+ renderHistory();
932
+ }
933
+ function recordSourceHistoryItem(url, name){
934
+ if(!url) return; var abs; try { abs = absoluteUrl(url); } catch(_){ return; }
935
+ var key = "source:" + abs;
936
+ var item = null;
937
+ for(var i = 0; i < sourceHistoryItems.length; i++){ if(sourceHistoryItems[i].key === key){ item = sourceHistoryItems[i]; break; } }
938
+ if(!item){ item = { key: key, kind: "source", url: abs, name: name || sourceNameFromUrl(abs), createdAt: nowIso() }; sourceHistoryItems.push(item); }
939
+ upsertHistory(item);
940
+ }
941
+ function reinjectSources(){ sourceHistoryItems.forEach(function(item){ if(!historyItems.some(function(e){ return e.key === item.key; })) historyItems.push(item); }); historyItems.sort(function(a, b){ return Date.parse(b.createdAt || 0) - Date.parse(a.createdAt || 0); }); }
942
+ function videoDownloadName(prefix, createdAt){ var d = createdAt ? new Date(createdAt) : new Date(); return prefix + "-" + d.getTime() + ".mp4"; }
943
+ function setHistoryEmpty(message){ historyList.innerHTML = ""; var e = document.createElement("div"); e.className = "custom-inpaint-history-empty"; e.textContent = message; historyList.appendChild(e); }
944
+ function renderHistory(){
945
+ historyList.innerHTML = "";
946
+ var visible = historyItems.filter(function(item){ return !hiddenHistoryKeys[item.key]; });
947
+ if(!visible.length){ setHistoryEmpty("No generated videos yet."); return; }
948
+ visible.forEach(function(item){
949
+ var row = document.createElement("div"); row.className = "custom-inpaint-history-item"; row.dataset.kind = item.kind;
950
+ var thumb;
951
+ if(item.kind === "pending"){ thumb = document.createElement("div"); thumb.className = "custom-inpaint-history-generating"; thumb.setAttribute("role", "img"); thumb.setAttribute("aria-label", "Video generating"); }
952
+ else {
953
+ thumb = document.createElement("button"); thumb.type = "button"; thumb.className = "custom-inpaint-history-thumb-button"; thumb.dataset.historyPreview = item.key; thumb.setAttribute("aria-label", "Preview video");
954
+ var v = document.createElement("video"); v.className = "custom-inpaint-history-thumb"; v.src = item.url + "#t=0.1"; v.muted = true; v.preload = "metadata"; v.playsInline = true; thumb.appendChild(v);
955
+ var badge = document.createElement("span"); badge.className = "custom-inpaint-history-thumb-badge"; badge.innerHTML = PLAY_SVG; thumb.appendChild(badge);
956
+ }
957
+ var meta = document.createElement("div"); meta.className = "custom-inpaint-history-meta";
958
+ var title = document.createElement("div"); title.className = "custom-inpaint-history-title";
959
+ title.textContent = item.kind === "pending" ? "Generating video" : item.kind === "source" ? "Source video" : (item.createdAt ? new Date(item.createdAt).toLocaleString() : "Generated video");
960
+ meta.appendChild(title);
961
+ if(item.kind === "pending"){ var sub = document.createElement("div"); sub.className = "custom-inpaint-history-subtitle"; var pr = Number(item.progress || 0); sub.textContent = "Job " + (item.status || "queued") + (pr > 0 ? " \\u00b7 " + Math.round(pr * 100) + "%" : ""); meta.appendChild(sub); }
962
+ else if(item.kind === "source"){ var sub2 = document.createElement("div"); sub2.className = "custom-inpaint-history-subtitle"; sub2.textContent = "Where you started" + (item.createdAt ? " \\u00b7 " + new Date(item.createdAt).toLocaleString() : ""); meta.appendChild(sub2); }
963
+ var actions = document.createElement("div"); actions.className = "custom-inpaint-history-actions";
964
+ if(item.kind === "pending"){ var check = document.createElement("button"); check.type = "button"; check.className = "custom-inpaint-history-check"; check.dataset.historyCheck = item.jobId || ""; check.textContent = "Check Status"; actions.appendChild(check); }
965
+ else {
966
+ var dl = document.createElement("a"); dl.className = "custom-inpaint-history-icon"; dl.href = item.url; dl.target = "_blank"; dl.rel = "noopener noreferrer"; dl.download = videoDownloadName("video", item.createdAt); dl.title = "Download"; dl.innerHTML = ${JSON.stringify(DL_ICON)};
967
+ var cp = document.createElement("button"); cp.type = "button"; cp.className = "custom-inpaint-history-icon"; cp.dataset.historyCopy = item.key; cp.title = "Copy URL"; cp.innerHTML = ${JSON.stringify(COPY_ICON)};
968
+ var sv = document.createElement("button"); sv.type = "button"; sv.className = "custom-inpaint-history-icon"; sv.dataset.historySave = item.key; sv.title = "Save to Files"; sv.innerHTML = "<svg viewBox='0 0 20 20' fill='none' stroke='currentColor' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round' aria-hidden='true'><path d='M5 4h8l2 2v10H5z'></path><path d='M8 4v4h4V4'></path><path d='M8 12h4'></path></svg>";
969
+ actions.appendChild(dl); actions.appendChild(cp); actions.appendChild(sv);
970
+ }
971
+ var rm = document.createElement("button"); rm.type = "button"; rm.className = "custom-inpaint-history-icon"; rm.dataset.historyRemove = item.key; rm.title = "Remove"; rm.innerHTML = ${JSON.stringify(CLOSE_ICON)}; actions.appendChild(rm);
972
+ meta.appendChild(actions);
973
+ var confirm = document.createElement("div"); confirm.className = "custom-inpaint-history-popconfirm"; confirm.hidden = true; confirm.dataset.historyConfirm = item.key;
974
+ var ctext = document.createElement("p"); ctext.textContent = "Remove from this history strip?";
975
+ var cacts = document.createElement("div"); cacts.className = "custom-inpaint-history-popconfirm-actions";
976
+ var cancel = document.createElement("button"); cancel.type = "button"; cancel.dataset.historyCancel = item.key; cancel.textContent = "Cancel";
977
+ var confirmRemove = document.createElement("button"); confirmRemove.type = "button"; confirmRemove.dataset.historyConfirmRemove = item.key; confirmRemove.dataset.danger = "true"; confirmRemove.textContent = "Remove";
978
+ cacts.appendChild(cancel); cacts.appendChild(confirmRemove); confirm.appendChild(ctext); confirm.appendChild(cacts);
979
+ row.appendChild(thumb); row.appendChild(meta); row.appendChild(confirm); historyList.appendChild(row);
980
+ });
981
+ }
982
+ function historyEntryUrls(entry){
983
+ var urls = (entry && entry.outputMediaUrls) || [];
984
+ return urls.filter(function(u){ return /\\.(mp4|webm|mov|m4v)(\\?|#|$)/i.test(String(u || "")); });
985
+ }
986
+ function isVideoEntry(entry){
987
+ var tid = String((entry && entry.linkedJob && (entry.linkedJob.template_id || entry.linkedJob.templateId)) || "").toLowerCase();
988
+ var path = String((entry && entry.path) || "").toLowerCase();
989
+ return tid === "primitive:video_generate" || path.indexOf("/primitives/videos/generate") >= 0 || path.indexOf("/primitives/video_generate") >= 0;
990
+ }
991
+ function extractHistoryItems(entries){
992
+ var items = []; var seen = {};
993
+ (entries || []).forEach(function(entry){
994
+ if(!isVideoEntry(entry)) return;
995
+ var linked = entry.linkedJob || null;
996
+ if(linked && isPendingStatus(linked.status)){
997
+ var p = makePending({ job_id: linked.job_id, status: linked.status, progress: linked.progress, created_at: linked.created_at || entry.createdAt });
998
+ if(p && !seen[p.key]){ seen[p.key] = 1; items.push(p); } return;
999
+ }
1000
+ historyEntryUrls(entry).forEach(function(raw, index){
1001
+ var url; try { url = absoluteUrl(raw); } catch(_){ return; }
1002
+ var key = String(entry.jobId || entry.job_id || entry.id || "video") + ":" + index + ":" + url;
1003
+ if(seen[key]) return; seen[key] = 1;
1004
+ items.push({ key: key, kind: "output", jobId: String(entry.jobId || entry.job_id || (entry.linkedJob && entry.linkedJob.job_id) || ""), url: url, createdAt: String(entry.createdAt || nowIso()) });
1005
+ });
1006
+ });
1007
+ return items;
1008
+ }
1009
+ function loadHistory(){
1010
+ var tracer = resolveTracer();
1011
+ if(!tracer){ historyItems = []; reinjectSources(); renderHistory(); return; }
1012
+ if(historyRefresh) historyRefresh.disabled = true;
1013
+ var endpoint = new URL("/job-runs/history", window.location.origin);
1014
+ endpoint.searchParams.set("limit", "120"); endpoint.searchParams.set("template_id", "primitive:video_generate"); endpoint.searchParams.set("tracer", tracer);
1015
+ fetch(endpoint, { credentials: "same-origin" }).then(function(r){ return parseJsonResponse(r).then(function(p){ return { ok: r.ok, p: p }; }); }).then(function(res){
1016
+ if(!res.ok || (res.p && res.p.error)) throw new Error((res.p && res.p.error) || "Unable to load history.");
1017
+ historyItems = extractHistoryItems(res.p.entries); reinjectSources(); renderHistory();
1018
+ }).catch(function(err){ setHistoryEmpty(err && err.message ? err.message : "Unable to load history."); }).then(function(){ if(historyRefresh) historyRefresh.disabled = false; });
1019
+ }
1020
+ function applyCompletedJob(payload){
1021
+ var out = extractVideoUrl(payload); if(!out) return "";
1022
+ var url = absoluteUrl(out); var jobId = String((payload && (payload.job_id || payload.jobId)) || "");
1023
+ resultVideo.src = url; outputPanel.hidden = false;
1024
+ upsertHistory({ key: String(jobId || "video") + ":0:" + url, kind: "output", jobId: jobId, url: url, createdAt: String((payload && (payload.completed_at || payload.updated_at)) || nowIso()) });
1025
+ return url;
1026
+ }
1027
+
1028
+ // ===== job status / polling =====
1029
+ function checkJobStatus(jobId, button){
1030
+ if(!jobId) return;
1031
+ if(button){ button.disabled = true; button.textContent = "Checking"; }
1032
+ fetch("/api/v1/primitives/video_generate/jobs/" + encodeURIComponent(jobId), { credentials: "same-origin" }).then(function(r){ return parseJsonResponse(r).then(function(p){ return { ok: r.ok, p: p }; }); }).then(function(res){
1033
+ var payload = res.p; if(!res.ok) throw new Error(payload.error || "Unable to check job.");
1034
+ if(payload.status === "failed" || payload.status === "cancelled") throw new Error((payload.error && payload.error.message) || ("Video job " + payload.status + "."));
1035
+ var out = extractVideoUrl(payload);
1036
+ if(payload.status === "succeeded" || payload.status === "complete" || out){ if(!applyCompletedJob(payload)) throw new Error("Completed without an output URL."); setStatus("Video ready.", "success"); return; }
1037
+ upsertHistory(makePending(payload)); setStatus("Video job " + jobId + " is " + (payload.status || "running") + ".", "");
1038
+ }).catch(function(err){ setStatus(err && err.message ? err.message : "Unable to check job.", "error"); }).then(function(){ if(button){ button.disabled = false; button.textContent = "Check Status"; } });
1039
+ }
1040
+ function pollJob(jobId){
1041
+ var attempt = 0;
1042
+ function step(){
1043
+ return fetch("/api/v1/primitives/video_generate/jobs/" + encodeURIComponent(jobId), { credentials: "same-origin" }).then(function(r){ return parseJsonResponse(r).then(function(p){ return { ok: r.ok, p: p }; }); }).then(function(res){
1044
+ var payload = res.p; if(!res.ok) throw new Error(payload.error || "Unable to poll job.");
1045
+ if(payload.status === "failed" || payload.status === "cancelled") throw new Error((payload.error && payload.error.message) || ("Video job " + payload.status + "."));
1046
+ var out = extractVideoUrl(payload);
1047
+ if(payload.status === "succeeded" || payload.status === "complete" || out){ if(!out) throw new Error("Completed without an output URL."); applyCompletedJob(payload); setStatus("Video ready.", "success"); return out; }
1048
+ upsertHistory(makePending(payload)); setStatus("Video job " + jobId + " is " + (payload.status || "running") + " (this can take a few minutes)\\u2026", "");
1049
+ attempt++; if(attempt >= 120){ setStatus("Still running \\u2014 use Check Status in History.", ""); return ""; }
1050
+ return new Promise(function(res2){ setTimeout(res2, attempt < 8 ? 2000 : 4000); }).then(step);
1051
+ });
1052
+ }
1053
+ return step();
1054
+ }
1055
+
1056
+ function frameImagesPayload(){
1057
+ var arr = [];
1058
+ if(frames.first && frames.first.url) arr.push({ image_url: frames.first.url, frame_type: "first_frame" });
1059
+ if(frames.last && frames.last.url) arr.push({ image_url: frames.last.url, frame_type: "last_frame" });
1060
+ return arr;
1061
+ }
1062
+ function setSubmitLoading(loading){ submitBtn.setAttribute("data-loading", loading ? "true" : "false"); submitBtn.disabled = loading; if(submitLabel) submitLabel.textContent = loading ? "Generating\\u2026" : "Generate video"; }
1063
+
1064
+ function submitGenerate(){
1065
+ if(submitBtn.disabled) return;
1066
+ var instruction = buildVideoInstruction();
1067
+ if(!instruction){ setStatus("Add a video prompt or a mask instruction first.", "error"); return; }
1068
+ var frameImgs = frameImagesPayload();
1069
+ setSubmitLoading(true);
1070
+ var seed = (globalPrompt && globalPrompt.value.trim()) || (masks.find(function(m){ return String(m.prompt || "").trim(); }) || {}).prompt || "video-edit";
1071
+ var tracer = tracerAutoPending ? nameTracerFromSeed(seed) : getTracerValue();
1072
+ var payload = { prompt: instruction, output_format: "mp4", aspect_ratio: getAspectRatio() };
1073
+ if(frameImgs.length) payload.frame_images = frameImgs;
1074
+ if(providerSelect && providerSelect.value) payload.provider = providerSelect.value;
1075
+ if(durationSelect && durationSelect.value) payload.duration = Number(durationSelect.value);
1076
+ if(audioToggle && audioToggle.checked) payload.generate_audio = true;
1077
+ setStatus("Creating video job\\u2026", "");
1078
+ fetch("/api/v1/primitives/videos/generate", { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json" }, body: JSON.stringify({ tracer: tracer, payload: payload }) })
1079
+ .then(function(r){ return parseJsonResponse(r).then(function(p){ return { ok: r.ok, p: p }; }); })
1080
+ .then(function(res){ if(!res.ok || !res.p.job_id) throw new Error(res.p.error || "Unable to create video job."); setStatus("Video job " + res.p.job_id + " started \\u2014 generating\\u2026", "success"); upsertHistory(makePending(res.p)); return pollJob(res.p.job_id); })
1081
+ .then(function(){ loadHistory(); loadSessions(); })
1082
+ .catch(function(err){ setStatus(err && err.message ? err.message : "Unable to create video.", "error"); })
1083
+ .then(function(){ setSubmitLoading(false); });
1084
+ }
1085
+
1086
+ // ===== lightbox =====
1087
+ function openVideoPreview(url, label, downloadName){
1088
+ if(!lightbox || !lightboxVideo) return;
1089
+ lightboxVideo.src = url; if(lightboxTitle) lightboxTitle.textContent = label || "Video output";
1090
+ if(lightboxDownload){ lightboxDownload.href = url; lightboxDownload.setAttribute("download", downloadName || "video.mp4"); }
1091
+ lightbox.hidden = false; lightbox.setAttribute("data-open", "true");
1092
+ }
1093
+ function closeVideoPreview(){ if(!lightbox) return; lightbox.setAttribute("data-open", "false"); lightbox.hidden = true; if(lightboxVideo){ try { lightboxVideo.pause(); } catch(_){} lightboxVideo.removeAttribute("src"); lightboxVideo.load(); } }
1094
+
1095
+ // ===== settings / sessions =====
1096
+ function openSettings(){ if(settingsModal) settingsModal.hidden = false; }
1097
+ function closeSettings(){ if(settingsModal) settingsModal.hidden = true; }
1098
+ function openSessions(){ if(sessionsPanel) sessionsPanel.classList.add("is-open"); if(sessionsBackdrop) sessionsBackdrop.hidden = false; loadSessions(); }
1099
+ function closeSessions(){ if(sessionsPanel) sessionsPanel.classList.remove("is-open"); if(sessionsBackdrop) sessionsBackdrop.hidden = true; }
1100
+ function relTimeShort(value){ var then = Date.parse(value || 0); if(!then) return ""; var secs = Math.max(0, Math.floor((Date.now() - then) / 1000)); if(secs < 60) return "just now"; var m = Math.floor(secs / 60); if(m < 60) return m + "m ago"; var h = Math.floor(m / 60); if(h < 24) return h + "h ago"; var d = Math.floor(h / 24); if(d < 7) return d + "d ago"; return new Date(then).toLocaleDateString(); }
1101
+ var sessionsLoading = false;
1102
+ function loadSessions(){
1103
+ if(!sessionsList || sessionsLoading) return; sessionsLoading = true;
1104
+ var endpoint = new URL("/job-runs/history", window.location.origin); endpoint.searchParams.set("limit", "500");
1105
+ fetch(endpoint, { credentials: "same-origin" }).then(function(r){ return parseJsonResponse(r).then(function(p){ return { ok: r.ok, p: p }; }); }).then(function(res){
1106
+ if(!res.ok || (res.p && res.p.error)) throw new Error((res.p && res.p.error) || "Unable to load sessions.");
1107
+ var groups = {}; var order = [];
1108
+ (res.p.entries || []).forEach(function(entry){
1109
+ if(!isVideoEntry(entry)) return; var tracer = String(entry.tracer || "").trim(); if(!tracer) return;
1110
+ var urls = historyEntryUrls(entry);
1111
+ if(!groups[tracer]){ groups[tracer] = { tracer: tracer, thumb: "", count: 0, createdAt: entry.createdAt || "" }; order.push(tracer); }
1112
+ var g = groups[tracer]; if(!g.thumb && urls[0]){ try { g.thumb = absoluteUrl(urls[0]); } catch(_){} } g.count += urls.length;
1113
+ if(Date.parse(entry.createdAt || 0) > Date.parse(g.createdAt || 0)) g.createdAt = entry.createdAt;
1114
+ });
1115
+ var list = order.map(function(t){ return groups[t]; }).sort(function(a, b){ return Date.parse(b.createdAt || 0) - Date.parse(a.createdAt || 0); });
1116
+ renderSessions(list);
1117
+ }).catch(function(err){ renderSessions([], err && err.message ? err.message : "Unable to load sessions."); }).then(function(){ sessionsLoading = false; });
1118
+ }
1119
+ function renderSessions(groups, message){
1120
+ if(!sessionsList) return; sessionsList.innerHTML = "";
1121
+ if(message){ var n = document.createElement("div"); n.className = "rk-ip-sessions-empty"; n.textContent = message; sessionsList.appendChild(n); return; }
1122
+ if(!groups.length){ var e = document.createElement("div"); e.className = "rk-ip-sessions-empty"; e.textContent = "No past sessions yet. Generate a video to start one."; sessionsList.appendChild(e); return; }
1123
+ var active = resolveTracer();
1124
+ groups.forEach(function(group){
1125
+ var row = document.createElement("button"); row.type = "button"; row.className = "rk-ip-session" + (group.tracer === active ? " is-active" : ""); row.dataset.sessionTracer = group.tracer;
1126
+ if(group.thumb){ var v = document.createElement("video"); v.className = "rk-ip-session-thumb"; v.src = group.thumb + "#t=0.1"; v.muted = true; v.preload = "metadata"; row.appendChild(v); }
1127
+ else { var ph = document.createElement("span"); ph.className = "rk-ip-session-thumb is-empty"; ph.innerHTML = PLAY_SVG; row.appendChild(ph); }
1128
+ var meta = document.createElement("div"); meta.className = "rk-ip-session-meta";
1129
+ var title = document.createElement("div"); title.className = "rk-ip-session-title"; title.textContent = group.tracer;
1130
+ var sub = document.createElement("div"); sub.className = "rk-ip-session-sub"; var when = relTimeShort(group.createdAt);
1131
+ sub.textContent = (group.count ? group.count + " video" + (group.count === 1 ? "" : "s") : "session") + (when ? " \\u00b7 " + when : "");
1132
+ meta.appendChild(title); meta.appendChild(sub); row.appendChild(meta); sessionsList.appendChild(row);
1133
+ });
1134
+ }
1135
+ function switchToTracer(tracer){
1136
+ var value = String(tracer || "").trim(); if(!value) return;
1137
+ sessionTracer = value; if(tracerInput) tracerInput.value = value; tracerAutoPending = false; closeSessions(); loadHistory(); loadSessions();
1138
+ setStatus("Opened session " + value + ".", "");
1139
+ }
1140
+
1141
+ // ===== reset =====
1142
+ function resetWorkspace(){
1143
+ clearSourceObjectUrl(); sourceVideoUrl = ""; sourceVideoName = "";
1144
+ try { videoEl.pause(); } catch(_){} videoEl.removeAttribute("src"); videoEl.load();
1145
+ masks = []; activeMaskId = ""; frames = { first: null, last: null };
1146
+ ["first","last"].forEach(function(w){ var img = document.querySelector('[data-ipv-frame-img="' + w + '"]'); var ph = document.querySelector('[data-ipv-frame-ph="' + w + '"]'); if(img){ img.hidden = true; img.removeAttribute("src"); } if(ph) ph.style.display = ""; });
1147
+ if(globalPrompt) globalPrompt.value = ""; attachments = []; renderAttachChips();
1148
+ outputPanel.hidden = true; resultVideo.removeAttribute("src");
1149
+ emptyState.hidden = false; if(player) player.hidden = true;
1150
+ canvas.width = 1; canvas.height = 1; renderMasks();
1151
+ if(scrub) scrub.value = "0"; if(timecodeInput) timecodeInput.value = "00:00:000"; if(durationEl) durationEl.textContent = "/ 00:00:000";
1152
+ }
1153
+ function startNew(){
1154
+ sessionTracer = ""; tracerAutoPending = true; if(tracerInput) tracerInput.value = "";
1155
+ clearUrlParams(); resetWorkspace(); loadHistory(); loadSessions();
1156
+ setStatus("Started a new session.", "");
1157
+ }
1158
+ function clearUrlParams(){
1159
+ var url = new URL(window.location.href);
1160
+ ["tracer","tracer_id","trace","source_video_url","video_url","source_url","source_video_name","video_name","file_name","source_image_url"].forEach(function(k){ url.searchParams.delete(k); });
1161
+ window.history.replaceState({}, "", url.toString());
1162
+ }
1163
+ function firstParam(names){ var params = new URLSearchParams(window.location.search); for(var i = 0; i < names.length; i++){ var v = params.get(names[i]); if(v && v.trim()) return v.trim(); } return ""; }
1164
+ function initFromUrl(){
1165
+ var urlTracer = firstParam(["tracer","tracer_id","trace"]);
1166
+ if(urlTracer){ sessionTracer = urlTracer; if(tracerInput) tracerInput.value = urlTracer; tracerAutoPending = false; }
1167
+ var src = firstParam(["source_video_url","video_url","source_url"]);
1168
+ if(src){ var nm = firstParam(["source_video_name","video_name","file_name"]); loadSourceUrl(src, nm); recordSourceHistoryItem(src, nm); }
1169
+ }
1170
+
1171
+ // ===== timeline sync =====
1172
+ function syncTimeline(){
1173
+ var d = videoEl.duration || 0; var t = videoEl.currentTime || 0;
1174
+ if(scrub && document.activeElement !== scrub){ scrub.value = String(d > 0 ? Math.round((t / d) * 1000) : 0); }
1175
+ if(timecodeInput && document.activeElement !== timecodeInput){ timecodeInput.value = formatTimecode(t); }
1176
+ }
1177
+ function tick(){ if(!videoEl.paused && !videoEl.ended){ syncTimeline(); redraw(); requestAnimationFrame(tick); } }
1178
+
1179
+ // ===== wire events =====
1180
+ if(brushInput){ brushInput.addEventListener("input", function(){ if(brushLabel) brushLabel.textContent = brushInput.value; updateBrushCursorSize(); }); }
1181
+ if(uploadBtn && fileInput){ uploadBtn.addEventListener("click", function(){ fileInput.click(); }); fileInput.addEventListener("change", function(){ if(fileInput.files && fileInput.files[0]) loadSourceFile(fileInput.files[0]); }); }
1182
+ if(startNewBtn) startNewBtn.addEventListener("click", startNew);
1183
+ if(addMaskBtn) addMaskBtn.addEventListener("click", function(){ addMask(videoEl.currentTime || 0, colorPicker ? colorPicker.value : null); });
1184
+ if(colorPicker) colorPicker.addEventListener("input", function(){ var m = activeMask(); if(m){ m.color = normalizeHex(colorPicker.value); renderMasks(); redraw(); } });
1185
+ if(undoBtn) undoBtn.addEventListener("click", function(){ var m = activeMask(); if(m && m.strokes.length){ m.redo.push(m.strokes.pop()); redraw(); syncUndoRedo(); } });
1186
+ if(redoBtn) redoBtn.addEventListener("click", function(){ var m = activeMask(); if(m && m.redo.length){ m.strokes.push(m.redo.pop()); redraw(); syncUndoRedo(); } });
1187
+ if(clearBtn) clearBtn.addEventListener("click", function(){ var m = activeMask(); if(m){ m.redo = m.strokes.concat(m.redo); m.strokes = []; redraw(); syncUndoRedo(); } });
1188
+ if(attachBtn && attachInput){ attachBtn.addEventListener("click", function(){ attachInput.click(); }); attachInput.addEventListener("change", function(){ addAttachmentFiles(attachInput.files); attachInput.value = ""; }); }
1189
+ if(submitBtn) submitBtn.addEventListener("click", submitGenerate);
1190
+
1191
+ masksList.addEventListener("click", function(event){
1192
+ var t = event.target; if(!(t instanceof Element)) return;
1193
+ var sel = t.closest("[data-mask-select]"); if(sel){ selectMask(sel.getAttribute("data-mask-select")); return; }
1194
+ var seek = t.closest("[data-mask-seek]"); if(seek){ var id = seek.getAttribute("data-mask-seek"); var m = null; masks.forEach(function(x){ if(x.id === id) m = x; }); if(m){ selectMask(id); if(videoEl.duration) videoEl.currentTime = Math.min(videoEl.duration, m.timeSec); syncTimeline(); redraw(); } return; }
1195
+ var setT = t.closest("[data-mask-settime]"); if(setT){ var id2 = setT.getAttribute("data-mask-settime"); masks.forEach(function(x){ if(x.id === id2) x.timeSec = videoEl.currentTime || 0; }); renderMasks(); redraw(); return; }
1196
+ var rm = t.closest("[data-mask-remove]"); if(rm){ removeMask(rm.getAttribute("data-mask-remove")); return; }
1197
+ });
1198
+ masksList.addEventListener("input", function(event){
1199
+ var t = event.target; if(!(t instanceof Element)) return;
1200
+ var ta = t.closest("[data-mask-prompt]"); if(ta){ var id = ta.getAttribute("data-mask-prompt"); masks.forEach(function(m){ if(m.id === id) m.prompt = ta.value; }); }
1201
+ });
1202
+ masksList.addEventListener("focusin", function(event){
1203
+ var t = event.target; if(!(t instanceof Element)) return;
1204
+ var ta = t.closest("[data-mask-prompt]"); if(ta){ var id = ta.getAttribute("data-mask-prompt"); if(id !== activeMaskId){ activeMaskId = id; var m = activeMask(); if(m && colorPicker) colorPicker.value = m.color; masksList.querySelectorAll(".rk-ipv-mask").forEach(function(r){ r.setAttribute("data-active", r.dataset.maskId === id ? "true" : "false"); }); redraw(); } }
1205
+ });
1206
+
1207
+ // frame slot buttons
1208
+ document.querySelectorAll("[data-ipv-frame-upload]").forEach(function(btn){ btn.addEventListener("click", function(){ pendingFrameSlot = btn.getAttribute("data-ipv-frame-upload"); if(frameInput) frameInput.click(); }); });
1209
+ document.querySelectorAll("[data-ipv-frame-capture]").forEach(function(btn){ btn.addEventListener("click", function(){ captureCurrentFrame(btn.getAttribute("data-ipv-frame-capture")); }); });
1210
+ document.querySelectorAll("[data-ipv-frame-open]").forEach(function(btn){ btn.addEventListener("click", function(){ var w = btn.getAttribute("data-ipv-frame-open"); if(frames[w] && frames[w].url){ window.open(frames[w].url, "_blank", "noopener,noreferrer"); } }); });
1211
+ if(frameInput) frameInput.addEventListener("change", function(){ if(pendingFrameSlot && frameInput.files && frameInput.files[0]) uploadFrameFile(pendingFrameSlot, frameInput.files[0]); pendingFrameSlot = null; frameInput.value = ""; });
1212
+
1213
+ // canvas painting
1214
+ canvas.addEventListener("pointerdown", beginStroke);
1215
+ canvas.addEventListener("pointermove", continueStroke);
1216
+ canvas.addEventListener("pointerup", endStroke);
1217
+ canvas.addEventListener("pointercancel", endStroke);
1218
+ canvas.addEventListener("pointerleave", function(){ hideBrushCursor(); });
1219
+ canvas.addEventListener("pointerenter", updateBrushCursorPos);
1220
+
1221
+ // player controls
1222
+ if(playpause) playpause.addEventListener("click", function(){ if(videoEl.paused){ videoEl.play(); } else { videoEl.pause(); } });
1223
+ videoEl.addEventListener("play", function(){ if(playpause) playpause.innerHTML = PAUSE_SVG; requestAnimationFrame(tick); });
1224
+ videoEl.addEventListener("pause", function(){ if(playpause) playpause.innerHTML = PLAY_SVG; syncTimeline(); redraw(); });
1225
+ videoEl.addEventListener("ended", function(){ if(playpause) playpause.innerHTML = PLAY_SVG; });
1226
+ videoEl.addEventListener("timeupdate", function(){ if(videoEl.paused){ syncTimeline(); redraw(); } });
1227
+ videoEl.addEventListener("seeked", function(){ syncTimeline(); redraw(); });
1228
+ if(scrub) scrub.addEventListener("input", function(){ var d = videoEl.duration || 0; if(d > 0){ videoEl.currentTime = (Number(scrub.value) / 1000) * d; } });
1229
+ if(timecodeInput){
1230
+ timecodeInput.addEventListener("change", function(){ var t = parseTimecode(timecodeInput.value); if(!isNaN(t) && videoEl.duration){ videoEl.currentTime = Math.max(0, Math.min(videoEl.duration, t)); } });
1231
+ timecodeInput.addEventListener("keydown", function(e){ if(e.key === "Enter"){ e.preventDefault(); timecodeInput.blur(); } });
1232
+ }
1233
+ if(timecodeCopy) timecodeCopy.addEventListener("click", function(){ copyText(timecodeInput ? timecodeInput.value : formatTimecode(videoEl.currentTime || 0)).then(function(ok){ timecodeCopy.setAttribute("title", ok ? "Copied" : "Copy failed"); setTimeout(function(){ timecodeCopy.setAttribute("title", "Copy timecode"); }, 1200); }); });
1234
+
1235
+ // history interactions
1236
+ historyList.addEventListener("click", function(event){
1237
+ var t = event.target; if(!(t instanceof Element)) return;
1238
+ var prev = t.closest("[data-history-preview]"); if(prev){ var key = prev.getAttribute("data-history-preview"); var it = historyItems.find(function(e){ return e.key === key; }); if(it) openVideoPreview(it.url, it.createdAt ? new Date(it.createdAt).toLocaleString() : "Video output", videoDownloadName("video-history", it.createdAt)); return; }
1239
+ var cp = t.closest("[data-history-copy]"); if(cp){ var k2 = cp.getAttribute("data-history-copy"); var it2 = historyItems.find(function(e){ return e.key === k2; }); if(it2) copyText(it2.url).then(function(ok){ cp.setAttribute("title", ok ? "Copied" : "Copy failed"); setTimeout(function(){ cp.setAttribute("title", "Copy URL"); }, 1200); }); return; }
1240
+ var sv = t.closest("[data-history-save]"); if(sv){ var k3 = sv.getAttribute("data-history-save"); var it3 = historyItems.find(function(e){ return e.key === k3; }); if(it3 && it3.url) saveMediaToFiles(it3.url, videoDownloadName("video-output", it3.createdAt)); return; }
1241
+ var ck = t.closest("[data-history-check]"); if(ck){ checkJobStatus(ck.getAttribute("data-history-check"), ck); return; }
1242
+ var rm = t.closest("[data-history-remove]"); if(rm){ var key4 = rm.getAttribute("data-history-remove"); var confirm = historyList.querySelector('[data-history-confirm="' + (window.CSS && CSS.escape ? CSS.escape(key4) : key4) + '"]'); if(confirm){ confirm.hidden = !confirm.hidden; } return; }
1243
+ var cancel = t.closest("[data-history-cancel]"); if(cancel){ var c = cancel.closest("[data-history-confirm]"); if(c) c.hidden = true; return; }
1244
+ var conf = t.closest("[data-history-confirm-remove]"); if(conf){ var key5 = conf.getAttribute("data-history-confirm-remove"); if(key5){ hiddenHistoryKeys[key5] = 1; renderHistory(); } return; }
1245
+ });
1246
+ if(historyRefresh) historyRefresh.addEventListener("click", loadHistory);
1247
+
1248
+ // save result
1249
+ if(saveBtn) saveBtn.addEventListener("click", function(){ if(resultVideo.src) saveMediaToFiles(resultVideo.currentSrc || resultVideo.src, videoDownloadName("video-result")); });
1250
+
1251
+ // settings + sessions
1252
+ if(settingsBtn) settingsBtn.addEventListener("click", openSettings);
1253
+ if(settingsClose) settingsClose.addEventListener("click", closeSettings);
1254
+ if(settingsModal) settingsModal.addEventListener("click", function(e){ if(e.target === settingsModal) closeSettings(); });
1255
+ if(sessionsToggle) sessionsToggle.addEventListener("click", function(){ if(sessionsPanel && sessionsPanel.classList.contains("is-open")) closeSessions(); else openSessions(); });
1256
+ if(sessionsClose) sessionsClose.addEventListener("click", closeSessions);
1257
+ if(sessionsBackdrop) sessionsBackdrop.addEventListener("click", closeSessions);
1258
+ if(sessionsList) sessionsList.addEventListener("click", function(event){ var t = event.target; if(!(t instanceof Element)) return; var row = t.closest("[data-session-tracer]"); if(row) switchToTracer(row.getAttribute("data-session-tracer")); });
1259
+
1260
+ // lightbox close
1261
+ if(lightboxClose) lightboxClose.addEventListener("click", closeVideoPreview);
1262
+ if(lightbox) lightbox.addEventListener("click", function(e){ if(e.target === lightbox) closeVideoPreview(); });
1263
+ if(lightboxCopy) lightboxCopy.addEventListener("click", function(){ if(lightboxVideo && lightboxVideo.src) copyText(lightboxVideo.src).then(function(ok){ lightboxCopy.setAttribute("title", ok ? "Copied" : "Copy failed"); setTimeout(function(){ lightboxCopy.setAttribute("title", "Copy URL"); }, 1200); }); });
1264
+
1265
+ window.addEventListener("resize", syncCanvasSize);
1266
+ window.addEventListener("keydown", function(e){ if(e.key !== "Escape") return; if(lightbox && lightbox.getAttribute("data-open") === "true"){ closeVideoPreview(); return; } if(settingsModal && !settingsModal.hidden){ closeSettings(); return; } if(sessionsPanel && sessionsPanel.classList.contains("is-open")){ closeSessions(); return; } });
1267
+
1268
+ // drag-drop video onto empty state
1269
+ if(emptyState){
1270
+ ["dragenter","dragover"].forEach(function(ev){ emptyState.addEventListener(ev, function(e){ e.preventDefault(); }); });
1271
+ emptyState.addEventListener("drop", function(e){ e.preventDefault(); var f = e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0]; if(f && /^video\\//.test(f.type)) loadSourceFile(f); });
1272
+ }
1273
+
1274
+ // ===== text-to-video composer =====
1275
+ var genPrompt = document.getElementById("rk-ipv-genprompt");
1276
+ var genClip = document.getElementById("rk-ipv-genclip");
1277
+ var genSend = document.getElementById("rk-ipv-gensend");
1278
+ var genRefsEl = document.getElementById("rk-ipv-genrefs");
1279
+ var genStatusEl = document.getElementById("rk-ipv-genstatus");
1280
+ var genWrap = document.getElementById("rk-ipv-gen");
1281
+ var GEN_SEND_HTML = genSend ? genSend.innerHTML : "";
1282
+ var genRefs = []; var genBusy = false; var genFileModal = null;
1283
+ function setGenStatus(m, t){ if(!genStatusEl) return; genStatusEl.textContent = m || ""; genStatusEl.setAttribute("data-tone", t || ""); }
1284
+ function syncGenSend(){ if(genPrompt) genPrompt.disabled = genBusy; if(genClip) genClip.disabled = genBusy; if(!genSend) return; if(genBusy){ genSend.classList.add("is-busy"); genSend.innerHTML = "<span class='rk-ip-gen-spin'></span>"; } else { genSend.classList.remove("is-busy"); genSend.innerHTML = GEN_SEND_HTML; } genSend.disabled = genBusy || !(genPrompt && genPrompt.value.trim()); }
1285
+ function renderGenRefs(){ if(!genRefsEl) return; genRefsEl.innerHTML = ""; if(!genRefs.length){ genRefsEl.hidden = true; return; } genRefsEl.hidden = false; genRefs.forEach(function(url, idx){ var chip = document.createElement("span"); chip.className = "rk-ip-gen-ref"; var img = document.createElement("img"); img.src = url; img.alt = ""; chip.appendChild(img); var label = document.createElement("span"); label.textContent = sourceNameFromUrl(url); chip.appendChild(label); var x = document.createElement("button"); x.type = "button"; x.textContent = "\\u00d7"; x.addEventListener("click", function(){ genRefs.splice(idx, 1); renderGenRefs(); }); chip.appendChild(x); genRefsEl.appendChild(chip); }); }
1286
+ function closeGenFilePanel(){ if(genFileModal){ try { genFileModal._handle && genFileModal._handle.destroy && genFileModal._handle.destroy(); } catch(_){} genFileModal.remove(); genFileModal = null; document.removeEventListener("keydown", onGenFileEsc, true); } }
1287
+ function onGenFileEsc(e){ if(e.key === "Escape") closeGenFilePanel(); }
1288
+ function openGenFilePanel(){
1289
+ var dir = window.__vidfarmDirectory; if(!dir || !dir.pick){ setGenStatus("File browser isn't available right now.", "error"); return; }
1290
+ dir.pick({ select: "file", roots: ["files","temp","raws"], title: "Attach reference images", hint: "Pick one or more images to guide the generation.", confirmLabel: "Attach", multiple: true,
1291
+ onPick: function(picked){ var arr = Array.isArray(picked) ? picked : [picked]; arr.forEach(function(f){ var url = f && (f.url || f.viewUrl); if(url){ try { genRefs.push(absoluteUrl(url)); } catch(_){} } }); renderGenRefs(); } });
1292
+ }
1293
+ function pollGenerateJob(jobId){
1294
+ var attempt = 0;
1295
+ function step(){
1296
+ return fetch("/api/v1/primitives/video_generate/jobs/" + encodeURIComponent(jobId), { credentials: "same-origin" }).then(function(r){ return parseJsonResponse(r).then(function(p){ return { ok: r.ok, p: p }; }); }).then(function(res){
1297
+ var payload = res.p; if(!res.ok) throw new Error(payload.error || "Unable to poll job.");
1298
+ if(payload.status === "failed" || payload.status === "cancelled") throw new Error((payload.error && payload.error.message) || ("Video job " + payload.status + "."));
1299
+ var out = extractVideoUrl(payload);
1300
+ if(payload.status === "succeeded" || payload.status === "complete" || out){ if(!out) throw new Error("Completed without an output URL."); return absoluteUrl(out); }
1301
+ setGenStatus("Generating video (" + (payload.status || "running") + ")\\u2026 this can take a few minutes", "");
1302
+ attempt++; if(attempt >= 120){ setGenStatus("Still running \\u2014 check History shortly.", ""); return ""; }
1303
+ return new Promise(function(res2){ setTimeout(res2, attempt < 8 ? 2000 : 4000); }).then(step);
1304
+ });
1305
+ }
1306
+ return step();
1307
+ }
1308
+ function submitGenerateNew(){
1309
+ var p = (genPrompt && genPrompt.value || "").trim();
1310
+ if(!p){ setGenStatus("Describe the video to generate.", "error"); if(genPrompt) genPrompt.focus(); return; }
1311
+ if(genBusy) return; genBusy = true; syncGenSend(); if(genWrap) genWrap.classList.add("is-busy");
1312
+ setGenStatus("Creating video\\u2026", "");
1313
+ var payload = { prompt: p, output_format: "mp4" };
1314
+ var frameImgs = frameImagesPayload(); if(frameImgs.length) payload.frame_images = frameImgs;
1315
+ if(genRefs.length) payload.input_references = genRefs.slice();
1316
+ if(providerSelect && providerSelect.value) payload.provider = providerSelect.value;
1317
+ if(durationSelect && durationSelect.value) payload.duration = Number(durationSelect.value);
1318
+ if(audioToggle && audioToggle.checked) payload.generate_audio = true;
1319
+ var genTracer = tracerAutoPending ? nameTracerFromSeed(p) : getTracerValue();
1320
+ fetch("/api/v1/primitives/videos/generate", { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json" }, body: JSON.stringify({ tracer: genTracer, payload: payload }) })
1321
+ .then(function(r){ return parseJsonResponse(r).then(function(j){ return { ok: r.ok, j: j }; }); })
1322
+ .then(function(res){ if(!res.ok || !res.j.job_id) throw new Error(res.j.error || "Unable to start video generation."); upsertHistory(makePending(res.j)); return pollGenerateJob(res.j.job_id); })
1323
+ .then(function(out){ if(out){ loadSourceUrl(out, "Generated video"); recordSourceHistoryItem(out, "Generated video"); setGenStatus("Video generated. Scrub and paint timed masks to refine.", "success"); setStatus("Generated video loaded.", "success"); loadHistory(); } })
1324
+ .catch(function(err){ var msg = (err && err.message) ? err.message : "Unable to generate video."; if(/401|403|sign in|unauth/i.test(msg)) msg = "Sign in to generate videos."; setGenStatus(msg, "error"); })
1325
+ .then(function(){ genBusy = false; if(genWrap) genWrap.classList.remove("is-busy"); syncGenSend(); });
1326
+ }
1327
+ if(genPrompt){ genPrompt.addEventListener("input", syncGenSend); genPrompt.addEventListener("keydown", function(e){ if(e.key === "Enter" && !e.shiftKey){ e.preventDefault(); submitGenerateNew(); } }); }
1328
+ if(genClip) genClip.addEventListener("click", openGenFilePanel);
1329
+ if(genSend) genSend.addEventListener("click", submitGenerateNew);
1330
+ renderGenRefs(); syncGenSend();
1331
+
1332
+ // ===== boot =====
1333
+ renderMasks();
1334
+ loadProviders();
1335
+ initFromUrl();
1336
+ loadHistory();
1337
+ })();
1338
+ `;
1339
+ //# sourceMappingURL=inpaint-video-page.js.map