@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.
- package/.agents/skills/music/SKILL.md +416 -0
- package/.agents/skills/music/references/api_reference.md +519 -0
- package/.agents/skills/music/references/installation.md +65 -0
- package/.agents/skills/text-to-speech/SKILL.md +226 -0
- package/.agents/skills/text-to-speech/references/installation.md +90 -0
- package/.agents/skills/text-to-speech/references/streaming.md +307 -0
- package/.agents/skills/text-to-speech/references/voice-settings.md +115 -0
- package/.agents/skills/vidfarm-media/SKILL.md +25 -11
- package/.agents/skills/vidfarm-media/references/tts.md +41 -3
- package/SKILL.director.md +31 -13
- package/SKILL.platform.md +2 -2
- package/demo/dist/app.css +1 -1
- package/demo/dist/app.js +75 -75
- package/dist/src/app.js +129 -41
- package/dist/src/cli.js +162 -5
- package/dist/src/config.js +12 -0
- package/dist/src/devcli/clips.js +7 -2
- package/dist/src/domain.js +3 -0
- package/dist/src/editor-chat.js +5 -1
- package/dist/src/frontend/file-directory.js +41 -18
- package/dist/src/primitive-context.js +14 -0
- package/dist/src/primitive-registry.js +140 -18
- package/dist/src/reskin/chat-page.js +1 -1
- package/dist/src/reskin/inpaint-clipper-page.js +446 -205
- package/dist/src/reskin/library-page.js +7 -1
- package/dist/src/reskin/portfolio-page.js +9 -9
- package/dist/src/reskin/settings-page.js +4 -4
- package/dist/src/reskin/theme.js +8 -4
- package/dist/src/services/billing.js +5 -0
- package/dist/src/services/clip-curation/ffmpeg.js +48 -0
- package/dist/src/services/clip-curation/hunt.js +2 -0
- package/dist/src/services/clip-curation/index.js +1 -1
- package/dist/src/services/clip-curation/scan.js +29 -16
- package/dist/src/services/elevenlabs.js +222 -0
- package/dist/src/services/providers.js +216 -2
- package/dist/src/services/swipe-customize.js +5 -2
- package/package.json +1 -1
- 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
|
|
4
|
-
// source straight from its resolved RapidAPI/direct URL (nothing is
|
|
5
|
-
// preview), lets the user pick an in/out point down to sub-second
|
|
6
|
-
// (HH:MM:SS:NNNN)
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
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
|
-
// •
|
|
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="
|
|
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="
|
|
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
|
|
36
|
-
<p class="rk-sub">Paste (or upload) a video, scrub to the exact moment, and
|
|
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-
|
|
46
|
-
<!-- LEFT: source +
|
|
47
|
-
<section class="rk-
|
|
48
|
-
|
|
49
|
-
|
|
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://…/
|
|
52
|
-
<button type="button" class="rk-btn rk-btn-gold rk-btn-sm" data-clp-url-load>Load</button>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
<button type="button" class="rk-
|
|
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
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
<
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
<div class="rk-clp-
|
|
71
|
-
<span class="rk-clp-
|
|
72
|
-
<
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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-
|
|
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
|
-
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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>
|
|
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
|
-
<!--
|
|
104
|
-
<
|
|
105
|
-
<div class="rk-clp-
|
|
106
|
-
<div class="rk-clp-
|
|
107
|
-
|
|
108
|
-
<
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
<
|
|
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-
|
|
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
|
-
<
|
|
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-
|
|
152
|
-
<label class="rk-
|
|
153
|
-
|
|
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-
|
|
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:
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
.rk-
|
|
177
|
-
.rk-
|
|
178
|
-
.rk-
|
|
179
|
-
.rk-
|
|
180
|
-
.rk-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
.rk-
|
|
184
|
-
.rk-
|
|
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-
|
|
187
|
-
.rk-clp-
|
|
188
|
-
.rk-
|
|
189
|
-
|
|
190
|
-
|
|
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-
|
|
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:#
|
|
195
|
-
|
|
196
|
-
|
|
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-
|
|
213
|
-
.rk-
|
|
214
|
-
.rk-clp-session-list{display:flex;flex-direction:column;gap:
|
|
215
|
-
.rk-clp-session-empty{font-size:12.5px;color:var(--rk-text-
|
|
216
|
-
.rk-clp-clip{display:flex;gap:10px;align-items:center;padding:
|
|
217
|
-
.rk-clp-clip:hover{border-color:var(--rk-gold
|
|
218
|
-
.rk-clp-clip-thumb{flex:none;width:
|
|
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{
|
|
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
|
-
|
|
225
|
-
|
|
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
|
|
229
|
-
description: "Cut exact subranges out of any video straight into your raws library — preview from the source,
|
|
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
|
|
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
|
|
267
|
-
var
|
|
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];
|
|
324
|
-
else if(parts.length === 3){ m=+parts[0]; s=+parts[1];
|
|
325
|
-
else if(parts.length === 2){ s=+parts[0];
|
|
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
|
|
328
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
393
|
-
.then(function(){
|
|
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
|
|
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
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
460
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
494
|
-
if(setEndBtn) setEndBtn.addEventListener("click", function(){ setEnd(video.currentTime
|
|
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
|
-
// ──
|
|
498
|
-
|
|
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
|
|
504
|
-
var tracer =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
626
|
-
if(urlInput) urlInput.value=""; if(nameInput) nameInput.value=""; if(folderInput) folderInput.value="";
|
|
627
|
-
|
|
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
|