@mevdragon/vidfarm-devcli 0.19.1 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/.agents/skills/music/SKILL.md +416 -0
  2. package/.agents/skills/music/references/api_reference.md +519 -0
  3. package/.agents/skills/music/references/installation.md +65 -0
  4. package/.agents/skills/text-to-speech/SKILL.md +226 -0
  5. package/.agents/skills/text-to-speech/references/installation.md +90 -0
  6. package/.agents/skills/text-to-speech/references/streaming.md +307 -0
  7. package/.agents/skills/text-to-speech/references/voice-settings.md +115 -0
  8. package/.agents/skills/vidfarm-media/SKILL.md +25 -11
  9. package/.agents/skills/vidfarm-media/references/tts.md +41 -3
  10. package/SKILL.director.md +31 -13
  11. package/SKILL.platform.md +2 -2
  12. package/demo/dist/app.css +1 -1
  13. package/demo/dist/app.js +75 -75
  14. package/dist/src/app.js +129 -41
  15. package/dist/src/cli.js +162 -5
  16. package/dist/src/config.js +12 -0
  17. package/dist/src/devcli/clips.js +7 -2
  18. package/dist/src/domain.js +3 -0
  19. package/dist/src/editor-chat.js +5 -1
  20. package/dist/src/frontend/file-directory.js +41 -18
  21. package/dist/src/primitive-context.js +14 -0
  22. package/dist/src/primitive-registry.js +140 -18
  23. package/dist/src/reskin/chat-page.js +1 -1
  24. package/dist/src/reskin/inpaint-clipper-page.js +446 -205
  25. package/dist/src/reskin/library-page.js +7 -1
  26. package/dist/src/reskin/portfolio-page.js +9 -9
  27. package/dist/src/reskin/settings-page.js +4 -4
  28. package/dist/src/reskin/theme.js +8 -4
  29. package/dist/src/services/billing.js +5 -0
  30. package/dist/src/services/clip-curation/ffmpeg.js +48 -0
  31. package/dist/src/services/clip-curation/hunt.js +2 -0
  32. package/dist/src/services/clip-curation/index.js +1 -1
  33. package/dist/src/services/clip-curation/scan.js +29 -16
  34. package/dist/src/services/elevenlabs.js +222 -0
  35. package/dist/src/services/providers.js +216 -2
  36. package/dist/src/services/swipe-customize.js +5 -2
  37. package/package.json +1 -1
  38. package/public/assets/file-directory-app.js +3 -2
@@ -1,39 +1,47 @@
1
1
  // /tools/clipper — the third creative-tool twin. Where /tools/image does masked
2
2
  // image edits and /tools/video does time-scoped video edits, /tools/clipper cuts
3
- // EXACT subranges out of a source video into the raws library. It plays the
4
- // source straight from its resolved RapidAPI/direct URL (nothing is uploaded to
5
- // preview), lets the user pick an in/out point down to sub-second precision
6
- // (HH:MM:SS:NNNN), shows a live estimate of the clip length, and on "Save clip"
7
- // POSTs to /raws/clip-range which trims just that window and lands one raw. It is
8
- // a MULTI-CLIP surface the source stays loaded so you can grab clip after clip,
9
- // each grouped under the session's tracer/folder and listed in "Clips this
10
- // session", with a past-sessions slide-in grouped by tracer (like /tools/video).
3
+ // EXACT subranges out of a source video (or audio) into the raws library. It
4
+ // plays the source straight from its resolved RapidAPI/direct URL (nothing is
5
+ // uploaded to preview), lets the user pick an in/out point down to sub-second
6
+ // precision (HH:MM:SS:NNNN) on ONE unified timeline shared with the playhead,
7
+ // shows a live estimate of the clip length, and on "Clip Media" POSTs to
8
+ // /raws/clip-range which trims just that window and lands one raw. It is a
9
+ // MULTI-CLIP surface the source stays loaded so you can grab clip after clip,
10
+ // each grouped under the session's tracer/folder and listed in the right-hand
11
+ // history card, with a past-sessions slide-in grouped by tracer (like /tools/*).
12
+ //
13
+ // Layout: one big LEFT column (source URL + compact ingest above the preview;
14
+ // video preview; unified in/out timeline + <advanced> below) and a RIGHT history
15
+ // card styled to match /tools/image. Works for audio sources too.
11
16
  //
12
17
  // Data flow:
13
18
  // • load source → POST /raws/resolve-source { source_url } → { preview_url, ... }
14
- // • save clip → POST /raws/clip-range { preview_url, start_sec, end_sec, tracer, folder_path, name }
19
+ // • clip media → POST /raws/clip-range { preview_url, start_sec, end_sec, tracer, folder_path, name, notes, audio_only }
15
20
  // • session list → GET /raws/feed?folder=<slug> (this session's clips)
16
21
  // • past sessions → GET /raws/feed?limit=200 grouped by folder_path/tracer
17
22
  import { reskinDocument } from "./document.js";
18
23
  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>`;
24
+ const UPLOAD_ICON = `<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" 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
25
  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>`;
21
26
  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>`;
27
+ 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>`;
22
28
  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>`;
23
29
  const CLIP_ICON = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M8.1 8.1 20 18M8.1 15.9 20 6"/></svg>`;
24
30
  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>`;
25
31
  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>`;
