ossclip 0.1.18 → 0.1.20
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/README.md +3 -0
- package/editor-dist/assets/index-B44I-xE4.js +163 -0
- package/editor-dist/assets/index-ChRVBVLj.css +1 -0
- package/editor-dist/index.html +2 -2
- package/package.json +4 -4
- package/src/analyse.ts +101 -0
- package/src/caption-report.ts +217 -0
- package/src/edit.ts +38 -0
- package/src/overrides-write.ts +77 -0
- package/src/phase-timing.ts +140 -0
- package/src/produce.ts +145 -88
- package/src/program.ts +79 -0
- package/editor-dist/assets/index-C8IPo60X.css +0 -1
- package/editor-dist/assets/index-CdwzWN3j.js +0 -162
package/src/produce.ts
CHANGED
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
applyOverrides,
|
|
16
16
|
applyRepairs,
|
|
17
17
|
assembleScenes,
|
|
18
|
-
applyCaptionEdits,
|
|
19
18
|
buildCaptionLines,
|
|
20
19
|
buildCutlist,
|
|
21
20
|
buildZoomPlan,
|
|
@@ -99,12 +98,15 @@ import {
|
|
|
99
98
|
} from "@ossclip/core";
|
|
100
99
|
import { recordRecentProject } from "./edit";
|
|
101
100
|
import { binOnPath, detectionLine } from "./llm-detect";
|
|
101
|
+
import { PhaseTimer, formatPhaseLine, type PhaseTimings } from "./phase-timing";
|
|
102
102
|
import {
|
|
103
103
|
strandedOverrideSiblings,
|
|
104
104
|
strandedPointerLine,
|
|
105
105
|
workdirBaseName,
|
|
106
106
|
} from "./stranded-overrides";
|
|
107
107
|
import { editHint } from "./interactive/edit-hint";
|
|
108
|
+
import { reconcileCaptionEdits } from "./caption-report";
|
|
109
|
+
import { overridesWriteLine, writeOverrideDoc } from "./overrides-write";
|
|
108
110
|
import { recordedProduceArgs } from "./replay-argv";
|
|
109
111
|
import { renderCover, renderProduction } from "@ossclip/renderer";
|
|
110
112
|
import {
|
|
@@ -133,6 +135,13 @@ export interface ProduceResult {
|
|
|
133
135
|
sceneCount: number;
|
|
134
136
|
/** Resolved provider name when the LLM ran; undefined without --produce. */
|
|
135
137
|
llmProvider?: string;
|
|
138
|
+
/**
|
|
139
|
+
* Milliseconds per attributed phase (FINDINGS §140) — same contract as the
|
|
140
|
+
* fields above: produce() surfaces the raw numbers, the command layer
|
|
141
|
+
* buckets them before anything crosses the wire (`phaseBucketProps`). A
|
|
142
|
+
* phase that never ran (cached transcript, --no-render) is absent, not 0.
|
|
143
|
+
*/
|
|
144
|
+
phaseTimings: PhaseTimings;
|
|
136
145
|
}
|
|
137
146
|
|
|
138
147
|
/**
|
|
@@ -531,6 +540,10 @@ async function preflight(bin: string, hint: string): Promise<void> {
|
|
|
531
540
|
}
|
|
532
541
|
|
|
533
542
|
export async function produce(inputArg: string, opts: ProduceOptions): Promise<ProduceResult> {
|
|
543
|
+
// First line on purpose: totalMs is the same wall clock program.ts wraps
|
|
544
|
+
// around this call for duration_ms, so `other` in the printed breakdown is
|
|
545
|
+
// genuinely "everything this run did that isn't an attributed phase" (§140).
|
|
546
|
+
const phases = new PhaseTimer();
|
|
534
547
|
const cfg = loadConfig();
|
|
535
548
|
// `let`, not `const`: a folder input is reassigned to the concat
|
|
536
549
|
// intermediate below (folder-input-brief.md) so nothing past that point has
|
|
@@ -640,10 +653,12 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
640
653
|
|
|
641
654
|
if (isFolder && folderListing) {
|
|
642
655
|
const sort = opts.sort ?? "name";
|
|
643
|
-
const result = await
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
656
|
+
const result = await phases.time("ffmpeg", () =>
|
|
657
|
+
concatFolder(tools, input, folderListing!, work, sort, {
|
|
658
|
+
w: frame.width,
|
|
659
|
+
h: frame.height,
|
|
660
|
+
}),
|
|
661
|
+
);
|
|
647
662
|
console.log(
|
|
648
663
|
`▸ folder: ${result.clips.length} clip(s), sorted by ${sort}, ` +
|
|
649
664
|
`concat ${result.durationSec.toFixed(1)}s${result.cached ? " (cached)" : ""}`,
|
|
@@ -757,14 +772,16 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
757
772
|
);
|
|
758
773
|
}
|
|
759
774
|
console.log(`▸ transcribing (${basename(modelPath)})…`);
|
|
760
|
-
transcript = await
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
775
|
+
transcript = await phases.time("transcribe", () =>
|
|
776
|
+
runWhisper(
|
|
777
|
+
{
|
|
778
|
+
whisperPath: cfg.whisperPath,
|
|
779
|
+
modelPath,
|
|
780
|
+
outBase: join(work, "whisper"),
|
|
781
|
+
language: opts.whisperLanguage,
|
|
782
|
+
},
|
|
783
|
+
audioPath,
|
|
784
|
+
),
|
|
768
785
|
);
|
|
769
786
|
console.log(`▸ transcribed ${transcript.words.length} words`);
|
|
770
787
|
await writeFile(transcriptKeyPath, JSON.stringify(requestedKey, null, 2));
|
|
@@ -900,14 +917,16 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
900
917
|
).transcript;
|
|
901
918
|
console.log(`▸ repairs cached (${repairs.filter((r) => r.applied).length})`);
|
|
902
919
|
} else {
|
|
903
|
-
const result = await
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
(
|
|
909
|
-
|
|
910
|
-
|
|
920
|
+
const result = await phases.time("llm", () =>
|
|
921
|
+
repairTranscript(provider!, rawTranscript, {
|
|
922
|
+
speaker: opts.speaker ?? cfg.speaker,
|
|
923
|
+
// A repair may not merge words across a cut.
|
|
924
|
+
isCut: (startSec, endSec) =>
|
|
925
|
+
cutlist.some(
|
|
926
|
+
(s) => s.kind === "remove" && s.srcIn < endSec && s.srcOut > startSec,
|
|
927
|
+
),
|
|
928
|
+
}),
|
|
929
|
+
);
|
|
911
930
|
transcript = result.transcript;
|
|
912
931
|
repairs = result.applied;
|
|
913
932
|
if (result.error) {
|
|
@@ -1054,16 +1073,18 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
1054
1073
|
console.log("▸ clip window cached");
|
|
1055
1074
|
} else {
|
|
1056
1075
|
console.log(`▸ selecting the strongest ~${clipTargetSec}s window (${providerName})…`);
|
|
1057
|
-
clipFresh = await
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1076
|
+
clipFresh = await phases.time("llm", () =>
|
|
1077
|
+
produceScenes(provider!, {
|
|
1078
|
+
transcript,
|
|
1079
|
+
outputDuration: clipTargetSec!,
|
|
1080
|
+
intent: opts.intent,
|
|
1081
|
+
speaker: opts.speaker ?? cfg.speaker,
|
|
1082
|
+
forceComponent: opts.forceComponent,
|
|
1083
|
+
framing: framingCtx,
|
|
1084
|
+
clip: { targetSec: clipTargetSec! },
|
|
1085
|
+
aspect: landscape ? "16:9" : "9:16",
|
|
1086
|
+
}),
|
|
1087
|
+
);
|
|
1067
1088
|
clipWindow = clipFresh.clip!.window;
|
|
1068
1089
|
for (const note of clipFresh.clip!.notes) console.log(` ▸ ${note}`);
|
|
1069
1090
|
await writeFile(clipWindowCache, JSON.stringify(clipWindow, null, 2));
|
|
@@ -1179,15 +1200,17 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
1179
1200
|
} else {
|
|
1180
1201
|
console.log(`▸ producing scenes (${providerName})…`);
|
|
1181
1202
|
if (opts.forceComponent) console.log(`▸ forcing every graphic to ${opts.forceComponent}`);
|
|
1182
|
-
const result = await
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1203
|
+
const result = await phases.time("llm", () =>
|
|
1204
|
+
produceScenes(provider!, {
|
|
1205
|
+
transcript,
|
|
1206
|
+
outputDuration: map.outputDuration,
|
|
1207
|
+
intent: opts.intent,
|
|
1208
|
+
speaker: opts.speaker ?? cfg.speaker,
|
|
1209
|
+
forceComponent: opts.forceComponent,
|
|
1210
|
+
framing: framingCtx,
|
|
1211
|
+
aspect: landscape ? "16:9" : "9:16",
|
|
1212
|
+
}),
|
|
1213
|
+
);
|
|
1191
1214
|
scenes = result.scenes;
|
|
1192
1215
|
beatSheet = { hook: result.beatSheet.hook, coverText: result.beatSheet.coverText };
|
|
1193
1216
|
console.log(`▸ hook: ${result.beatSheet.hook}`);
|
|
@@ -1595,7 +1618,8 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
1595
1618
|
clipStarts: map.spans.map((s) => s.outIn),
|
|
1596
1619
|
});
|
|
1597
1620
|
// User splits (R16 §61) — after the fill so takes split like scenes, and
|
|
1598
|
-
// before the final override pass so edits on the `id
|
|
1621
|
+
// before the final override pass so edits on the `id@<split id>` halves land
|
|
1622
|
+
// (the suffix is the split's own minted id, §137, not its time). A
|
|
1599
1623
|
// split whose ROOT was a graphic scene already happened once inside
|
|
1600
1624
|
// `splitThenDropHidden` above (PLAN 2026-08-04 Task 1) — re-running it here
|
|
1601
1625
|
// is a no-op for that scene (the split point sits exactly on the joint
|
|
@@ -1607,7 +1631,7 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
1607
1631
|
}
|
|
1608
1632
|
const { cues: mergedCues, orphans: rawOrphans } = applyOverrides(split, overrideDoc);
|
|
1609
1633
|
// Halves of a TAKE the user deleted after splitting: a take id only exists
|
|
1610
|
-
// once the fill above runs, so its `id
|
|
1634
|
+
// once the fill above runs, so its `id@<split id>` half couldn't have been seen by
|
|
1611
1635
|
// `splitThenDropHidden` earlier (that pass only ever saw graphic scenes).
|
|
1612
1636
|
// Scene halves were already caught above; this is a no-op for them. Same
|
|
1613
1637
|
// order as the editor's live memo.
|
|
@@ -1931,22 +1955,36 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
1931
1955
|
// caption output for zero visual reason (PLAN Task A4.4).
|
|
1932
1956
|
breakpoints: graphicCues.flatMap((c) => [c.startSec, c.endSec]),
|
|
1933
1957
|
});
|
|
1934
|
-
//
|
|
1935
|
-
//
|
|
1936
|
-
//
|
|
1937
|
-
//
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1958
|
+
// §137 (Task 6 review, Critical 1): the caption half of a run — migrate the
|
|
1959
|
+
// doc's keys, apply what applies, and account for the rest — is one pure
|
|
1960
|
+
// pass in `caption-report.ts`, and this is its I/O.
|
|
1961
|
+
//
|
|
1962
|
+
// MIGRATION RUNS HERE, not only in the editor. The editor's is in memory and
|
|
1963
|
+
// `edits.load` leaves the doc CLEAN, so `onRender` (which saves only when
|
|
1964
|
+
// dirty) sends a user who opened an old project, saw every retype come back
|
|
1965
|
+
// on screen, and clicked Render straight into this function with the
|
|
1966
|
+
// untouched legacy doc — and `applyCaptionEdits` matched nothing, so the
|
|
1967
|
+
// render shipped without a single retype after showing a state strictly more
|
|
1968
|
+
// convincing than the truth. None of the reasoning that keeps this call out
|
|
1969
|
+
// of `edit.ts` applies: `buildCaptionLines` just stamped a real `srcStart`
|
|
1970
|
+
// on every word above (`captions.ts:128`), so there is nothing to backfill
|
|
1971
|
+
// and no repair rule to duplicate.
|
|
1972
|
+
//
|
|
1973
|
+
// THE MIGRATED DOC IS WRITTEN BACK — the decision, stated: through the one
|
|
1974
|
+
// sanctioned `overrides.json` write further down, with its `.bak` and atomic
|
|
1975
|
+
// rename, never a second write here. A legacy key's resolvability DECAYS (it
|
|
1976
|
+
// is found by the word it names, so the next re-plan that rewrites that word
|
|
1977
|
+
// loses it for good), and this is the only durable repair in the product —
|
|
1978
|
+
// the editor's evaporates, as Critical 1 showed. The edits it could NOT
|
|
1979
|
+
// place stay in the doc regardless (`captionEditsToKeep`): they are printed
|
|
1980
|
+
// by name below, and a run that cannot anchor one today is not permission to
|
|
1981
|
+
// delete it — the next run, against a different cut, may place it (final
|
|
1982
|
+
// review, Critical 1).
|
|
1983
|
+
const captionWork = reconcileCaptionEdits(overrideDoc, baseCaptionLines);
|
|
1984
|
+
overrideDoc = captionWork.doc;
|
|
1985
|
+
const captionLines = captionWork.lines;
|
|
1986
|
+
const captionKeysReanchored = captionWork.reanchored;
|
|
1987
|
+
for (const line of captionWork.log) console.log(line);
|
|
1950
1988
|
|
|
1951
1989
|
// Micro zoom punches (FINDINGS §15) reversing at real phrase breaks (§18).
|
|
1952
1990
|
// Breaths are source-time; TimeMap has no span mapper, so both ends go
|
|
@@ -2185,26 +2223,38 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
2185
2223
|
// next run's `priorMap` then sees drift and re-anchors again, the same
|
|
2186
2224
|
// recovery path finding 3 already has to support — never a false "nothing
|
|
2187
2225
|
// changed" that quietly corrupts positions.
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2226
|
+
// `cutResult.changed` OR a §137 caption-key migration that actually MOVED an
|
|
2227
|
+
// edit — the second is why this is no longer a bare cut check. Both are
|
|
2228
|
+
// re-anchorings of the user's doc to something the pipeline just recomputed,
|
|
2229
|
+
// and both go through the one sanctioned write; a separate write for the
|
|
2230
|
+
// migration would be a SECOND sanctioned write, which the comment above
|
|
2231
|
+
// exists to prevent.
|
|
2232
|
+
//
|
|
2233
|
+
// "ACTUALLY MOVED" is load-bearing and was not there at first (final review,
|
|
2234
|
+
// Critical 2). Gated on "the migration reported something" instead, this
|
|
2235
|
+
// fires on a run that repaired NOTHING — and since a caption migration is
|
|
2236
|
+
// independent of the cut, it fires on runs where the pre-§137 gate wrote
|
|
2237
|
+
// nothing at all.
|
|
2238
|
+
//
|
|
2239
|
+
// THEY DO NOT SHARE THE `.bak`, THOUGH, and that is the rest of the same
|
|
2240
|
+
// finding (final review round 2). `refreshBackup: cutResult.changed`, never
|
|
2241
|
+
// the gate: on the field workdir the caption migration re-anchors THREE
|
|
2242
|
+
// edits, so the gate legitimately fires while `cutResult.changed` is false —
|
|
2243
|
+
// and an unconditional refresh would then copy the already-damaged
|
|
2244
|
+
// `overrides.json` over `overrides.json.bak`, which is that user's only
|
|
2245
|
+
// pre-cut save and the only artefact their deleted split half can still be
|
|
2246
|
+
// recovered from (`legacySplitId`). Repairing the captions would destroy the
|
|
2247
|
+
// evidence for the split. `writeOverrideDoc` carries the full argument for
|
|
2248
|
+
// why a caption-only write has nothing worth backing up.
|
|
2249
|
+
if (cutResult.changed || captionKeysReanchored) {
|
|
2250
|
+
await writeOverrideDoc(overridesPath, overrideDoc, { refreshBackup: cutResult.changed });
|
|
2251
|
+
console.log(overridesWriteLine(cutResult.changed));
|
|
2205
2252
|
}
|
|
2206
2253
|
|
|
2207
2254
|
if (!opts.render) {
|
|
2255
|
+
// §140: the breakdown goes above the closing lines on both exits, so the
|
|
2256
|
+
// last thing on screen stays the success line and the edit hint.
|
|
2257
|
+
console.log(formatPhaseLine(phases.timings(), phases.totalMs()));
|
|
2208
2258
|
console.log(`▸ skipping render (--no-render). Props at ${join(work, "render-props.json")}`);
|
|
2209
2259
|
console.log(editHint(work));
|
|
2210
2260
|
return {
|
|
@@ -2213,6 +2263,7 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
2213
2263
|
sourceDurationSec: sourceProbe.duration,
|
|
2214
2264
|
sceneCount: scenes.length,
|
|
2215
2265
|
llmProvider: provider ? providerName : undefined,
|
|
2266
|
+
phaseTimings: phases.timings(),
|
|
2216
2267
|
};
|
|
2217
2268
|
}
|
|
2218
2269
|
|
|
@@ -2220,21 +2271,23 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
2220
2271
|
const rawPath = join(work, "render-raw.mp4");
|
|
2221
2272
|
console.log("▸ rendering…");
|
|
2222
2273
|
let lastPct = -10;
|
|
2223
|
-
await
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
lastPct
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2274
|
+
await phases.time("render", () =>
|
|
2275
|
+
renderProduction(props, {
|
|
2276
|
+
publicDir: dirname(renderVideo),
|
|
2277
|
+
outPath: rawPath,
|
|
2278
|
+
browserExecutable: cfg.browserExecutable,
|
|
2279
|
+
onProgress: (p) => {
|
|
2280
|
+
const pct = Math.floor(p * 100);
|
|
2281
|
+
if (pct >= lastPct + 10) {
|
|
2282
|
+
lastPct = pct;
|
|
2283
|
+
process.stdout.write(` ${pct}%\n`);
|
|
2284
|
+
}
|
|
2285
|
+
},
|
|
2286
|
+
}),
|
|
2287
|
+
);
|
|
2235
2288
|
console.log("▸ normalizing loudness…");
|
|
2236
2289
|
const normPath = join(work, "render-norm.mp4");
|
|
2237
|
-
await loudnorm(tools, rawPath, normPath);
|
|
2290
|
+
await phases.time("ffmpeg", () => loudnorm(tools, rawPath, normPath));
|
|
2238
2291
|
await rename(normPath, outPath);
|
|
2239
2292
|
|
|
2240
2293
|
// ---- Cover image (FINDINGS §31) -----------------------------------------
|
|
@@ -2394,6 +2447,9 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
2394
2447
|
// Every produce run is a project the picker should offer (R17 §83) —
|
|
2395
2448
|
// best-effort, so a read-only home dir never fails the render.
|
|
2396
2449
|
await recordRecentProject(work);
|
|
2450
|
+
// §140, same placement as the --no-render exit: breakdown, then the
|
|
2451
|
+
// success line and the hint keep the bottom of the screen.
|
|
2452
|
+
console.log(formatPhaseLine(phases.timings(), phases.totalMs()));
|
|
2397
2453
|
console.log(`✓ done → ${outPath}`);
|
|
2398
2454
|
console.log(editHint(work));
|
|
2399
2455
|
return {
|
|
@@ -2403,5 +2459,6 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
2403
2459
|
sourceDurationSec: sourceProbe.duration,
|
|
2404
2460
|
sceneCount: scenes.length,
|
|
2405
2461
|
llmProvider: provider ? providerName : undefined,
|
|
2462
|
+
phaseTimings: phases.timings(),
|
|
2406
2463
|
};
|
|
2407
2464
|
}
|
package/src/program.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { z } from "zod/v4";
|
|
|
6
6
|
import { CleanupLevelSchema, SceneComponentIdSchema } from "@ossclip/core";
|
|
7
7
|
import { STUDIO_ENTRY } from "@ossclip/renderer";
|
|
8
8
|
import { loadEnvFiles } from "./env";
|
|
9
|
+
import { ExportFormatSchema, runAnalyse } from "./analyse";
|
|
10
|
+
import { phaseBucketProps } from "./phase-timing";
|
|
9
11
|
import { produce } from "./produce";
|
|
10
12
|
// The one interactive import that is STATIC rather than `await import()`: the
|
|
11
13
|
// `resetInputSource()` run boundary in `buildProgram` has to run synchronously
|
|
@@ -461,6 +463,11 @@ export function buildProgram(): Command {
|
|
|
461
463
|
render: opts.render !== false,
|
|
462
464
|
source_duration_bucket: durationBucket(result.sourceDurationSec),
|
|
463
465
|
scenes: result.sceneCount,
|
|
466
|
+
// Per-phase buckets (§140), one `<phase>_bucket` per phase that
|
|
467
|
+
// ran — bucketed in phaseBucketProps, never raw ms. Spread keys are
|
|
468
|
+
// invisible to telemetry.test.ts's source-text drift check, so the
|
|
469
|
+
// §134 pin for these lives in phase-timing.test.ts instead.
|
|
470
|
+
...phaseBucketProps(result.phaseTimings),
|
|
464
471
|
// Which branch of the input prompt was used — a branch name, never
|
|
465
472
|
// the path itself (§136). The picker exists because typing a path
|
|
466
473
|
// blocked non-technical users; this is how we find out if it helped.
|
|
@@ -528,6 +535,78 @@ export function buildProgram(): Command {
|
|
|
528
535
|
await telemetry.flush();
|
|
529
536
|
});
|
|
530
537
|
|
|
538
|
+
program
|
|
539
|
+
.command("analyse")
|
|
540
|
+
.alias("analyze")
|
|
541
|
+
.description(
|
|
542
|
+
"analyse a take and export the planned cuts as labelled NLE markers — no render, no LLM " +
|
|
543
|
+
"(FCPXML imports into Resolve and Premiere; review the markers, then cut in your own editor)",
|
|
544
|
+
)
|
|
545
|
+
.argument("<input>", "input video file (or a folder of clips)")
|
|
546
|
+
.option("--format <format>", "export format: fcpxml", "fcpxml")
|
|
547
|
+
.option("--out <path>", "export file path (default: <input>.<format>)")
|
|
548
|
+
.option("--cleanup <level>", "exact | light | standard | aggressive", "standard")
|
|
549
|
+
.option("--transcript <path>", "inject a transcript JSON instead of running whisper")
|
|
550
|
+
.option("--noise-db <db>", "override the measured silence threshold, e.g. -30", parseFloat)
|
|
551
|
+
.option("--workdir <dir>", "cache/work directory")
|
|
552
|
+
.option("--whisper-model <name>", "transcription model for this run, e.g. base.en | small.en")
|
|
553
|
+
.option(
|
|
554
|
+
"--whisper-language <code>",
|
|
555
|
+
"transcription language code for a multilingual model, e.g. ur | de | auto (whisper defaults to en)",
|
|
556
|
+
)
|
|
557
|
+
.option(
|
|
558
|
+
"--blooper-marker <word>",
|
|
559
|
+
"mark the flubbed take wherever you say this word out loud (e.g. blooper). Off unless given",
|
|
560
|
+
)
|
|
561
|
+
.option(
|
|
562
|
+
"--collapse-retakes",
|
|
563
|
+
"also mark consecutive near-identical sentences, keeping only the last complete attempt",
|
|
564
|
+
false,
|
|
565
|
+
)
|
|
566
|
+
.option("--sort <order>", "folder input: clip order, name | mtime", "name")
|
|
567
|
+
.action(async (input: string, opts, command) => {
|
|
568
|
+
const cleanup = CleanupLevelSchema.parse(opts.cleanup);
|
|
569
|
+
// Same parse-don't-coerce guard as --source-fit: a typo'd format must
|
|
570
|
+
// error naming the flag, never silently export a different file.
|
|
571
|
+
const format = ExportFormatSchema.parse(opts.format);
|
|
572
|
+
try {
|
|
573
|
+
const result = await runAnalyse(input, {
|
|
574
|
+
cleanup,
|
|
575
|
+
format,
|
|
576
|
+
out: opts.out,
|
|
577
|
+
transcript: opts.transcript,
|
|
578
|
+
workdir: opts.workdir,
|
|
579
|
+
noiseDb: opts.noiseDb,
|
|
580
|
+
whisperModel: opts.whisperModel,
|
|
581
|
+
whisperLanguage:
|
|
582
|
+
opts.whisperLanguage !== undefined
|
|
583
|
+
? z.string().trim().min(1, "--whisper-language needs a code, e.g. ur").parse(opts.whisperLanguage)
|
|
584
|
+
: undefined,
|
|
585
|
+
blooperMarker: opts.blooperMarker,
|
|
586
|
+
collapseRetakes: opts.collapseRetakes,
|
|
587
|
+
sort: opts.sort === "mtime" ? "mtime" : "name",
|
|
588
|
+
sortExplicit: command.getOptionValueSource("sort") === "cli",
|
|
589
|
+
});
|
|
590
|
+
// Counts, buckets and names only (§134) — the format is an enum name,
|
|
591
|
+
// never a path; per-phase buckets ride along like produce's.
|
|
592
|
+
telemetry.record("analyse_completed", {
|
|
593
|
+
format,
|
|
594
|
+
cleanup_level: cleanup,
|
|
595
|
+
source_duration_bucket: durationBucket(result.sourceDurationSec),
|
|
596
|
+
markers: result.markerCount,
|
|
597
|
+
...phaseBucketProps(result.phaseTimings),
|
|
598
|
+
});
|
|
599
|
+
} catch (err) {
|
|
600
|
+
// Constructor name only, like produce_failed: messages quote paths.
|
|
601
|
+
telemetry.record("analyse_failed", {
|
|
602
|
+
error_class: err instanceof Error ? err.constructor.name : "NonError",
|
|
603
|
+
});
|
|
604
|
+
throw err;
|
|
605
|
+
} finally {
|
|
606
|
+
await telemetry.flush();
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
|
|
531
610
|
program
|
|
532
611
|
.command("studio")
|
|
533
612
|
.description("open Remotion Studio on a produced composition (visual debugging)")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
.ossclip-scroll-list{scrollbar-width:thin;scrollbar-color:#3a3a44 transparent}.ossclip-scroll-list::-webkit-scrollbar{width:8px}.ossclip-scroll-list::-webkit-scrollbar-track{background:transparent}.ossclip-scroll-list::-webkit-scrollbar-thumb{background:#3a3a44;border-radius:4px}
|