@ossclip/core 0.1.31 → 0.1.34
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/browser.ts +19 -0
- package/src/captions.ts +101 -2
- package/src/config.ts +64 -10
- package/src/cover-in-video.ts +56 -0
- package/src/cutlist.ts +38 -1
- package/src/exec.ts +13 -2
- package/src/grounding.ts +35 -13
- package/src/index.ts +5 -0
- package/src/ingest.ts +35 -1
- package/src/kept-takes.ts +170 -0
- package/src/overrides.ts +468 -35
- package/src/producer/caption-regen.ts +132 -0
- package/src/producer/index.ts +1 -0
- package/src/producer/youtube.ts +36 -2
- package/src/publish/captions.ts +102 -0
- package/src/publish/delivery.ts +303 -0
- package/src/publish/index.ts +6 -0
- package/src/publish/limits.ts +52 -0
- package/src/publish/postiz.ts +320 -0
- package/src/publish/progress.ts +75 -0
- package/src/publish/provider.ts +81 -0
- package/src/recut.ts +20 -4
- package/src/resolution.ts +114 -0
- package/src/restamp.ts +383 -0
- package/src/retime-preview.ts +166 -60
- package/src/scene-schema.ts +13 -0
- package/src/transcribe.ts +13 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { SceneCue } from "./scene-schema";
|
|
2
|
+
import type { TimeMap } from "./timemap";
|
|
3
|
+
import { SPLIT_MIN_PIECE_SEC } from "./overrides";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Carve KEPT (vetoed) and DISMISSED cleanup removals out of the plain-take
|
|
7
|
+
* cues, so revived material is a first-class block instead of an invisible
|
|
8
|
+
* stretch annexed by its neighbour (cut-review rework, 2026-08-26).
|
|
9
|
+
*
|
|
10
|
+
* Why annexation happens without this: the live memo's `retimeForPreview`
|
|
11
|
+
* only remaps EXISTING cue endpoints, and `TimeMap.toSource` at a seam
|
|
12
|
+
* returns the earlier preimage (timemap.ts's own boundary doc), so the cue
|
|
13
|
+
* after the seam starts exactly where the revived material begins — the
|
|
14
|
+
* stretch belongs to it, carries no id of its own, and cannot be selected,
|
|
15
|
+
* labeled, split, or trimmed.
|
|
16
|
+
*
|
|
17
|
+
* ONE implementation, two callers (the `applyCleanupChoices` pattern):
|
|
18
|
+
* produce carves between `fillPlainCues` and `splitCues` with this run's
|
|
19
|
+
* map, so `take-kept-*` ids exist server-side and framing edits on them
|
|
20
|
+
* survive a re-render; the editor carves after `retimeForPreview` with
|
|
21
|
+
* `livePreviewMap`'s newMap, so the block appears the moment a chip is
|
|
22
|
+
* clicked. Same ranges, same map semantics — the two cannot drift.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export interface KeptRange {
|
|
26
|
+
/** SOURCE seconds of the removal the user kept or dismissed. */
|
|
27
|
+
srcIn: number;
|
|
28
|
+
srcOut: number;
|
|
29
|
+
/**
|
|
30
|
+
* A dismissed range carves the same stable block but WITHOUT the `kept`
|
|
31
|
+
* tag: dismissed material is ordinary footage and must render as a normal
|
|
32
|
+
* take, while a vetoed-kept range renders in the revived state.
|
|
33
|
+
*/
|
|
34
|
+
dismissed?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The carved cue's id, from the range's SOURCE milliseconds — stable across
|
|
39
|
+
* re-cuts, veto toggles and re-produces by construction (§155: key on the
|
|
40
|
+
* property the disruption cannot move), so a framing edit on the revived
|
|
41
|
+
* block survives all of them.
|
|
42
|
+
*/
|
|
43
|
+
export function keptTakeId(srcIn: number): string {
|
|
44
|
+
return `take-kept-${Math.round(srcIn * 1000)}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Below this, a leading/trailing remainder of the carved cue is float dust
|
|
48
|
+
* from seam math, not a piece anyone can edit — it folds into the carved
|
|
49
|
+
* block instead of surviving as a sliver cue. */
|
|
50
|
+
const REMAINDER_EPS = 0.05;
|
|
51
|
+
|
|
52
|
+
export interface CarveResult {
|
|
53
|
+
cues: SceneCue[];
|
|
54
|
+
reports: string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* For each range: map its source edges onto `map`'s output clock (exact —
|
|
59
|
+
* a kept range is interior to a merged keep span) and split the covering
|
|
60
|
+
* PLAIN cue into up-to-three pieces, the middle one becoming the
|
|
61
|
+
* `take-kept-<srcInMs>` block. Rules, each stated where enforced:
|
|
62
|
+
*
|
|
63
|
+
* - a range shorter than `SPLIT_MIN_PIECE_SEC` carves nothing (chip-only,
|
|
64
|
+
* reported) — deliberately below `MIN_PLAIN_SEC` (0.6, fill.ts), because
|
|
65
|
+
* this is real footage the user asked to see, not an assembler gap;
|
|
66
|
+
* - a range covered by a GRAPHIC cue is left alone with a report — the
|
|
67
|
+
* graphic owns that window;
|
|
68
|
+
* - a range whose block already exists (produce carved it server-side, or
|
|
69
|
+
* an earlier call did) is skipped — carving is idempotent;
|
|
70
|
+
* - a range not fully inside one plain cue carves the part that is, with a
|
|
71
|
+
* report — never two cues sharing one id.
|
|
72
|
+
*/
|
|
73
|
+
export function carveKeptTakes(
|
|
74
|
+
cues: readonly SceneCue[],
|
|
75
|
+
ranges: readonly KeptRange[],
|
|
76
|
+
map: TimeMap,
|
|
77
|
+
): CarveResult {
|
|
78
|
+
const out = [...cues];
|
|
79
|
+
const reports: string[] = [];
|
|
80
|
+
for (const range of ranges) {
|
|
81
|
+
const id = keptTakeId(range.srcIn);
|
|
82
|
+
const label = `kept range ${range.srcIn.toFixed(3)}–${range.srcOut.toFixed(3)}s`;
|
|
83
|
+
if (out.some((c) => c.id === id || c.id.startsWith(`${id}@`))) continue; // already carved
|
|
84
|
+
if (range.srcOut - range.srcIn < SPLIT_MIN_PIECE_SEC) {
|
|
85
|
+
reports.push(`${label} is shorter than ${SPLIT_MIN_PIECE_SEC}s — shown in the lane only`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const outIn = map.toOutput(range.srcIn);
|
|
89
|
+
const outOut = map.toOutput(range.srcOut);
|
|
90
|
+
if (outIn === null || outOut === null || outOut <= outIn) {
|
|
91
|
+
// Not on this clock at all — the range's material is (still) removed
|
|
92
|
+
// here; nothing to carve, and clamping would mint a lie of a block.
|
|
93
|
+
reports.push(`${label} is not in this cut — no block carved`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const i = out.findIndex((c) => c.startSec <= outIn + 1e-6 && c.endSec > outIn + 1e-6);
|
|
97
|
+
const host = i === -1 ? undefined : out[i];
|
|
98
|
+
if (host === undefined) {
|
|
99
|
+
// A HOLE, not an annexation: a removal at the head (or against a
|
|
100
|
+
// graphic's edge) retimes the neighbouring take AWAY from the revived
|
|
101
|
+
// stretch instead of over it (`TimeMap.toSource`'s earlier-preimage
|
|
102
|
+
// rule points the old 0 at the neighbour's own source start). Nothing
|
|
103
|
+
// owns the window, so the block is minted from scratch — layout
|
|
104
|
+
// borrowed from the nearest plain cue so the revived footage frames
|
|
105
|
+
// like its neighbours, never like a graphic.
|
|
106
|
+
const overlapping = out.some((c) => c.startSec < outOut - 1e-6 && c.endSec > outIn + 1e-6);
|
|
107
|
+
if (overlapping) {
|
|
108
|
+
reports.push(`${label} straddles existing cues — no block carved`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const neighbour = [...out]
|
|
112
|
+
.filter((c) => c.kind === "plain")
|
|
113
|
+
.sort(
|
|
114
|
+
(a, b) => Math.abs(a.startSec - outIn) - Math.abs(b.startSec - outIn),
|
|
115
|
+
)[0];
|
|
116
|
+
const minted: SceneCue = {
|
|
117
|
+
id,
|
|
118
|
+
kind: "plain",
|
|
119
|
+
layout: neighbour?.layout ?? "video-top",
|
|
120
|
+
...(neighbour?.video !== undefined ? { video: neighbour.video } : {}),
|
|
121
|
+
startSec: outIn,
|
|
122
|
+
endSec: outOut,
|
|
123
|
+
...(range.dismissed === true
|
|
124
|
+
? {}
|
|
125
|
+
: { kept: { srcIn: range.srcIn, srcOut: range.srcOut } }),
|
|
126
|
+
};
|
|
127
|
+
const insertAt = out.findIndex((c) => c.startSec >= outOut - 1e-6);
|
|
128
|
+
out.splice(insertAt === -1 ? out.length : insertAt, 0, minted);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (host.kind !== "plain") {
|
|
132
|
+
// Absence means "graphic" (SceneCueSchema's kind doc) — either way,
|
|
133
|
+
// not ours to carve.
|
|
134
|
+
reports.push(`${label} sits under graphic "${host.id}" — the graphic keeps the window`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const end = Math.min(outOut, host.endSec);
|
|
138
|
+
if (end < outOut - 1e-6) {
|
|
139
|
+
reports.push(
|
|
140
|
+
`${label} crosses out of take "${host.id}" — carved up to its edge (${end.toFixed(3)}s)`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
// Pieces: [host.start, outIn] (host keeps its id), the carved block,
|
|
144
|
+
// [end, host.end] (host's id too — `splitCues`' both-halves-keep rule
|
|
145
|
+
// does not apply: these are the SAME take around a foreign block, and
|
|
146
|
+
// minting `@` names here would collide with the split-id namespace).
|
|
147
|
+
const carved: SceneCue = {
|
|
148
|
+
...host,
|
|
149
|
+
id,
|
|
150
|
+
startSec: Math.max(host.startSec, outIn),
|
|
151
|
+
endSec: end,
|
|
152
|
+
...(range.dismissed === true ? {} : { kept: { srcIn: range.srcIn, srcOut: range.srcOut } }),
|
|
153
|
+
};
|
|
154
|
+
// Sub-eps remainders fold into the carved block — a 20ms sliver take is
|
|
155
|
+
// seam float dust, not content (REMAINDER_EPS).
|
|
156
|
+
const lead = outIn - host.startSec;
|
|
157
|
+
const tail = host.endSec - end;
|
|
158
|
+
const pieces: SceneCue[] = [
|
|
159
|
+
...(lead >= REMAINDER_EPS ? [{ ...host, endSec: outIn }] : []),
|
|
160
|
+
lead >= REMAINDER_EPS ? carved : { ...carved, startSec: host.startSec },
|
|
161
|
+
...(tail >= REMAINDER_EPS ? [{ ...host, startSec: end }] : []),
|
|
162
|
+
];
|
|
163
|
+
if (tail < REMAINDER_EPS) {
|
|
164
|
+
const last = pieces[pieces.length - 1]!;
|
|
165
|
+
pieces[pieces.length - 1] = { ...last, endSec: host.endSec };
|
|
166
|
+
}
|
|
167
|
+
out.splice(i, 1, ...pieces);
|
|
168
|
+
}
|
|
169
|
+
return { cues: out, reports };
|
|
170
|
+
}
|