26
32
  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>`;
27
- const FOLDER_ICON = `<svg width="15" height="15" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M2.5 5.5A1.5 1.5 0 0 1 4 4h3.2l1.4 1.6H16a1.5 1.5 0 0 1 1.5 1.5v7A1.5 1.5 0 0 1 16 15.6H4a1.5 1.5 0 0 1-1.5-1.5Z"/></svg>`;
33
+ const FOLDER_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="M2.5 5.5A1.5 1.5 0 0 1 4 4h3.2l1.4 1.6H16a1.5 1.5 0 0 1 1.5 1.5v7A1.5 1.5 0 0 1 16 15.6H4a1.5 1.5 0 0 1-1.5-1.5Z"/></svg>`;
34
+ const CHEVRON_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="m6 8 4 4 4-4"/></svg>`;
35
+ const AUDIO_ICON = `<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 10v4"/><path d="M8 6v12"/><path d="M12 3v18"/><path d="M16 7v10"/><path d="M20 10v4"/></svg>`;
28
36
  export function renderReskinInpaintClipper(input = {}) {
29
37
  const boot = { accountId: input.accountId || "", isLoggedIn: Boolean(input.isLoggedIn) };
30
38
  const body = `
31
39
  <main class="rk-container-wide rk-page rk-inpaint-page rk-clp-page">
32
- <div class="rk-page-head rk-inpaint-head">
40
+ <div class="rk-page-head rk-inpaint-head rk-clp-head">
33
41
  <div class="rk-inpaint-head-text">
34
42
  ${renderInpaintModeTabs("clipper")}
35
- <h1 class="rk-h1">Clip a Video</h1>
36
- <p class="rk-sub">Paste (or upload) a video, scrub to the exact moment, and set a precise <strong>in</strong> and <strong>out</strong> point (HH:MM:SS:NNNN). Only the selected subrange is saved to your raws — grab as many clips as you like from the same source.</p>
43
+ <h1 class="rk-h1">Clip Media</h1>
44
+ <p class="rk-sub">Paste (or upload) a video or audio source, scrub to the exact moment, and drag the <strong>in</strong> and <strong>out</strong> edges on the timeline (HH:MM:SS:NNNN). Only the selected subrange is saved to your raws — grab as many clips as you like from the same source.</p>
37
45
  </div>
38
46
  <div class="rk-inpaint-toolbar">
39
47
  <button type="button" class="rk-btn rk-btn-ghost rk-btn-sm rk-inpaint-startnew" data-clp-start-new>${PLUS_ICON} Start New</button>
@@ -42,115 +50,137 @@ export function renderReskinInpaintClipper(input = {}) {
42
50
  </div>
43
51
  </div>
44
52
 
45
- <div class="rk-inpaint-workbench rk-clp-workbench">
46
- <!-- LEFT: source + subrange controls -->
47
- <section class="rk-inpaint-panel" aria-label="Clip controls">
48
- <div class="rk-inpaint-block">
49
- <label class="rk-label" for="rkClpUrl">Source video URL</label>
53
+ <div class="rk-clp-workbench">
54
+ <!-- LEFT: source (top) → preview → timeline + advanced (bottom) -->
55
+ <section class="rk-clp-main" aria-label="Clip controls">
56
+ <!-- Source, above the preview -->
57
+ <div class="rk-clp-sourcebar">
58
+ <label class="rk-label" for="rkClpUrl">Source URL <span class="rk-hint">video or audio</span></label>
50
59
  <div class="rk-clp-urlrow">
51
- <input class="rk-input" id="rkClpUrl" data-clp-url-input type="url" placeholder="https://youtube.com/watch?v=… or https://…/video.mp4" autocomplete="off" spellcheck="false" />
52
- <button type="button" class="rk-btn rk-btn-gold rk-btn-sm" data-clp-url-load>Load</button>
53
- </div>
54
- <div class="rk-clp-ingest">
55
- <button type="button" class="rk-btn rk-btn-ghost rk-btn-sm" data-clp-upload>${UPLOAD_ICON}<span>Upload</span></button>
56
- <button type="button" class="rk-btn rk-btn-ghost rk-btn-sm" data-clp-attach>${FOLDER_ICON}<span>From Files</span></button>
60
+ <input class="rk-input" id="rkClpUrl" data-clp-url-input type="url" placeholder="https://youtube.com/watch?v=… or https://…/media.mp4" autocomplete="off" spellcheck="false" />
61
+ <button type="button" class="rk-btn rk-btn-gold rk-btn-sm rk-clp-loadbtn" data-clp-url-load>Load</button>
62
+ <span class="rk-clp-or">or</span>
63
+ <button type="button" class="rk-ip-iconbtn rk-clp-ingestbtn" data-clp-upload title="Upload from your computer" aria-label="Upload">${UPLOAD_ICON}</button>
64
+ <button type="button" class="rk-ip-iconbtn rk-clp-ingestbtn" data-clp-attach title="Attach from your Files" aria-label="From Files">${FOLDER_ICON}</button>
57
65
  </div>
58
- <input data-clp-file type="file" accept="video/mp4,video/quicktime,video/webm,video/x-matroska,video/*" hidden />
59
- <span class="rk-hint">TikTok, YouTube, Instagram, X, or a direct link — we play it straight from the source; nothing is saved until you clip.</span>
66
+ <input data-clp-file type="file" accept="video/*,audio/*" hidden />
67
+ <span class="rk-hint rk-clp-sourcehint">TikTok, YouTube, Instagram, X, or a direct link — we play it straight from the source; nothing is saved until you clip.</span>
60
68
  </div>
61
69
 
62
- <div class="rk-inpaint-block rk-clp-rangeblock" data-clp-range-controls hidden>
63
- <div class="rk-inpaint-brushrow">
64
- <label class="rk-label">Subrange</label>
65
- <button type="button" class="rk-btn rk-btn-ghost rk-btn-xs" data-clp-whole title="Select the whole video">Whole video</button>
70
+ <!-- Preview -->
71
+ <div class="rk-clp-stage" data-clp-stage>
72
+ <div class="rk-clp-empty" data-clp-empty>
73
+ <span class="rk-clp-empty-ic" aria-hidden="true">${CLIP_ICON}</span>
74
+ <p>Load media to start clipping.</p>
75
+ <span>Paste a URL and hit <strong>Load</strong>, or upload a file.</span>
66
76
  </div>
67
-
68
- <div class="rk-clp-inout">
69
- <div class="rk-clp-field">
70
- <div class="rk-clp-field-head">
71
- <span class="rk-clp-field-label">In</span>
72
- <button type="button" class="rk-clp-tinybtn" data-clp-set-start title="Set in-point to the playhead">Set to playhead</button>
77
+ <div class="rk-clp-player" data-clp-player hidden>
78
+ <div class="rk-clp-videowrap" data-clp-videowrap>
79
+ <video data-clp-video playsinline preload="auto"></video>
80
+ <div class="rk-clp-audioposter" data-clp-audio-poster hidden>
81
+ <span class="rk-clp-audioposter-ic" aria-hidden="true">${AUDIO_ICON}</span>
82
+ <span>Audio source</span>
73
83
  </div>
74
- <input class="rk-input rk-clp-tc" data-clp-start-input type="text" inputmode="numeric" placeholder="00:00:00:0000" value="00:00:00:0000" />
75
- <input class="rk-clp-slider" data-clp-start-slider type="range" min="0" max="1000" step="1" value="0" aria-label="In point" />
76
84
  </div>
77
- <div class="rk-clp-field">
78
- <div class="rk-clp-field-head">
79
- <span class="rk-clp-field-label">Out</span>
80
- <button type="button" class="rk-clp-tinybtn" data-clp-set-end title="Set out-point to the playhead">Set to playhead</button>
81
- </div>
82
- <input class="rk-input rk-clp-tc" data-clp-end-input type="text" inputmode="numeric" placeholder="00:00:00:0000" value="00:00:00:0000" />
83
- <input class="rk-clp-slider" data-clp-end-slider type="range" min="0" max="1000" step="1" value="1000" aria-label="Out point" />
85
+ </div>
86
+ </div>
87
+
88
+ <!-- Transport + unified timeline (only when a source is loaded) -->
89
+ <div class="rk-clp-controls" data-clp-controls hidden>
90
+ <div class="rk-clp-transport">
91
+ <button type="button" class="rk-ip-iconbtn rk-clp-playpause" data-clp-playpause aria-label="Play / pause">${PLAY_ICON}</button>
92
+ <input class="rk-clp-tc rk-clp-tc-now" data-clp-timecode type="text" inputmode="numeric" value="00:00:00:0000" aria-label="Current time" />
93
+ <span class="rk-clp-duration" data-clp-duration>/ 00:00:00:0000</span>
94
+ <div class="rk-clp-spacer"></div>
95
+ <button type="button" class="rk-btn rk-btn-ghost rk-btn-xs" data-clp-whole title="Select the whole source">Whole</button>
96
+ </div>
97
+
98
+ <!-- ONE timeline: playhead + draggable in/out edges + selected subrange -->
99
+ <div class="rk-clp-timeline" data-clp-timeline>
100
+ <div class="rk-clp-tl-track" data-clp-tl-track>
101
+ <div class="rk-clp-tl-sel" data-clp-tl-sel></div>
102
+ <div class="rk-clp-tl-playhead" data-clp-tl-playhead aria-hidden="true"></div>
103
+ <button type="button" class="rk-clp-tl-handle rk-clp-tl-handle-in" data-clp-tl-in aria-label="In point"><span></span></button>
104
+ <button type="button" class="rk-clp-tl-handle rk-clp-tl-handle-out" data-clp-tl-out aria-label="Out point"><span></span></button>
84
105
  </div>
85
106
  </div>
86
107
 
87
- <div class="rk-clp-estimate" data-clp-estimate>Clip length — <strong data-clp-length>0:00.0</strong></div>
108
+ <div class="rk-clp-inout">
109
+ <label class="rk-clp-field">
110
+ <span class="rk-clp-field-label">In</span>
111
+ <input class="rk-input rk-clp-tc" data-clp-start-input type="text" inputmode="numeric" value="00:00:00:0000" />
112
+ <button type="button" class="rk-clp-tinybtn" data-clp-set-start title="Set in-point to the playhead">Set to playhead</button>
113
+ </label>
114
+ <label class="rk-clp-field">
115
+ <span class="rk-clp-field-label">Out</span>
116
+ <input class="rk-input rk-clp-tc" data-clp-end-input type="text" inputmode="numeric" value="00:00:00:0000" />
117
+ <button type="button" class="rk-clp-tinybtn" data-clp-set-end title="Set out-point to the playhead">Set to playhead</button>
118
+ </label>
119
+ <div class="rk-clp-lenbox">
120
+ <span class="rk-clp-field-label">Length</span>
121
+ <strong data-clp-length>0:00.0</strong>
122
+ </div>
123
+ </div>
88
124
  <div class="rk-clp-rangenote" data-clp-range-note hidden></div>
89
- </div>
90
125
 
91
- <div class="rk-inpaint-block rk-clp-saveblock" data-clp-save-block hidden>
92
- <label class="rk-label" for="rkClpName">Clip name <span class="rk-hint">optional</span></label>
93
- <input class="rk-input" id="rkClpName" data-clp-name type="text" placeholder="hook-01" autocomplete="off" />
94
- <label class="rk-label" for="rkClpFolder" style="margin-top:10px">Save to raws folder <span class="rk-hint">optional</span></label>
95
- <input class="rk-input" id="rkClpFolder" data-clp-folder type="text" placeholder="defaults to the session tracer" autocomplete="off" spellcheck="false" />
126
+ <!-- Advanced (collapsed) -->
127
+ <div class="rk-clp-advanced" data-clp-advanced>
128
+ <button type="button" class="rk-clp-adv-toggle" data-clp-adv-toggle aria-expanded="false">
129
+ <span class="rk-clp-adv-chev" aria-hidden="true">${CHEVRON_ICON}</span>
130
+ <span>Advanced</span>
131
+ <span class="rk-hint rk-clp-adv-hint">name · notes · audio only</span>
132
+ </button>
133
+ <div class="rk-clp-adv-body" data-clp-adv-body hidden>
134
+ <label class="rk-label" for="rkClpName">Filename <span class="rk-hint">optional</span></label>
135
+ <input class="rk-input" id="rkClpName" data-clp-name type="text" placeholder="hook-01" autocomplete="off" />
136
+ <label class="rk-label" for="rkClpNotes" style="margin-top:11px">Notes <span class="rk-hint">helps you find this clip later by search</span></label>
137
+ <textarea class="rk-input rk-clp-notes" id="rkClpNotes" data-clp-notes rows="2" placeholder="What's in this clip / how you'll use it — indexed for search."></textarea>
138
+ <label class="rk-label" for="rkClpFolder" style="margin-top:11px">Save to raws folder <span class="rk-hint">optional</span></label>
139
+ <input class="rk-input" id="rkClpFolder" data-clp-folder type="text" placeholder="defaults to the session tracer" autocomplete="off" spellcheck="false" />
140
+ <label class="rk-clp-audio-toggle">
141
+ <input type="checkbox" data-clp-audio-only />
142
+ <span class="rk-clp-audio-text">
143
+ <strong>Clip audio only</strong>
144
+ <span class="rk-hint">Save just the sound (no video) — e.g. grab a track from a copyright-free sound video. Your choice is remembered.</span>
145
+ </span>
146
+ </label>
147
+ </div>
148
+ </div>
149
+
96
150
  <button type="button" class="rk-btn rk-btn-gold rk-clp-savebtn" data-clp-save>
97
- ${CLIP_ICON}<span data-clp-save-label>Save clip to raws</span>
151
+ ${CLIP_ICON}<span data-clp-save-label>Clip Media</span>
98
152
  </button>
99
153
  <div class="rk-clp-status" data-clp-status role="status"></div>
100
154
  </div>
101
155
  </section>
102
156
 
103
- <!-- CENTER: video preview + timeline -->
104
- <section class="rk-inpaint-stage rk-clp-stage" aria-label="Video preview">
105
- <div class="rk-clp-empty" data-clp-empty>
106
- <div class="rk-clp-empty-inner">
107
- ${CLIP_ICON}
108
- <p>Load a video to start clipping.</p>
109
- <span>Paste a URL and hit <strong>Load</strong>, or upload a file.</span>
157
+ <!-- RIGHT: this-session history card -->
158
+ <aside class="rk-clp-side" aria-label="Clips this session">
159
+ <div class="rk-clp-side-card">
160
+ <div class="rk-clp-side-head">
161
+ <h2 class="rk-inpaint-h2">This session</h2>
162
+ <button type="button" class="rk-ip-iconbtn rk-ip-iconbtn-sm" data-clp-session-refresh aria-label="Refresh" title="Refresh">${REFRESH_ICON}</button>
110
163
  </div>
111
- </div>
112
- <div class="rk-clp-player" data-clp-player hidden>
113
- <div class="rk-clp-videowrap">
114
- <video data-clp-video playsinline preload="auto"></video>
164
+ <div class="rk-clp-session-list" data-clp-session-list>
165
+ <div class="rk-clp-session-empty">No clips yet — set an in/out point and clip your first media.</div>
115
166
  </div>
116
- <div class="rk-clp-transport">
117
- <button type="button" class="rk-ip-iconbtn rk-clp-playpause" data-clp-playpause aria-label="Play / pause">${PLAY_ICON}</button>
118
- <div class="rk-clp-scrubwrap">
119
- <input class="rk-clp-scrub" data-clp-scrub type="range" min="0" max="1000" step="1" value="0" aria-label="Scrub" />
120
- <div class="rk-clp-rangebar" data-clp-rangebar aria-hidden="true"><span class="rk-clp-rangefill" data-clp-range-fill></span></div>
121
- </div>
122
- <div class="rk-clp-time">
123
- <input class="rk-clp-tc rk-clp-tc-now" data-clp-timecode type="text" inputmode="numeric" value="00:00:00:0000" aria-label="Current time" />
124
- <span class="rk-clp-duration" data-clp-duration>/ 00:00:00:0000</span>
125
- </div>
126
- </div>
127
- </div>
128
- </section>
129
-
130
- <!-- RIGHT: clips this session -->
131
- <aside class="rk-inpaint-side rk-clp-side" aria-label="Clips this session">
132
- <div class="rk-clp-side-head">
133
- <span class="rk-eyebrow">Clips this session</span>
134
- <button type="button" class="rk-ip-iconbtn rk-ip-iconbtn-sm" data-clp-session-refresh aria-label="Refresh" title="Refresh">${HISTORY_ICON}</button>
167
+ <a class="rk-clp-librarylink" href="/library/raws">Open your raws library →</a>
135
168
  </div>
136
- <div class="rk-clp-session-list" data-clp-session-list>
137
- <div class="rk-clp-session-empty">No clips yet — set an in/out point and save your first clip.</div>
138
- </div>
139
- <a class="rk-clp-librarylink" href="/library/raws">Open your raws library →</a>
140
169
  </aside>
141
170
  </div>
142
171
 
143
172
  <!-- Settings modal -->
144
173
  <div class="rk-ip-settingsmodal" data-clp-settings hidden>
145
- <div class="rk-ip-settingsmodal-backdrop" data-clp-settings-close></div>
146
- <div class="rk-ip-settingsmodal-card" role="dialog" aria-modal="true" aria-label="Clipper settings">
174
+ <div class="rk-ip-settingsmodal-panel" role="dialog" aria-modal="true" aria-label="Clipper settings">
147
175
  <div class="rk-ip-settingsmodal-head">
148
- <h2>Session settings</h2>
176
+ <span class="rk-ip-settingsmodal-title">Settings</span>
149
177
  <button type="button" class="rk-ip-iconbtn" data-clp-settings-close aria-label="Close">${CLOSE_ICON}</button>
150
178
  </div>
151
- <div class="rk-field">
152
- <label class="rk-label" for="rkClpTracer">Session name (tracer)</label>
153
- <input class="rk-input" id="rkClpTracer" data-clp-tracer type="text" placeholder="auto named from the source" autocomplete="off" spellcheck="false" />
179
+ <div class="rk-ip-settingsmodal-body">
180
+ <label class="rk-ip-setting">
181
+ <span class="rk-label">Session name <span class="rk-ip-setting-note">(tracer — groups this session's clips)</span></span>
182
+ <input class="rk-ip-input" id="rkClpTracer" data-clp-tracer type="text" placeholder="auto — named from the source" autocomplete="off" spellcheck="false" />
183
+ </label>
154
184
  <span class="rk-hint">Every clip you save in this session is grouped under this tracer — and lands in a matching <code>/raws</code> folder.</span>
155
185
  </div>
156
186
  </div>
@@ -159,7 +189,7 @@ export function renderReskinInpaintClipper(input = {}) {
159
189
  <!-- Past sessions slide-in -->
160
190
  <aside class="rk-ip-sessions rk-clp-sessions" data-clp-sessions aria-label="Past clipping sessions">
161
191
  <div class="rk-ip-sessions-head">
162
- <span class="rk-eyebrow">Past sessions</span>
192
+ <span class="rk-ip-sessions-title">Past sessions</span>
163
193
  <button type="button" class="rk-ip-iconbtn" data-clp-sessions-close aria-label="Close">${CLOSE_ICON}</button>
164
194
  </div>
165
195
  <div class="rk-ip-sessions-list" data-clp-sessions-list></div>
@@ -170,63 +200,160 @@ export function renderReskinInpaintClipper(input = {}) {
170
200
  </main>`;
171
201
  const pageCss = `
172
202
  ${INPAINT_TABS_CSS}
173
- .rk-clp-page{padding-bottom:40px}
174
- .rk-clp-workbench{display:grid;grid-template-columns:320px minmax(0,1fr) 300px;gap:18px;align-items:start}
175
- .rk-clp-urlrow{display:flex;gap:8px}
176
- .rk-clp-urlrow .rk-input{flex:1;min-width:0}
177
- .rk-clp-ingest{display:flex;gap:8px;margin-top:8px}
178
- .rk-clp-ingest .rk-btn{flex:1;gap:6px}
179
- .rk-clp-rangeblock .rk-clp-inout{display:flex;flex-direction:column;gap:14px;margin-top:6px}
180
- .rk-clp-field{display:flex;flex-direction:column;gap:6px}
181
- .rk-clp-field-head{display:flex;align-items:center;justify-content:space-between}
182
- .rk-clp-field-label{font-size:12px;font-weight:800;letter-spacing:.02em;color:var(--rk-ink)}
183
- .rk-clp-tinybtn{border:0;background:transparent;color:var(--rk-gold-ink,#9a6b12);font-size:11px;font-weight:700;cursor:pointer;padding:2px 4px;border-radius:6px}
184
- .rk-clp-tinybtn:hover{background:var(--rk-n-100)}
203
+ .rk-clp-page{padding-left:var(--rk-gutter);padding-right:var(--rk-gutter);padding-bottom:44px}
204
+
205
+ /* shared page-head toolbar (Start New + gear + history) — top-right, like /tools/image */
206
+ .rk-inpaint-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;flex-wrap:wrap}
207
+ .rk-inpaint-head-text{min-width:0;flex:1 1 340px}
208
+ .rk-inpaint-toolbar{flex:none;display:flex;align-items:center;gap:8px;padding-top:2px}
209
+ .rk-inpaint-startnew{gap:6px}
210
+ .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;margin:0}
211
+
212
+ /* shared icon buttons */
213
+ .rk-ip-iconbtn{display:grid;place-items:center;width:34px;height:34px;border-radius:var(--rk-r-lg);border:1px solid var(--rk-border);background:#fff;color:var(--rk-n-600);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)}
214
+ .rk-ip-iconbtn:hover{background:var(--rk-gold-tint);color:var(--rk-gold-700);border-color:var(--rk-gold-600)}
215
+ .rk-ip-iconbtn:disabled{opacity:.45;cursor:not-allowed;background:var(--rk-n-50);color:var(--rk-n-400);border-color:var(--rk-border)}
216
+ .rk-ip-iconbtn-sm{width:28px;height:28px}
217
+
218
+ /* workbench grid — one big left column + right history card */
219
+ .rk-clp-workbench{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:20px;align-items:start}
220
+ .rk-clp-main{background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);box-shadow:var(--rk-shadow-card);padding:18px;display:grid;gap:16px;align-content:start}
221
+
222
+ /* source bar (above the preview) */
223
+ .rk-clp-sourcebar{display:grid;gap:8px}
224
+ .rk-clp-urlrow{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
225
+ .rk-clp-urlrow .rk-input{flex:1 1 240px;min-width:0}
226
+ .rk-clp-loadbtn{flex:none}
227
+ .rk-clp-or{flex:none;font-size:11.5px;font-weight:700;letter-spacing:.03em;text-transform:uppercase;color:var(--rk-text-faint)}
228
+ .rk-clp-ingestbtn{flex:none}
229
+ .rk-clp-sourcehint{line-height:1.4}
230
+ .rk-clp-loadbtn[data-loading="true"]{opacity:.7;pointer-events:none}
231
+ .rk-clp-sourcebar.is-loading .rk-clp-ingestbtn,.rk-clp-sourcebar.is-loading .rk-input{opacity:.5;pointer-events:none}
232
+
233
+ /* preview stage */
234
+ .rk-clp-stage{min-height:300px;background:var(--rk-n-100);border:1px solid var(--rk-border);border-radius:var(--rk-r-2xl);display:flex;align-items:center;justify-content:center;overflow:hidden;position:relative}
235
+ .rk-clp-empty{padding:44px 24px;text-align:center;color:var(--rk-text-muted)}
236
+ .rk-clp-empty[hidden]{display:none}
237
+ .rk-clp-empty-ic{display:inline-grid;place-items:center;color:var(--rk-n-400);margin-bottom:10px}
238
+ .rk-clp-empty-ic svg{width:30px;height:30px}
239
+ .rk-clp-empty p{font-size:15px;font-weight:800;color:var(--rk-ink);margin:0 0 4px}
240
+ .rk-clp-empty span{font-size:12.5px}
241
+ .rk-clp-player{width:100%}
242
+ .rk-clp-player[hidden]{display:none}
243
+ .rk-clp-videowrap{width:100%;background:#000;display:flex;align-items:center;justify-content:center;position:relative;min-height:300px;max-height:58vh}
244
+ .rk-clp-videowrap video{width:100%;max-height:58vh;display:block}
245
+ .rk-clp-audioposter{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;color:#fff;background:linear-gradient(135deg,#2a2622,#1b1815)}
246
+ .rk-clp-audioposter[hidden]{display:none}
247
+ .rk-clp-audioposter-ic{display:grid;place-items:center;color:var(--rk-gold-500,#f0b429)}
248
+ .rk-clp-audioposter-ic svg{width:34px;height:34px}
249
+ .rk-clp-audioposter span{font-size:13px;font-weight:700;letter-spacing:.02em}
250
+
251
+ /* transport row */
252
+ .rk-clp-controls[hidden]{display:none}
253
+ .rk-clp-controls{display:grid;gap:13px}
254
+ .rk-clp-transport{display:flex;align-items:center;gap:10px}
255
+ .rk-clp-spacer{flex:1}
185
256
  .rk-clp-tc{font-variant-numeric:tabular-nums;letter-spacing:.02em;font-weight:700}
186
- .rk-clp-slider,.rk-clp-scrub{width:100%;accent-color:var(--rk-gold,#e2a63a)}
187
- .rk-clp-estimate{margin-top:12px;font-size:13px;color:var(--rk-text-muted)}
188
- .rk-clp-estimate strong{color:var(--rk-ink);font-variant-numeric:tabular-nums}
189
- .rk-clp-rangenote{margin-top:8px;font-size:12px;font-weight:600;color:#b42318}
190
- .rk-clp-savebtn{width:100%;justify-content:center;gap:8px;margin-top:12px}
257
+ .rk-clp-tc-now{width:132px;padding:6px 9px;font-size:12.5px}
258
+ .rk-clp-duration{font-variant-numeric:tabular-nums;white-space:nowrap;font-size:12.5px;color:var(--rk-text-muted)}
259
+ .rk-btn-xs{padding:4px 11px;font-size:11.5px}
260
+
261
+ /* unified timeline */
262
+ .rk-clp-timeline{padding:16px 12px 8px}
263
+ .rk-clp-tl-track{position:relative;height:12px;border-radius:999px;background:var(--rk-n-200,#e7e2da);cursor:pointer;touch-action:none}
264
+ .rk-clp-tl-sel{position:absolute;top:0;bottom:0;left:0;width:100%;background:var(--rk-gold-500,#f0b429);border-radius:999px;box-shadow:inset 0 0 0 1px var(--rk-gold-600,#dd9a1f)}
265
+ .rk-clp-tl-playhead{position:absolute;top:-4px;bottom:-4px;left:0;width:2px;background:var(--rk-ink);border-radius:2px;transform:translateX(-1px);pointer-events:none;z-index:3}
266
+ .rk-clp-tl-playhead::after{content:"";position:absolute;top:-3px;left:50%;width:9px;height:9px;background:var(--rk-ink);border:2px solid #fff;border-radius:50%;transform:translateX(-50%)}
267
+ .rk-clp-tl-handle{position:absolute;top:50%;width:16px;height:26px;padding:0;border:0;background:transparent;cursor:ew-resize;transform:translate(-50%,-50%);z-index:4;touch-action:none}
268
+ .rk-clp-tl-handle span{display:block;width:14px;height:26px;margin:0 auto;border-radius:5px;background:#fff;border:2px solid var(--rk-gold-600,#dd9a1f);box-shadow:var(--rk-shadow-sm),0 0 0 1px rgba(0,0,0,.04)}
269
+ .rk-clp-tl-handle:hover span,.rk-clp-tl-handle.is-drag span{border-color:var(--rk-gold-700);background:var(--rk-gold-tint)}
270
+ .rk-clp-tl-handle::before{content:"";position:absolute;top:50%;left:50%;width:2px;height:12px;background:var(--rk-gold-700,#c98a1a);border-radius:2px;transform:translate(-50%,-50%);opacity:.7}
271
+
272
+ /* in / out inputs */
273
+ .rk-clp-inout{display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap}
274
+ .rk-clp-field{display:flex;flex-direction:column;gap:5px;flex:1 1 150px;min-width:130px}
275
+ .rk-clp-field-label{font-size:11px;font-weight:800;letter-spacing:.04em;text-transform:uppercase;color:var(--rk-text-muted)}
276
+ .rk-clp-tinybtn{align-self:flex-start;border:0;background:transparent;color:var(--rk-gold-ink,#9a6b12);font-size:11px;font-weight:700;cursor:pointer;padding:1px 2px;border-radius:6px}
277
+ .rk-clp-tinybtn:hover{text-decoration:underline}
278
+ .rk-clp-lenbox{display:flex;flex-direction:column;gap:5px;flex:0 0 auto;padding-bottom:2px}
279
+ .rk-clp-lenbox strong{font-size:14px;color:var(--rk-ink);font-variant-numeric:tabular-nums}
280
+ .rk-clp-rangenote{font-size:12px;font-weight:600;color:#b42318}
281
+ .rk-clp-rangenote[hidden]{display:none}
282
+
283
+ /* advanced */
284
+ .rk-clp-advanced{border-top:1px solid var(--rk-border);padding-top:12px}
285
+ .rk-clp-adv-toggle{display:flex;align-items:center;gap:8px;width:100%;padding:2px;border:0;background:transparent;cursor:pointer;font:inherit;font-size:13px;font-weight:800;color:var(--rk-ink);text-align:left}
286
+ .rk-clp-adv-chev{display:grid;place-items:center;color:var(--rk-text-muted);transition:transform var(--rk-dur) var(--rk-ease)}
287
+ .rk-clp-adv-toggle[aria-expanded="true"] .rk-clp-adv-chev{transform:rotate(180deg)}
288
+ .rk-clp-adv-hint{font-weight:600;margin-left:auto}
289
+ .rk-clp-adv-body{margin-top:12px}
290
+ .rk-clp-adv-body[hidden]{display:none}
291
+ .rk-clp-notes{min-height:52px;resize:vertical;line-height:1.45}
292
+ .rk-clp-audio-toggle{display:flex;gap:9px;align-items:flex-start;margin-top:14px;cursor:pointer}
293
+ .rk-clp-audio-toggle input{margin-top:2px;flex:none;width:16px;height:16px;accent-color:var(--rk-gold-700);cursor:pointer}
294
+ .rk-clp-audio-text{display:flex;flex-direction:column;gap:2px;line-height:1.35}
295
+ .rk-clp-audio-text strong{font-size:13px;color:var(--rk-ink);font-weight:800}
296
+ .rk-clp-audio-text .rk-hint{font-size:11.5px}
297
+
298
+ /* clip button + status */
299
+ .rk-clp-savebtn{width:100%;justify-content:center;gap:8px;padding:13px}
191
300
  .rk-clp-savebtn[data-loading="true"]{opacity:.7;pointer-events:none}
192
- .rk-clp-status{margin-top:8px;font-size:12.5px;font-weight:600;min-height:16px}
301
+ .rk-clp-savebtn:disabled{opacity:.5;cursor:not-allowed}
302
+ .rk-clp-status{font-size:12.5px;font-weight:600;min-height:16px;line-height:1.5}
193
303
  .rk-clp-status[data-tone="error"]{color:#b42318}
194
- .rk-clp-status[data-tone="success"]{color:#15803d}
195
- .rk-clp-stage{min-height:360px;background:var(--rk-n-100);border:1px solid var(--rk-border);border-radius:var(--rk-radius-lg,18px);display:flex;align-items:center;justify-content:center;overflow:hidden;position:relative}
196
- .rk-clp-empty{padding:40px;text-align:center;color:var(--rk-text-muted)}
197
- .rk-clp-empty-inner svg{width:34px;height:34px;opacity:.5;margin-bottom:10px}
198
- .rk-clp-empty-inner p{font-size:15px;font-weight:700;color:var(--rk-ink);margin:0 0 4px}
199
- .rk-clp-empty-inner span{font-size:12.5px}
200
- .rk-clp-player{width:100%;display:flex;flex-direction:column;gap:10px;padding:12px}
201
- .rk-clp-videowrap{width:100%;background:#000;border-radius:12px;overflow:hidden;display:flex;align-items:center;justify-content:center;max-height:56vh}
202
- .rk-clp-videowrap video{width:100%;max-height:56vh;display:block}
203
- .rk-clp-transport{display:flex;align-items:center;gap:12px;padding:2px 4px}
204
- .rk-clp-playpause{flex:none}
205
- .rk-clp-scrubwrap{flex:1;min-width:0;position:relative;display:flex;flex-direction:column;gap:4px}
206
- .rk-clp-rangebar{position:relative;height:5px;border-radius:999px;background:var(--rk-n-200,#e7e2da);overflow:hidden}
207
- .rk-clp-rangefill{position:absolute;top:0;bottom:0;left:0;width:100%;background:var(--rk-gold,#e2a63a);border-radius:999px}
208
- .rk-clp-time{flex:none;display:flex;align-items:center;gap:6px;font-size:12px;color:var(--rk-text-muted)}
209
- .rk-clp-tc-now{width:118px;padding:5px 8px;font-size:12px}
210
- .rk-clp-duration{font-variant-numeric:tabular-nums;white-space:nowrap}
304
+ .rk-clp-status[data-tone="success"]{color:#1f7a44}
305
+
306
+ /* right: this-session history card (matches /tools/image) */
211
307
  .rk-clp-side{position:sticky;top:16px}
212
- .rk-clp-side-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
213
- .rk-ip-iconbtn-sm{width:28px;height:28px}
214
- .rk-clp-session-list{display:flex;flex-direction:column;gap:8px;max-height:60vh;overflow:auto}
215
- .rk-clp-session-empty{font-size:12.5px;color:var(--rk-text-muted);padding:10px 2px;line-height:1.5}
216
- .rk-clp-clip{display:flex;gap:10px;align-items:center;padding:6px;border:1px solid var(--rk-border);border-radius:12px;background:var(--rk-surface);text-decoration:none}
217
- .rk-clp-clip:hover{border-color:var(--rk-gold,#e2a63a)}
218
- .rk-clp-clip-thumb{flex:none;width:64px;height:40px;border-radius:8px;object-fit:cover;background:var(--rk-n-200,#e7e2da)}
219
- .rk-clp-clip-meta{min-width:0;flex:1}
308
+ .rk-clp-side-card{background:var(--rk-surface);border:1px solid var(--rk-border);border-radius:var(--rk-r-3xl);box-shadow:var(--rk-shadow-card);padding:18px;display:grid;gap:12px;align-content:start}
309
+ .rk-clp-side-head{display:flex;align-items:center;justify-content:space-between;gap:10px}
310
+ .rk-clp-session-list{display:flex;flex-direction:column;gap:9px;max-height:62vh;overflow:auto}
311
+ .rk-clp-session-empty{font-size:12.5px;color:var(--rk-text-faint);padding:14px 4px;line-height:1.5;text-align:center}
312
+ .rk-clp-clip{display:flex;gap:10px;align-items:center;padding:8px;border:1px solid var(--rk-border);border-radius:var(--rk-r-xl);background:var(--rk-n-50);text-decoration:none}
313
+ .rk-clp-clip:hover{border-color:var(--rk-gold-600)}
314
+ .rk-clp-clip-thumb{flex:none;width:60px;height:40px;border-radius:var(--rk-r-lg);object-fit:cover;background:var(--rk-n-200,#e7e2da)}
315
+ .rk-clp-clip-meta{min-width:0;flex:1;display:grid;gap:3px}
220
316
  .rk-clp-clip-title{font-size:12.5px;font-weight:700;color:var(--rk-ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
221
317
  .rk-clp-clip-sub{font-size:11px;color:var(--rk-text-muted);font-variant-numeric:tabular-nums}
222
- .rk-clp-librarylink{display:inline-block;margin-top:12px;font-size:12.5px;font-weight:700;color:var(--rk-gold-ink,#9a6b12);text-decoration:none}
318
+ .rk-clp-librarylink{font-size:12.5px;font-weight:700;color:var(--rk-gold-ink,#9a6b12);text-decoration:none}
223
319
  .rk-clp-librarylink:hover{text-decoration:underline}
224
- .rk-btn-xs{padding:3px 9px;font-size:11px}
225
- @media (max-width: 1080px){ .rk-clp-workbench{grid-template-columns:1fr;} .rk-clp-side{position:static} }
320
+
321
+ /* settings modal (shared look with /tools/image) */
322
+ .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;animation:rk-clp-fade .16s var(--rk-ease)}
323
+ .rk-ip-settingsmodal[hidden]{display:none}
324
+ .rk-ip-settingsmodal-panel{width:min(440px,100%);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}
325
+ .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)}
326
+ .rk-ip-settingsmodal-title{font-family:var(--rk-font-display);font-weight:800;font-size:var(--rk-text-md,15px);color:var(--rk-ink)}
327
+ .rk-ip-settingsmodal-body{padding:16px 18px;display:grid;gap:10px}
328
+ .rk-ip-setting{display:grid;gap:6px}
329
+ .rk-ip-setting-note{font-weight:600;color:var(--rk-text-faint)}
330
+ .rk-ip-input{width:100%;padding:9px 11px;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}
331
+ .rk-ip-input:focus{border-color:var(--rk-gold-600);box-shadow:0 0 0 3px var(--rk-gold-tint)}
332
+ @keyframes rk-clp-fade{from{opacity:0}to{opacity:1}}
333
+
334
+ /* past-sessions left slide-in panel (shared look with /tools/image) */
335
+ .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)}
336
+ .rk-ip-sessions.is-open{transform:none}
337
+ .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)}
338
+ .rk-ip-sessions-title{font-family:var(--rk-font-display);font-weight:800;font-size:14px;color:var(--rk-ink)}
339
+ .rk-ip-sessions-list{flex:1;min-height:0;overflow-y:auto;padding:8px;display:grid;gap:4px;align-content:start}
340
+ .rk-ip-sessions-empty{font-size:12.5px;color:var(--rk-text-faint);padding:16px 8px;text-align:center;line-height:1.4}
341
+ .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%}
342
+ .rk-ip-session:hover{background:var(--rk-n-50)}
343
+ .rk-ip-session.is-active{background:var(--rk-gold-tint);border-color:var(--rk-gold-600)}
344
+ .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)}
345
+ .rk-ip-session-thumb.is-empty{display:grid;place-items:center;color:var(--rk-text-faint)}
346
+ .rk-ip-session-meta{min-width:0;display:grid;gap:3px}
347
+ .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}
348
+ .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}
349
+ .rk-ip-sessions-backdrop{position:fixed;inset:0;z-index:95;background:rgba(28,25,23,.34)}
350
+ .rk-ip-sessions-backdrop[hidden]{display:none}
351
+
352
+ @media(max-width:960px){.rk-clp-workbench{grid-template-columns:1fr}.rk-clp-side{position:static}}
226
353
  `;
227
354
  return reskinDocument({
228
- title: "vidfarm — Clip a Video",
229
- description: "Cut exact subranges out of any video straight into your raws library — preview from the source, pick a precise in/out point, and save only the clip.",
355
+ title: "vidfarm — Clip Media",
356
+ description: "Cut exact subranges out of any video or audio straight into your raws library — preview from the source, drag a precise in/out point, and save only the clip.",
230
357
  activeSlug: "inpaint",
231
358
  pageCss,
232
359
  body,
@@ -240,7 +367,9 @@ const CLIPPER_SCRIPT = `
240
367
  (function(){
241
368
  var bootEl = document.querySelector("[data-clp-boot]");
242
369
  var boot = {}; try { boot = JSON.parse(bootEl && bootEl.textContent || "{}"); } catch(_){}
370
+ var AUDIO_ONLY_KEY = "vf_clipper_audio_only";
243
371
 
372
+ var sourceBar = document.querySelector(".rk-clp-sourcebar");
244
373
  var urlInput = document.querySelector("[data-clp-url-input]");
245
374
  var loadBtn = document.querySelector("[data-clp-url-load]");
246
375
  var uploadBtn = document.querySelector("[data-clp-upload]");
@@ -249,24 +378,29 @@ const CLIPPER_SCRIPT = `
249
378
  var emptyState = document.querySelector("[data-clp-empty]");
250
379
  var player = document.querySelector("[data-clp-player]");
251
380
  var video = document.querySelector("[data-clp-video]");
381
+ var audioPoster = document.querySelector("[data-clp-audio-poster]");
382
+ var controls = document.querySelector("[data-clp-controls]");
252
383
  var playpause = document.querySelector("[data-clp-playpause]");
253
- var scrub = document.querySelector("[data-clp-scrub]");
254
384
  var timecodeNow = document.querySelector("[data-clp-timecode]");
255
385
  var durationEl = document.querySelector("[data-clp-duration]");
256
- var rangeControls = document.querySelector("[data-clp-range-controls]");
386
+ var track = document.querySelector("[data-clp-tl-track]");
387
+ var selEl = document.querySelector("[data-clp-tl-sel]");
388
+ var playhead = document.querySelector("[data-clp-tl-playhead]");
389
+ var handleIn = document.querySelector("[data-clp-tl-in]");
390
+ var handleOut = document.querySelector("[data-clp-tl-out]");
257
391
  var startInput = document.querySelector("[data-clp-start-input]");
258
392
  var endInput = document.querySelector("[data-clp-end-input]");
259
- var startSlider = document.querySelector("[data-clp-start-slider]");
260
- var endSlider = document.querySelector("[data-clp-end-slider]");
261
393
  var setStartBtn = document.querySelector("[data-clp-set-start]");
262
394
  var setEndBtn = document.querySelector("[data-clp-set-end]");
263
395
  var wholeBtn = document.querySelector("[data-clp-whole]");
264
396
  var lengthEl = document.querySelector("[data-clp-length]");
265
397
  var rangeNote = document.querySelector("[data-clp-range-note]");
266
- var rangeFill = document.querySelector("[data-clp-range-fill]");
267
- var saveBlock = document.querySelector("[data-clp-save-block]");
398
+ var advToggle = document.querySelector("[data-clp-adv-toggle]");
399
+ var advBody = document.querySelector("[data-clp-adv-body]");
268
400
  var nameInput = document.querySelector("[data-clp-name]");
401
+ var notesInput = document.querySelector("[data-clp-notes]");
269
402
  var folderInput = document.querySelector("[data-clp-folder]");
403
+ var audioOnlyInput = document.querySelector("[data-clp-audio-only]");
270
404
  var saveBtn = document.querySelector("[data-clp-save]");
271
405
  var saveLabel = document.querySelector("[data-clp-save-label]");
272
406
  var statusEl = document.querySelector("[data-clp-status]");
@@ -292,13 +426,14 @@ const CLIPPER_SCRIPT = `
292
426
  var sourceName = "";
293
427
  var sourceObjectUrl = ""; // object URL for a local upload
294
428
  var uploadTempFileId = ""; // temp_file_id when the source was uploaded
429
+ var isAudioSource = false; // heuristic: source looks like audio
295
430
  var durationSec = 0;
296
431
  var startSec = 0;
297
432
  var endSec = 0;
298
433
  var sessionTracer = "";
299
434
  var tracerAutoPending = true;
435
+ var dragging = null; // "in" | "out" | null
300
436
 
301
- function nowIso(){ return new Date().toISOString(); }
302
437
  function esc(t){ var d=document.createElement("div"); d.textContent=(t==null?"":String(t)); return d.innerHTML; }
303
438
  function pad(n, w){ n=String(Math.max(0, Math.floor(n))); while(n.length<w) n="0"+n; return n; }
304
439
  function absoluteUrl(u){ try { return new URL(u, window.location.origin).toString(); } catch(_){ return u; } }
@@ -320,14 +455,12 @@ const CLIPPER_SCRIPT = `
320
455
  var parts = String(str).trim().split(":").map(function(p){ return p.trim(); });
321
456
  if(!parts.length) return NaN;
322
457
  var h=0,m=0,s=0,frac=0;
323
- if(parts.length === 4){ h=+parts[0]; m=+parts[1]; s=+parts[2]; frac=+parts[3]; }
324
- else if(parts.length === 3){ m=+parts[0]; s=+parts[1]; frac=+parts[2]; }
325
- else if(parts.length === 2){ s=+parts[0]; frac=+parts[1]; }
458
+ if(parts.length === 4){ h=+parts[0]; m=+parts[1]; s=+parts[2]; }
459
+ else if(parts.length === 3){ m=+parts[0]; s=+parts[1]; }
460
+ else if(parts.length === 2){ s=+parts[0]; }
326
461
  else { s=+parts[0]; }
327
- if([h,m,s,frac].some(function(n){ return isNaN(n); })) return NaN;
328
- // Frac field: 4 digits = ten-thousandths; be forgiving of shorter entries.
329
- var fracDivisor = Math.pow(10, String(parts[parts.length-1]||"").length || 4);
330
- if(parts.length >= 2){ frac = (+parts[parts.length-1]) / fracDivisor; } else { frac = 0; }
462
+ if([h,m,s].some(function(n){ return isNaN(n); })) return NaN;
463
+ if(parts.length >= 2){ var last = parts[parts.length-1]||""; var div = Math.pow(10, last.length||4); frac = (+last)/div; if(isNaN(frac)) frac=0; }
331
464
  return h*3600 + m*60 + s + frac;
332
465
  }
333
466
  function fmtLen(sec){
@@ -337,11 +470,12 @@ const CLIPPER_SCRIPT = `
337
470
  return m+":"+pad(Math.floor(s),2)+"."+pad(Math.round((s-Math.floor(s))*10),1);
338
471
  }
339
472
  function slug(v){ return String(v||"").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"").slice(0,60); }
473
+ function looksAudio(u){ return /\\.(mp3|m4a|aac|wav|ogg|oga|flac|opus)(\\?|#|$)/i.test(String(u||"")); }
340
474
 
341
475
  function setStatus(msg, tone){ if(!statusEl) return; statusEl.textContent = msg||""; if(tone) statusEl.setAttribute("data-tone", tone); else statusEl.removeAttribute("data-tone"); }
342
476
  function setRangeNote(msg){ if(!rangeNote) return; if(msg){ rangeNote.textContent = msg; rangeNote.hidden = false; } else { rangeNote.hidden = true; } }
343
477
 
344
- // ── tracer ──
478
+ // ── tracer / session id ──
345
479
  function tracerSuffix(){ return Math.random().toString(36).slice(2, 6); }
346
480
  function nameTracerFromSeed(seed){
347
481
  var base = slug(seed) || "clipper";
@@ -354,14 +488,31 @@ const CLIPPER_SCRIPT = `
354
488
  if(tracerInput && tracerInput.value.trim()){ sessionTracer = tracerInput.value.trim(); tracerAutoPending = false; return sessionTracer; }
355
489
  return sessionTracer;
356
490
  }
491
+ // Always return a concrete session id — never leave the tracer unset at save time.
492
+ function ensureTracer(){
493
+ var t = resolveTracer();
494
+ if(tracerAutoPending || !t){ t = nameTracerFromSeed(sourceName || "clip"); }
495
+ return t;
496
+ }
357
497
  function currentFolder(){
358
498
  var f = (folderInput && folderInput.value.trim()) || "";
359
499
  return f || slug(resolveTracer() || sessionTracer || "");
360
500
  }
361
501
 
362
502
  // ── source loading ──
363
- function revealPlayer(){ if(emptyState) emptyState.hidden = true; if(player) player.hidden = false; if(rangeControls) rangeControls.hidden = false; if(saveBlock) saveBlock.hidden = false; }
503
+ function revealPlayer(){ if(emptyState) emptyState.hidden = true; if(player) player.hidden = false; if(controls) controls.hidden = false; }
364
504
  function clearSourceObjectUrl(){ if(sourceObjectUrl){ try { URL.revokeObjectURL(sourceObjectUrl); } catch(_){} sourceObjectUrl=""; } }
505
+ function applyAudioPoster(){
506
+ // Once a frame is decoded (readyState>=2) a real video reports videoWidth>0;
507
+ // a frameless source (audio) never does → treat it as audio and force the
508
+ // audio-only clip mode (a "video" trim of a frameless source is meaningless).
509
+ if(video && video.readyState >= 2 && (!video.videoWidth || !video.videoHeight)){
510
+ isAudioSource = true;
511
+ if(audioOnlyInput && !audioOnlyInput.checked){ audioOnlyInput.checked = true; refreshSaveLabel(); }
512
+ }
513
+ var showPoster = isAudioSource || (video && video.readyState >= 1 && (!video.videoWidth || !video.videoHeight));
514
+ if(audioPoster) audioPoster.hidden = !showPoster;
515
+ }
365
516
 
366
517
  function playSrc(url){
367
518
  if(!video) return;
@@ -369,28 +520,39 @@ const CLIPPER_SCRIPT = `
369
520
  video.src = url;
370
521
  video.load();
371
522
  revealPlayer();
523
+ applyAudioPoster();
524
+ }
525
+
526
+ // Toggle the ingest controls off while a URL is resolving.
527
+ function setSourceLoading(on){
528
+ if(sourceBar) sourceBar.classList.toggle("is-loading", !!on);
529
+ if(loadBtn){ loadBtn.setAttribute("data-loading", on?"true":"false"); loadBtn.disabled = !!on; loadBtn.textContent = on ? "Loading\\u2026" : "Load"; }
530
+ if(uploadBtn) uploadBtn.disabled = !!on;
531
+ if(attachBtn) attachBtn.disabled = !!on;
372
532
  }
373
533
 
374
534
  // Resolve a pasted/handoff URL to a direct preview media URL, then play it.
375
535
  function loadSourceUrl(rawUrl, name){
376
536
  var u = String(rawUrl||"").trim();
377
- if(!u){ setStatus("Enter a video URL.", "error"); return; }
537
+ if(!u){ setStatus("Enter a media URL.", "error"); return; }
378
538
  if(!/^https?:\\/\\//i.test(u)){ setStatus("That doesn't look like a valid URL.", "error"); return; }
379
539
  sourceUrl = u; sourceName = name || ""; uploadTempFileId = ""; clearSourceObjectUrl();
380
- if(loadBtn){ loadBtn.disabled = true; loadBtn.textContent = "Loading\\u2026"; }
540
+ isAudioSource = looksAudio(u);
541
+ setSourceLoading(true);
381
542
  setStatus("Resolving source\\u2026");
382
543
  fetch("/raws/resolve-source", { method:"POST", credentials:"same-origin", headers:{ "content-type":"application/json" }, body: JSON.stringify({ source_url: u }) })
383
544
  .then(function(r){ return parseJson(r).then(function(p){ return { ok:r.ok, p:p }; }); })
384
545
  .then(function(res){
385
- if(!res.ok || !res.p.preview_url) throw new Error(res.p.error || "Could not load that video.");
546
+ if(!res.ok || !res.p.preview_url) throw new Error(res.p.error || "Could not load that source.");
386
547
  previewUrl = res.p.preview_url;
387
548
  if(res.p.title && !sourceName) sourceName = res.p.title;
549
+ if(looksAudio(previewUrl)) isAudioSource = true;
388
550
  if(tracerAutoPending) nameTracerFromSeed(sourceName || "clip");
389
551
  playSrc(previewUrl);
390
- setStatus("Loaded \\u2014 scrub and set your in/out points.", "success");
552
+ setStatus("Loaded \\u2014 drag the in/out edges on the timeline.", "success");
391
553
  })
392
- .catch(function(err){ setStatus(err && err.message ? err.message : "Could not load that video.", "error"); })
393
- .then(function(){ if(loadBtn){ loadBtn.disabled = false; loadBtn.textContent = "Load"; } });
554
+ .catch(function(err){ setStatus(err && err.message ? err.message : "Could not load that source.", "error"); })
555
+ .then(function(){ setSourceLoading(false); });
394
556
  }
395
557
 
396
558
  function loadLocalFile(file){
@@ -398,6 +560,7 @@ const CLIPPER_SCRIPT = `
398
560
  clearSourceObjectUrl();
399
561
  sourceObjectUrl = URL.createObjectURL(file);
400
562
  previewUrl = ""; sourceName = file.name || "upload.mp4"; sourceUrl = ""; uploadTempFileId = "";
563
+ isAudioSource = /^audio\\//i.test(file.type||"") || looksAudio(file.name);
401
564
  if(tracerAutoPending) nameTracerFromSeed(sourceName);
402
565
  playSrc(sourceObjectUrl);
403
566
  setStatus("Uploading source in the background\\u2026");
@@ -409,7 +572,7 @@ const CLIPPER_SCRIPT = `
409
572
  .then(function(r){ return parseJson(r).then(function(p){ return { ok:r.ok, p:p }; }); })
410
573
  .then(function(res){
411
574
  var id = res.p && (res.p.id || res.p.file_id || (res.p.file && res.p.file.id));
412
- if(res.ok && id){ uploadTempFileId = String(id); setStatus("Source ready \\u2014 set your in/out points.", "success"); }
575
+ if(res.ok && id){ uploadTempFileId = String(id); setStatus("Source ready \\u2014 drag the in/out edges on the timeline.", "success"); }
413
576
  else { setStatus("Preview ready, but the upload failed \\u2014 saving a clip may not work. " + ((res.p && res.p.error)||""), "error"); }
414
577
  })
415
578
  .catch(function(){ setStatus("Preview ready, but the upload failed \\u2014 saving a clip may not work.", "error"); });
@@ -427,38 +590,83 @@ const CLIPPER_SCRIPT = `
427
590
  }
428
591
  }
429
592
 
430
- // ── range sync ──
593
+ // ── range + timeline sync ──
431
594
  function clamp(v, lo, hi){ return Math.max(lo, Math.min(hi, v)); }
432
- function updateFill(){
433
- if(!rangeFill || durationSec <= 0){ if(rangeFill){ rangeFill.style.left="0"; rangeFill.style.width="0"; } return; }
434
- var a = clamp(startSec/durationSec, 0, 1) * 100;
435
- var b = clamp(endSec/durationSec, 0, 1) * 100;
436
- rangeFill.style.left = a + "%";
437
- rangeFill.style.width = Math.max(0, b - a) + "%";
595
+ function pctOf(sec){ return durationSec > 0 ? clamp(sec/durationSec, 0, 1) : 0; }
596
+ function renderTimeline(){
597
+ if(!durationSec || durationSec <= 0){
598
+ if(selEl){ selEl.style.left="0"; selEl.style.width="100%"; }
599
+ if(handleIn) handleIn.style.left="0%";
600
+ if(handleOut) handleOut.style.left="100%";
601
+ return;
602
+ }
603
+ var a = pctOf(startSec)*100, b = pctOf(endSec)*100;
604
+ if(selEl){ selEl.style.left = a+"%"; selEl.style.width = Math.max(0, b-a)+"%"; }
605
+ if(handleIn) handleIn.style.left = a+"%";
606
+ if(handleOut) handleOut.style.left = b+"%";
607
+ }
608
+ function renderPlayhead(){
609
+ if(!playhead) return;
610
+ playhead.style.left = (pctOf(video ? (video.currentTime||0) : 0)*100)+"%";
438
611
  }
439
612
  function refreshRangeUI(sync){
440
613
  if(sync !== "start" && startInput && document.activeElement !== startInput) startInput.value = fmtTC(startSec);
441
614
  if(sync !== "end" && endInput && document.activeElement !== endInput) endInput.value = fmtTC(endSec);
442
- if(durationSec > 0){
443
- if(startSlider && document.activeElement !== startSlider) startSlider.value = String(Math.round((startSec/durationSec)*1000));
444
- if(endSlider && document.activeElement !== endSlider) endSlider.value = String(Math.round((endSec/durationSec)*1000));
445
- }
446
615
  var len = endSec - startSec;
447
616
  if(lengthEl) lengthEl.textContent = fmtLen(Math.max(0, len)) + " (" + Math.max(0, len).toFixed(2) + "s)";
448
- updateFill();
617
+ renderTimeline();
449
618
  var valid = len > 0.02 && (durationSec <= 0 || (startSec >= 0 && endSec <= durationSec + 0.01));
450
- if(!valid){ setRangeNote(len <= 0 ? "Out point must be after the in point." : "Range is outside the video."); }
619
+ if(!valid){ setRangeNote(len <= 0 ? "Out point must be after the in point." : "Range is outside the source."); }
451
620
  else setRangeNote("");
452
621
  if(saveBtn) saveBtn.disabled = !valid;
453
622
  }
454
623
  function setStart(sec){ startSec = clamp(sec, 0, durationSec>0?durationSec:sec); if(endSec < startSec) endSec = Math.min(durationSec>0?durationSec:startSec, startSec + 0.1); refreshRangeUI(); }
455
624
  function setEnd(sec){ endSec = clamp(sec, 0, durationSec>0?durationSec:sec); if(startSec > endSec) startSec = Math.max(0, endSec - 0.1); refreshRangeUI(); }
456
625
 
626
+ // Seek to a time and reflect the playhead / timecode.
627
+ function seekTo(sec){ if(!video || durationSec<=0) return; video.currentTime = clamp(sec, 0, durationSec); syncNow(); }
628
+
629
+ function secFromPointer(clientX){
630
+ if(!track || durationSec<=0) return 0;
631
+ var r = track.getBoundingClientRect();
632
+ var pct = r.width>0 ? clamp((clientX - r.left)/r.width, 0, 1) : 0;
633
+ return pct * durationSec;
634
+ }
635
+
636
+ // Timeline pointer interactions: click track = seek; drag handle = set in/out.
637
+ function onTrackPointerDown(e){
638
+ if(durationSec<=0) return;
639
+ var t = e.target;
640
+ if(t === handleIn || (handleIn && handleIn.contains(t))){ beginDrag("in", e); return; }
641
+ if(t === handleOut || (handleOut && handleOut.contains(t))){ beginDrag("out", e); return; }
642
+ seekTo(secFromPointer(e.clientX));
643
+ }
644
+ function beginDrag(which, e){
645
+ dragging = which;
646
+ var h = which==="in" ? handleIn : handleOut;
647
+ if(h) h.classList.add("is-drag");
648
+ e.preventDefault();
649
+ window.addEventListener("pointermove", onDragMove);
650
+ window.addEventListener("pointerup", onDragEnd);
651
+ }
652
+ function onDragMove(e){
653
+ if(!dragging) return;
654
+ var sec = secFromPointer(e.clientX);
655
+ if(dragging==="in"){ setStart(sec); seekTo(sec); }
656
+ else { setEnd(sec); seekTo(sec); }
657
+ }
658
+ function onDragEnd(){
659
+ if(handleIn) handleIn.classList.remove("is-drag");
660
+ if(handleOut) handleOut.classList.remove("is-drag");
661
+ dragging = null;
662
+ window.removeEventListener("pointermove", onDragMove);
663
+ window.removeEventListener("pointerup", onDragEnd);
664
+ }
665
+
457
666
  // ── playback ──
458
667
  function syncNow(){
459
- var t = video.currentTime || 0;
460
- if(scrub && document.activeElement !== scrub && durationSec > 0) scrub.value = String(Math.round((t/durationSec)*1000));
461
- if(timecodeNow && document.activeElement !== timecodeNow) timecodeNow.value = fmtTC(t);
668
+ if(timecodeNow && document.activeElement !== timecodeNow) timecodeNow.value = fmtTC(video ? (video.currentTime||0) : 0);
669
+ renderPlayhead();
462
670
  }
463
671
  function tick(){ if(video && !video.paused && !video.ended){ syncNow(); requestAnimationFrame(tick); } }
464
672
  function setPlayIcon(){ if(playpause) playpause.innerHTML = (video && !video.paused) ? PAUSE_SVG : PLAY_SVG; }
@@ -468,55 +676,82 @@ const CLIPPER_SCRIPT = `
468
676
  durationSec = video.duration || 0;
469
677
  startSec = 0; endSec = durationSec || 0;
470
678
  if(durationEl) durationEl.textContent = "/ " + fmtTC(durationSec);
471
- refreshRangeUI();
679
+ applyAudioPoster();
680
+ refreshRangeUI(); renderPlayhead();
472
681
  });
682
+ video.addEventListener("loadeddata", applyAudioPoster);
473
683
  video.addEventListener("timeupdate", syncNow);
474
684
  video.addEventListener("play", function(){ setPlayIcon(); tick(); });
475
685
  video.addEventListener("pause", setPlayIcon);
476
686
  video.addEventListener("ended", setPlayIcon);
477
- video.addEventListener("error", function(){ setStatus("The browser couldn't play this source. Try a direct .mp4 URL or upload the file.", "error"); });
687
+ video.addEventListener("error", function(){ setStatus("The browser couldn't play this source. Try a direct file URL or upload it.", "error"); });
478
688
  }
479
689
 
480
690
  // ── wire controls ──
481
691
  if(loadBtn) loadBtn.addEventListener("click", function(){ loadSourceUrl(urlInput && urlInput.value, ""); });
482
692
  if(urlInput) urlInput.addEventListener("keydown", function(e){ if(e.key === "Enter"){ e.preventDefault(); loadSourceUrl(urlInput.value, ""); } });
483
- if(uploadBtn && fileInput){ uploadBtn.addEventListener("click", function(){ fileInput.click(); }); fileInput.addEventListener("change", function(){ if(fileInput.files && fileInput.files[0]) loadLocalFile(fileInput.files[0]); }); }
484
- if(attachBtn) attachBtn.addEventListener("click", openAttach);
693
+ if(uploadBtn && fileInput){ uploadBtn.addEventListener("click", function(){ if(!uploadBtn.disabled) fileInput.click(); }); fileInput.addEventListener("change", function(){ if(fileInput.files && fileInput.files[0]) loadLocalFile(fileInput.files[0]); }); }
694
+ if(attachBtn) attachBtn.addEventListener("click", function(){ if(!attachBtn.disabled) openAttach(); });
485
695
  if(playpause) playpause.addEventListener("click", function(){ if(!video) return; if(video.paused) video.play(); else video.pause(); });
486
- if(scrub) scrub.addEventListener("input", function(){ if(durationSec > 0){ video.currentTime = (scrub.value/1000)*durationSec; syncNow(); } });
487
- if(timecodeNow) timecodeNow.addEventListener("change", function(){ var t = parseTC(timecodeNow.value); if(isFinite(t)){ video.currentTime = clamp(t, 0, durationSec||t); syncNow(); } });
696
+ if(timecodeNow) timecodeNow.addEventListener("change", function(){ var t = parseTC(timecodeNow.value); if(isFinite(t)) seekTo(t); });
488
697
 
489
- if(startSlider) startSlider.addEventListener("input", function(){ if(durationSec>0) setStart((startSlider.value/1000)*durationSec); });
490
- if(endSlider) endSlider.addEventListener("input", function(){ if(durationSec>0) setEnd((endSlider.value/1000)*durationSec); });
698
+ if(track) track.addEventListener("pointerdown", onTrackPointerDown);
491
699
  if(startInput) startInput.addEventListener("change", function(){ var t = parseTC(startInput.value); if(isFinite(t)) setStart(t); else refreshRangeUI(); });
492
700
  if(endInput) endInput.addEventListener("change", function(){ var t = parseTC(endInput.value); if(isFinite(t)) setEnd(t); else refreshRangeUI(); });
493
- if(setStartBtn) setStartBtn.addEventListener("click", function(){ setStart(video.currentTime || 0); });
494
- if(setEndBtn) setEndBtn.addEventListener("click", function(){ setEnd(video.currentTime || 0); });
701
+ if(setStartBtn) setStartBtn.addEventListener("click", function(){ setStart(video ? (video.currentTime||0) : 0); });
702
+ if(setEndBtn) setEndBtn.addEventListener("click", function(){ setEnd(video ? (video.currentTime||0) : 0); });
495
703
  if(wholeBtn) wholeBtn.addEventListener("click", function(){ startSec = 0; endSec = durationSec || 0; refreshRangeUI(); });
496
704
 
497
- // ── save clip ──
498
- function setSaving(on){ if(saveBtn){ saveBtn.setAttribute("data-loading", on?"true":"false"); saveBtn.disabled = on; } if(saveLabel) saveLabel.textContent = on ? "Saving\\u2026" : "Save clip to raws"; }
705
+ // ── advanced toggle + sticky audio-only ──
706
+ if(advToggle && advBody){
707
+ advToggle.addEventListener("click", function(){
708
+ var open = advBody.hidden;
709
+ advBody.hidden = !open;
710
+ advToggle.setAttribute("aria-expanded", open ? "true" : "false");
711
+ });
712
+ }
713
+ function loadStickyAudioOnly(){
714
+ var v = false; try { v = localStorage.getItem(AUDIO_ONLY_KEY) === "1"; } catch(_){}
715
+ if(audioOnlyInput) audioOnlyInput.checked = v;
716
+ // If it was previously enabled, reveal Advanced so the choice is visible.
717
+ if(v && advBody && advToggle && advBody.hidden){ advBody.hidden = false; advToggle.setAttribute("aria-expanded","true"); }
718
+ }
719
+ if(audioOnlyInput){
720
+ audioOnlyInput.addEventListener("change", function(){
721
+ try { localStorage.setItem(AUDIO_ONLY_KEY, audioOnlyInput.checked ? "1" : "0"); } catch(_){}
722
+ refreshSaveLabel();
723
+ });
724
+ }
725
+
726
+ // ── clip media ──
727
+ function isAudioOnly(){ return !!(audioOnlyInput && audioOnlyInput.checked); }
728
+ function restingSaveLabel(){ return isAudioOnly() ? "Clip Audio" : "Clip Media"; }
729
+ function refreshSaveLabel(){ if(saveLabel && saveBtn && saveBtn.getAttribute("data-loading") !== "true") saveLabel.textContent = restingSaveLabel(); }
730
+ function setSaving(on){ if(saveBtn){ saveBtn.setAttribute("data-loading", on?"true":"false"); saveBtn.disabled = on; } if(saveLabel) saveLabel.textContent = on ? "Saving\\u2026" : restingSaveLabel(); }
499
731
  function saveClip(){
500
732
  if(!video){ return; }
501
733
  var len = endSec - startSec;
502
734
  if(!(len > 0.02)){ setStatus("Set an in point and a later out point first.", "error"); return; }
503
- if(!previewUrl && !sourceUrl && !uploadTempFileId){ setStatus("Load a source video first.", "error"); return; }
504
- var tracer = resolveTracer(); if(tracerAutoPending || !tracer){ tracer = nameTracerFromSeed(sourceName || "clip"); }
735
+ if(!previewUrl && !sourceUrl && !uploadTempFileId){ setStatus("Load a source first.", "error"); return; }
736
+ var tracer = ensureTracer();
505
737
  var folder = currentFolder();
506
- var payload = { start_sec: Number(startSec.toFixed(3)), end_sec: Number(endSec.toFixed(3)), tracer: tracer };
738
+ var payload = { start_sec: Number(startSec.toFixed(3)), end_sec: Number(endSec.toFixed(3)), tracer: tracer, session_id: tracer };
739
+ if(isAudioOnly()) payload.audio_only = true;
507
740
  if(folder) payload.folder_path = folder;
508
741
  if(nameInput && nameInput.value.trim()) payload.name = nameInput.value.trim();
742
+ if(notesInput && notesInput.value.trim()) payload.notes = notesInput.value.trim();
509
743
  // Prefer the already-resolved direct URL (no re-resolve); else the uploaded
510
744
  // temp file; else the raw source URL (server resolves + bills).
511
745
  if(previewUrl) payload.preview_url = previewUrl;
512
746
  if(uploadTempFileId) payload.temp_file_id = uploadTempFileId;
513
747
  if(sourceUrl) payload.source_url = sourceUrl;
514
- setSaving(true); setStatus("Trimming and saving your clip\\u2026");
748
+ var audioOnly = isAudioOnly();
749
+ setSaving(true); setStatus(audioOnly ? "Extracting and saving the audio\\u2026" : "Trimming and saving your clip\\u2026");
515
750
  fetch("/raws/clip-range", { method:"POST", credentials:"same-origin", headers:{ "content-type":"application/json" }, body: JSON.stringify(payload) })
516
751
  .then(function(r){ return parseJson(r).then(function(p){ return { ok:r.ok, p:p }; }); })
517
752
  .then(function(res){
518
753
  if(!res.ok || !res.p.clip_id) throw new Error(res.p.error || "Could not save that clip.");
519
- setStatus("Saved \\u2014 clip added to your raws (" + (res.p.folder_path||folder) + ").", "success");
754
+ setStatus("Saved \\u2014 " + (audioOnly ? "audio" : "clip") + " added to your raws (" + (res.p.folder_path||folder) + ").", "success");
520
755
  prependSessionClip(res.p);
521
756
  loadSession();
522
757
  })
@@ -536,7 +771,8 @@ const CLIPPER_SCRIPT = `
536
771
  var title = document.createElement("div"); title.className = "rk-clp-clip-title"; title.textContent = clip.description || clip.name || clip.clip_id || "clip";
537
772
  var sub = document.createElement("div"); sub.className = "rk-clp-clip-sub";
538
773
  var d = Number(clip.duration_sec || 0);
539
- sub.textContent = (d>0 ? d.toFixed(2)+"s" : "clip");
774
+ var kind = clip.media_kind === "audio" ? "audio" : "clip";
775
+ sub.textContent = (d>0 ? d.toFixed(2)+"s" : "") + (d>0 ? " · " : "") + kind;
540
776
  meta.appendChild(title); meta.appendChild(sub); a.appendChild(thumb); a.appendChild(meta);
541
777
  return a;
542
778
  }
@@ -557,7 +793,7 @@ const CLIPPER_SCRIPT = `
557
793
  if(!res.ok) return;
558
794
  var clips = (res.p && res.p.clips) || [];
559
795
  sessionList.innerHTML = "";
560
- if(!clips.length){ var e=document.createElement("div"); e.className="rk-clp-session-empty"; e.textContent="No clips yet — set an in/out point and save your first clip."; sessionList.appendChild(e); return; }
796
+ if(!clips.length){ var e=document.createElement("div"); e.className="rk-clp-session-empty"; e.textContent="No clips yet — set an in/out point and clip your first media."; sessionList.appendChild(e); return; }
561
797
  clips.forEach(function(c){ sessionList.appendChild(clipRow(c)); });
562
798
  }).catch(function(){});
563
799
  }
@@ -614,17 +850,20 @@ const CLIPPER_SCRIPT = `
614
850
  if(sessionsToggle) sessionsToggle.addEventListener("click", openSessions);
615
851
  if(sessionsClose) sessionsClose.addEventListener("click", closeSessions);
616
852
  if(sessionsBackdrop) sessionsBackdrop.addEventListener("click", closeSessions);
853
+ if(settingsModal) settingsModal.addEventListener("click", function(e){ if(e.target === settingsModal) settingsModal.hidden = true; });
617
854
  document.addEventListener("keydown", function(e){ if(e.key === "Escape"){ if(settingsModal && !settingsModal.hidden) settingsModal.hidden = true; closeSessions(); } });
618
855
 
619
856
  // ── start new ──
620
857
  function resetWorkspace(){
621
858
  clearSourceObjectUrl();
622
- sourceUrl=""; previewUrl=""; sourceName=""; uploadTempFileId=""; durationSec=0; startSec=0; endSec=0;
859
+ sourceUrl=""; previewUrl=""; sourceName=""; uploadTempFileId=""; isAudioSource=false; durationSec=0; startSec=0; endSec=0;
623
860
  try { video.pause(); } catch(_){} if(video){ video.removeAttribute("src"); video.load(); }
624
861
  if(emptyState) emptyState.hidden = false; if(player) player.hidden = true;
625
- if(rangeControls) rangeControls.hidden = true; if(saveBlock) saveBlock.hidden = true;
626
- if(urlInput) urlInput.value=""; if(nameInput) nameInput.value=""; if(folderInput) folderInput.value="";
627
- if(sessionList) sessionList.innerHTML = "<div class=\\"rk-clp-session-empty\\">No clips yet — set an in/out point and save your first clip.</div>";
862
+ if(controls) controls.hidden = true; if(audioPoster) audioPoster.hidden = true;
863
+ if(urlInput) urlInput.value=""; if(nameInput) nameInput.value=""; if(notesInput) notesInput.value=""; if(folderInput) folderInput.value="";
864
+ setSourceLoading(false);
865
+ refreshSaveLabel();
866
+ if(sessionList) sessionList.innerHTML = "<div class=\\"rk-clp-session-empty\\">No clips yet — set an in/out point and clip your first media.</div>";
628
867
  setStatus(""); setRangeNote("");
629
868
  }
630
869
  if(startNewBtn) startNewBtn.addEventListener("click", function(){
@@ -644,6 +883,8 @@ const CLIPPER_SCRIPT = `
644
883
  if(t) loadSession();
645
884
  }
646
885
 
886
+ loadStickyAudioOnly();
887
+ refreshSaveLabel();
647
888
  initFromUrl();
648
889
  })();`;
649
890
  //# sourceMappingURL=inpaint-clipper-page.js.map