@ossclip/core 0.1.30 → 0.1.33
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/package.json +1 -1
- package/src/assemble.ts +3 -0
- package/src/browser.ts +19 -0
- package/src/captions.ts +101 -2
- package/src/config.ts +24 -0
- package/src/cover-in-video.ts +56 -0
- package/src/cutlist.ts +38 -1
- package/src/index.ts +4 -0
- package/src/ingest.ts +34 -0
- package/src/kept-takes.ts +170 -0
- package/src/overrides.ts +713 -35
- package/src/producer/youtube.ts +36 -2
- package/src/publish/captions.ts +79 -0
- package/src/publish/index.ts +3 -0
- package/src/publish/postiz.ts +232 -0
- package/src/publish/provider.ts +58 -0
- package/src/recut.ts +20 -4
- package/src/restamp.ts +383 -0
- package/src/retime-preview.ts +166 -60
- package/src/scene-schema.ts +22 -0
package/src/retime-preview.ts
CHANGED
|
@@ -4,15 +4,17 @@
|
|
|
4
4
|
* so the player actually PLAYS the revived material, immediately, instead of
|
|
5
5
|
* marking a seam that only the next render honours.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
7
|
+
* Vetoes ADD time back and live cuts REMOVE it, and since the cut-review
|
|
8
|
+
* rework (2026-08-26) BOTH play immediately. The old "the editor never
|
|
9
|
+
* applies a cut" premise is retired with the ban it rested on: a fresh cut's
|
|
10
|
+
* `src` is no longer produce's alone to resolve — the writer resolves it at
|
|
11
|
+
* the gesture, on the very clock this module hands it (`toSourceSec` /
|
|
12
|
+
* `oldToSourceSec`), and the schema now says the editor MAY write it
|
|
13
|
+
* (`OverrideDocSchema.cuts`, overrides.ts). So `src` present doubles as
|
|
14
|
+
* "live-applied": those ranges subtract here and the material genuinely
|
|
15
|
+
* stops playing. A `src`-LESS entry is the legacy marked-only shape and is
|
|
16
|
+
* still never applied — the struck band communicates it, byte-identically to
|
|
17
|
+
* before.
|
|
16
18
|
*
|
|
17
19
|
* Pure and browser-safe by construction (the cover-headline.ts split): this
|
|
18
20
|
* module's whole import graph — cutlist, recut, timemap, and types — has
|
|
@@ -48,19 +50,29 @@ export interface LivePreviewClocks {
|
|
|
48
50
|
* The new clock is produce's own sequence, same functions, same order:
|
|
49
51
|
* `applyCleanupChoices(proposal, choices)` then user cuts subtract from the
|
|
50
52
|
* result (`subtractRangesFromCutlist`), so a user cut drawn over a vetoed
|
|
51
|
-
* pause still cuts here exactly as it does in produce. Only cuts
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
53
|
+
* pause still cuts here exactly as it does in produce. Only cuts carrying a
|
|
54
|
+
* `src` subtract, and since the cut-review rework that is the LIVE-APPLIED
|
|
55
|
+
* set, not just produce's own past resolutions: the editor's cut writers now
|
|
56
|
+
* resolve `src` at the gesture (module doc), so a fresh cut removes its
|
|
57
|
+
* material from the preview the moment it is made. A src-LESS entry is the
|
|
58
|
+
* legacy marked-only shape and never subtracts. Skipping the subtraction
|
|
59
|
+
* entirely would be worse than incomplete: every ALREADY-APPLIED cut (src
|
|
60
|
+
* resolved by a past produce, absent from `oldSpans`) would silently come
|
|
61
|
+
* back the moment any veto went live.
|
|
58
62
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
63
|
+
* A src cut ALONE opens the clocks (`hasLiveEdit`), which is what makes a cut
|
|
64
|
+
* inside revived material previewable at all. With no cleanup proposal on
|
|
65
|
+
* disk there is no partition to re-keep from, so the base cutlist becomes the
|
|
66
|
+
* LAST RENDER's own spans as keep-only segments and the cuts subtract from
|
|
67
|
+
* that — the honest base, and identity-safe: a cut a past produce already
|
|
68
|
+
* applied is absent from those spans, subtracts nothing, and `mapsClose`
|
|
69
|
+
* takes the null exit (`subtractRangesFromCutlist` is set-like).
|
|
70
|
+
*
|
|
71
|
+
* `null` on any degenerate input — no old spans, a veto with no proposal to
|
|
72
|
+
* apply it to, choices with no actual veto and no src cut — and on a proposal
|
|
73
|
+
* `TimeMap`'s constructor rejects (a hand-mangled production.json): the
|
|
74
|
+
* preview degrades to step 3's honest marks-rather-than-applies, never a
|
|
75
|
+
* crash, the same lenient posture as GET /api/cleanup itself.
|
|
64
76
|
*/
|
|
65
77
|
export function livePreviewMap(
|
|
66
78
|
proposal: readonly Segment[],
|
|
@@ -73,16 +85,33 @@ export function livePreviewMap(
|
|
|
73
85
|
// the cheap exact exit, not a float comparison of two equal maps.
|
|
74
86
|
const hasVeto =
|
|
75
87
|
Object.values(choices?.reasons ?? {}).some((v) => v === false) ||
|
|
76
|
-
(choices?.kept?.length ?? 0) > 0
|
|
77
|
-
|
|
78
|
-
|
|
88
|
+
(choices?.kept?.length ?? 0) > 0 ||
|
|
89
|
+
// A dismissal re-keeps content exactly like a veto does — the live
|
|
90
|
+
// preview must play it (dismissedRemovals' doc: same render outcome,
|
|
91
|
+
// different display state).
|
|
92
|
+
(choices?.dismissed?.length ?? 0) > 0;
|
|
93
|
+
const ranges = cuts.flatMap((c) =>
|
|
94
|
+
c.src !== undefined && c.src.endSec > c.src.startSec
|
|
95
|
+
? [{ start: c.src.startSec, end: c.src.endSec }]
|
|
96
|
+
: [],
|
|
97
|
+
);
|
|
98
|
+
// A src cut is a live edit in its own right now (the doc above) — the gate
|
|
99
|
+
// is no longer "is a veto live" but "is ANY of this applied live".
|
|
100
|
+
const hasLiveEdit = hasVeto || ranges.length > 0;
|
|
101
|
+
if (!hasLiveEdit) return null;
|
|
102
|
+
if (oldSpans.length === 0) return null;
|
|
103
|
+
// A veto with no proposal to apply it to is still nothing to show — the
|
|
104
|
+
// pre-rework early exit, kept explicit so that path stays byte-identical
|
|
105
|
+
// rather than relying on the `mapsClose` exit below to reach the same null.
|
|
106
|
+
if (proposal.length === 0 && ranges.length === 0) return null;
|
|
79
107
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
108
|
+
// No proposal on disk → the last render's spans ARE the base partition
|
|
109
|
+
// (keep-only): the cuts have to subtract from something, and this is the
|
|
110
|
+
// one honest description of what is currently kept.
|
|
111
|
+
const rekept =
|
|
112
|
+
proposal.length > 0
|
|
113
|
+
? applyCleanupChoices(proposal, choices)
|
|
114
|
+
: oldSpans.map((s) => ({ srcIn: s.srcIn, srcOut: s.srcOut, kind: "keep" as const }));
|
|
86
115
|
const newMap = new TimeMap(subtractRangesFromCutlist(rekept, ranges));
|
|
87
116
|
const oldMap = mapFromKeptSpans(oldSpans);
|
|
88
117
|
// Choices that change nothing (a veto already baked into the last
|
|
@@ -106,10 +135,16 @@ export function livePreviewMap(
|
|
|
106
135
|
export interface PreviewClockMappers {
|
|
107
136
|
/** OLD-clock output seconds (the last render's own timeline — what the
|
|
108
137
|
* render props, the ghost cues and the pre-retime caption lines are timed
|
|
109
|
-
* in) → the clock the player is actually on. Exact for every
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
138
|
+
* in) → the clock the player is actually on. Exact for every VETO: those
|
|
139
|
+
* only ever ADD time back, so every old moment survives on the new clock
|
|
140
|
+
* (`retimeForPreview`'s direction argument). The clamp behind it is real,
|
|
141
|
+
* not theoretical, in the two directions that REMOVE time: the
|
|
142
|
+
* retracted-veto shape the retime already reports, and — since the
|
|
143
|
+
* cut-review rework — an old instant a LIVE cut removed, which snaps to
|
|
144
|
+
* the nearest surviving edge. The documented consumer of that clamp is
|
|
145
|
+
* App.tsx's playhead-continuity effect (~:1204-1225): the playhead sitting
|
|
146
|
+
* inside material the user just cut has to land SOMEWHERE, and the seam is
|
|
147
|
+
* the closest honest answer. */
|
|
113
148
|
toLive: (sec: number) => number;
|
|
114
149
|
/** The reverse: the player's clock → the last render's own output seconds.
|
|
115
150
|
* A live moment inside REVIVED material has no old-clock preimage at all —
|
|
@@ -133,6 +168,21 @@ export interface PreviewClockMappers {
|
|
|
133
168
|
* (timemap.ts), so the seam moment is one the last render still contained.
|
|
134
169
|
* Always true for the identity pair — no veto, nothing revived. */
|
|
135
170
|
hasOldClockPreimage: (sec: number) => boolean;
|
|
171
|
+
/** Live-output → SOURCE seconds, or null when no conversion exists (the
|
|
172
|
+
* identity case with no spans-backed fallback supplied). Exact under a
|
|
173
|
+
* live veto (`newMap.toSource` is total on the player's own clock). The
|
|
174
|
+
* split writer's anchor (`splits[].src`) and, since the cut-review rework,
|
|
175
|
+
* the cut writers' too (`cuts[].src`, resolved at the gesture). */
|
|
176
|
+
toSourceSec: ((sec: number) => number) | null;
|
|
177
|
+
/** OLD-clock output → SOURCE seconds, or null when no conversion exists
|
|
178
|
+
* (the identity case with no spans-backed fallback). `toSourceSec`'s
|
|
179
|
+
* sibling for the surfaces whose windows are still timed against the LAST
|
|
180
|
+
* RENDER rather than the player — the transcript panel's word windows, and
|
|
181
|
+
* therefore `cutWords`' `src`. Exact under a live veto (`oldMap.toSource`
|
|
182
|
+
* is total on the old clock). Reaching for `toSourceSec` there would
|
|
183
|
+
* resolve the wrong source instant by exactly the revived seconds, which
|
|
184
|
+
* is the whole class of bug the cut-review audit found. */
|
|
185
|
+
oldToSourceSec: ((sec: number) => number) | null;
|
|
136
186
|
}
|
|
137
187
|
|
|
138
188
|
/**
|
|
@@ -142,10 +192,30 @@ export interface PreviewClockMappers {
|
|
|
142
192
|
* bit-identical values to what it computed before the mapping existed — the
|
|
143
193
|
* same regression anchor the `live` memo's null branch holds to.
|
|
144
194
|
*/
|
|
145
|
-
export function previewClockMappers(
|
|
195
|
+
export function previewClockMappers(
|
|
196
|
+
clocks: LivePreviewClocks | null,
|
|
197
|
+
opts: {
|
|
198
|
+
/** Live-output → source when NO veto is live (`clocks === null`): the
|
|
199
|
+
* identity clocks carry no map, and output seconds are NOT source
|
|
200
|
+
* seconds, so the caller supplies the spans-backed conversion
|
|
201
|
+
* (`mapFromKeptSpans(renderProps.spans).toSource`). Absent means
|
|
202
|
+
* `toSourceSec` is null — a writer that needs source time then falls
|
|
203
|
+
* back to old-clock-only behaviour rather than storing a lie. */
|
|
204
|
+
identityToSource?: (sec: number) => number;
|
|
205
|
+
} = {},
|
|
206
|
+
): PreviewClockMappers {
|
|
146
207
|
if (clocks === null) {
|
|
147
208
|
const identity = (sec: number): number => sec;
|
|
148
|
-
return {
|
|
209
|
+
return {
|
|
210
|
+
toLive: identity,
|
|
211
|
+
fromLive: identity,
|
|
212
|
+
hasOldClockPreimage: () => true,
|
|
213
|
+
toSourceSec: opts.identityToSource ?? null,
|
|
214
|
+
// With no live re-cut the player's clock IS the last render's, so the
|
|
215
|
+
// two source conversions are the same function — the same fallback,
|
|
216
|
+
// never a second, differently-derived one.
|
|
217
|
+
oldToSourceSec: opts.identityToSource ?? null,
|
|
218
|
+
};
|
|
149
219
|
}
|
|
150
220
|
const { oldMap, newMap } = clocks;
|
|
151
221
|
return {
|
|
@@ -162,25 +232,41 @@ export function previewClockMappers(clocks: LivePreviewClocks | null): PreviewCl
|
|
|
162
232
|
// — i.e. the live moment is inside revived material (its own doc comment
|
|
163
233
|
// above pins the inclusive-seam semantics).
|
|
164
234
|
hasOldClockPreimage: (sec) => oldMap.toOutput(newMap.toSource(sec)) !== null,
|
|
235
|
+
// The SOURCE second under the live playhead — exact: `newMap` is the
|
|
236
|
+
// very clock the player is on, and `toSource` is total. This is what
|
|
237
|
+
// lets the editor write `splits[].src` directly (SplitSchema's
|
|
238
|
+
// documented divergence from the cuts rule).
|
|
239
|
+
toSourceSec: (sec) => newMap.toSource(sec),
|
|
240
|
+
// The OLD clock's own source conversion — `oldMap`, not `newMap`: a
|
|
241
|
+
// window that has not been retimed onto the player's clock (the
|
|
242
|
+
// transcript panel's) must resolve through the map it was timed against.
|
|
243
|
+
oldToSourceSec: (sec) => oldMap.toSource(sec),
|
|
165
244
|
};
|
|
166
245
|
}
|
|
167
246
|
|
|
168
247
|
/** `cutRangeToOldClock`'s verdict on a live-clock window headed for a doc
|
|
169
|
-
* `cuts[]` slot. `exact`/`shrunk` carry OLD-clock seconds
|
|
170
|
-
* `shrunk` also carries a report (the `remapPoint` posture —
|
|
171
|
-
* says so) for the caller's feedback channel; `degenerate`
|
|
172
|
-
* has NO old-clock extent at all
|
|
248
|
+
* `cuts[]` slot's HISTORICAL record. `exact`/`shrunk` carry OLD-clock seconds
|
|
249
|
+
* ready to store; `shrunk` also carries a report (the `remapPoint` posture —
|
|
250
|
+
* a moved value says so) for the caller's feedback channel; `degenerate`
|
|
251
|
+
* means the window has NO old-clock extent at all: a writer that can resolve
|
|
252
|
+
* `src` stores it with a clamped record anyway, one that cannot must refuse
|
|
253
|
+
* out loud. */
|
|
173
254
|
export type OldClockCutRange =
|
|
174
255
|
| { kind: "exact"; startSec: number; endSec: number }
|
|
175
256
|
| { kind: "shrunk"; startSec: number; endSec: number; report: string }
|
|
176
257
|
| { kind: "degenerate" };
|
|
177
258
|
|
|
178
259
|
/**
|
|
179
|
-
* Convert a cut gesture's LIVE-clock window into the OLD-clock window
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
260
|
+
* Convert a cut gesture's LIVE-clock window into the OLD-clock window a
|
|
261
|
+
* `cuts[]` entry's `startSec`/`endSec` speak — since the cut-review rework
|
|
262
|
+
* that is the HISTORICAL RECORD half of the write (the schema comment on
|
|
263
|
+
* `OverrideDocSchema.cuts`: those two numbers describe the render-props the
|
|
264
|
+
* user was looking at, and `src` is what is authoritative once present). It
|
|
265
|
+
* is still the whole write for the two paths that have no `src` to offer:
|
|
266
|
+
* a legacy src-less entry, whose range produce resolves through the PRIOR
|
|
267
|
+
* TimeMap (so a new-clock number stored there would land the cut the revived
|
|
268
|
+
* seconds off), and a writer whose source mapper is null (no spans, no live
|
|
269
|
+
* map) — that one keeps the refusal, the pre-rework flow verbatim.
|
|
184
270
|
*
|
|
185
271
|
* Endpoints inside revived material clamp to the nearest kept edge
|
|
186
272
|
* (`fromLive`'s doc): when only ONE edge clamps the range SHRINKS there and
|
|
@@ -190,10 +276,12 @@ export type OldClockCutRange =
|
|
|
190
276
|
* sliver past the clamped edge is lost, and the report says so. When the
|
|
191
277
|
* whole window collapses to one point — both endpoints inside one revived
|
|
192
278
|
* region, or the window exactly covering it seam to seam (each seam HAS a
|
|
193
|
-
* preimage, but the same one twice) —
|
|
194
|
-
* verdict is `degenerate
|
|
195
|
-
*
|
|
196
|
-
*
|
|
279
|
+
* preimage, but the same one twice) — the old clock has no record to give:
|
|
280
|
+
* the verdict is `degenerate`, and what the caller does with it depends on
|
|
281
|
+
* whether it holds a source mapper (the type's own doc) — never a silent
|
|
282
|
+
* zero-length entry that pretends the old clock said something. Checked on
|
|
283
|
+
* the mapped WIDTH first, before the preimage question, for exactly that
|
|
284
|
+
* seam-to-seam case. The module `EPS`, not 0: the
|
|
197
285
|
* mapped ends ride TimeMap arithmetic, and a real cut is never under a
|
|
198
286
|
* microsecond.
|
|
199
287
|
*
|
|
@@ -259,12 +347,23 @@ export interface RetimedPreview {
|
|
|
259
347
|
* `newMap`'s: old-output → source → new-output, `remapPoint`'s exact
|
|
260
348
|
* algorithm — the same one produce re-anchors splits and pins with.
|
|
261
349
|
*
|
|
262
|
-
*
|
|
263
|
-
* the old clock could express survives on the new one and
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
350
|
+
* A veto only ever ADDS time back (a removal becomes a keep), so under vetoes
|
|
351
|
+
* alone every moment the old clock could express survives on the new one and
|
|
352
|
+
* maps exactly. Two directions REMOVE time and need the clamped fallback
|
|
353
|
+
* behind each point (`toOutputClamped`'s documented role): old spans carrying
|
|
354
|
+
* a veto the doc no longer holds, and — since the cut-review rework — a LIVE
|
|
355
|
+
* user cut (`cuts[].src`, subtracted by `livePreviewMap`). A moment inside
|
|
356
|
+
* either snaps to the nearest kept edge and is reported, never silently
|
|
357
|
+
* dropped.
|
|
358
|
+
*
|
|
359
|
+
* Removing time can also COLLAPSE a scene cue: a cut covering a whole block
|
|
360
|
+
* leaves both its ends clamped to the same seam. A zero-width cue is dropped
|
|
361
|
+
* from the preview outright, WITH a report — that is the honest rendering of
|
|
362
|
+
* material the user just removed, where a kept sliver would draw a phantom
|
|
363
|
+
* block on the timeline for footage that no longer plays. Only `sceneCues`
|
|
364
|
+
* get this: a collapsed caption line or zoom segment is inert where it sits
|
|
365
|
+
* (nothing renders across a zero window), while a cue is a timeline BLOCK
|
|
366
|
+
* with a label, a hit target and a selection.
|
|
268
367
|
*
|
|
269
368
|
* Word `srcStart` is already SOURCE time (§137's recut-immune key) and is
|
|
270
369
|
* carried untouched. `punch` comes back provably inert — `{scale: 1,
|
|
@@ -300,11 +399,18 @@ export function retimeForPreview(
|
|
|
300
399
|
end: at(`caption word "${w.text}" end`, w.end),
|
|
301
400
|
})),
|
|
302
401
|
})),
|
|
303
|
-
sceneCues: props.sceneCues.
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
402
|
+
sceneCues: props.sceneCues.flatMap((c) => {
|
|
403
|
+
const startSec = at(`scene "${c.id}" start`, c.startSec);
|
|
404
|
+
const endSec = at(`scene "${c.id}" end`, c.endSec);
|
|
405
|
+
// Collapsed by a live cut (the doc's own paragraph) — dropped, and
|
|
406
|
+
// said out loud, the `remapPoint` "nothing moves without saying so"
|
|
407
|
+
// rule applied to a block that stopped existing.
|
|
408
|
+
if (endSec - startSec < EPS) {
|
|
409
|
+
reports.push(`scene "${c.id}" removed from the live preview by a cut`);
|
|
410
|
+
return [];
|
|
411
|
+
}
|
|
412
|
+
return [{ ...c, startSec, endSec }];
|
|
413
|
+
}),
|
|
308
414
|
punch: { scale: 1, allowed: [] },
|
|
309
415
|
};
|
|
310
416
|
if (props.zoomPlan) {
|
package/src/scene-schema.ts
CHANGED
|
@@ -73,6 +73,28 @@ export const SceneCueSchema = z
|
|
|
73
73
|
* `=== "graphic"`.
|
|
74
74
|
*/
|
|
75
75
|
kind: z.enum(["graphic", "plain"]).optional(),
|
|
76
|
+
/**
|
|
77
|
+
* Set on a plain cue carved out of a KEPT (vetoed) cleanup removal
|
|
78
|
+
* (`carveKeptTakes`, cut-review rework 2026-08-26) — the SOURCE range the
|
|
79
|
+
* user chose to keep, so the timeline can render the revived stretch as
|
|
80
|
+
* its own labeled block instead of an indistinguishable part of the
|
|
81
|
+
* neighbouring take. Explicit field over id-prefix sniffing on purpose;
|
|
82
|
+
* optional so every render-props/production.json written before it exists
|
|
83
|
+
* parses byte-identically. A DISMISSED range's carved cue does NOT carry
|
|
84
|
+
* this — dismissed material is ordinary footage.
|
|
85
|
+
*/
|
|
86
|
+
kept: z
|
|
87
|
+
.object({ srcIn: z.number().nonnegative(), srcOut: z.number().nonnegative() })
|
|
88
|
+
.optional(),
|
|
89
|
+
/**
|
|
90
|
+
* The plan anchor this cue was resolved from — the scene's word range,
|
|
91
|
+
* carried through so an edit made against this cue can be re-keyed when a
|
|
92
|
+
* re-plan renumbers ids (handoff-edit-anchoring; §137 is the caption-side
|
|
93
|
+
* precedent). Optional: plain fill cues have no plan anchor, and
|
|
94
|
+
* render-props.json written before this field carries none — absence means
|
|
95
|
+
* "id-only identity", exactly today's behaviour.
|
|
96
|
+
*/
|
|
97
|
+
anchor: SceneAnchorSchema.optional(),
|
|
76
98
|
layout: LayoutSchema,
|
|
77
99
|
/**
|
|
78
100
|
* Required for graphic cues (the superRefine below enforces it), absent on
|