ossclip 0.1.21 → 0.1.22
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 +2 -0
- package/editor-dist/assets/{index-B44I-xE4.js → index-MLGz89mM.js} +32 -32
- package/editor-dist/index.html +1 -1
- package/package.json +4 -4
- package/src/analyze.ts +96 -4
- package/src/edit.ts +63 -3
- package/src/interactive/picker.ts +47 -26
- package/src/produce.ts +158 -23
- package/src/program.ts +10 -2
- package/src/ui/animation.ts +421 -0
package/src/produce.ts
CHANGED
|
@@ -105,6 +105,8 @@ import {
|
|
|
105
105
|
workdirBaseName,
|
|
106
106
|
} from "./stranded-overrides";
|
|
107
107
|
import { editHint } from "./interactive/edit-hint";
|
|
108
|
+
import { isInteractive } from "./interactive/tty";
|
|
109
|
+
import { RenderTimelineHUD, StageAnimator, printProductionCompleteBanner } from "./ui/animation";
|
|
108
110
|
import { reconcileCaptionEdits } from "./caption-report";
|
|
109
111
|
import { overridesWriteLine, writeOverrideDoc } from "./overrides-write";
|
|
110
112
|
import { recordedProduceArgs } from "./replay-argv";
|
|
@@ -259,6 +261,15 @@ export interface ProduceOptions {
|
|
|
259
261
|
* window with zero LLM calls. Written by clip runs; not for hand use.
|
|
260
262
|
*/
|
|
261
263
|
clipWindow?: string;
|
|
264
|
+
/**
|
|
265
|
+
* `--no-zoom` (field complaint 2026-08-13): switches off BOTH automatic
|
|
266
|
+
* camera-motion drivers — the idle push (zoom.ts) and EdlVideo's cut
|
|
267
|
+
* punch-in — in one flag. The per-scene editor switch (`autoZoom`) already
|
|
268
|
+
* existed, but the complaint was a whole take whose compounded motion
|
|
269
|
+
* cropped the crown on a close-up, and per-scene relief for a global
|
|
270
|
+
* problem is the wrong shape. `false` when typed; undefined means on.
|
|
271
|
+
*/
|
|
272
|
+
zoom?: boolean;
|
|
262
273
|
/**
|
|
263
274
|
* `--watermark` / `--no-watermark` tri-state: true/false when TYPED,
|
|
264
275
|
* undefined when not — undefined lets the config's `watermark` key supply
|
|
@@ -545,6 +556,7 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
545
556
|
// genuinely "everything this run did that isn't an attributed phase" (§140).
|
|
546
557
|
const phases = new PhaseTimer();
|
|
547
558
|
const cfg = loadConfig();
|
|
559
|
+
const baseCwd = process.env.INIT_CWD ?? process.cwd();
|
|
548
560
|
// `let`, not `const`: a folder input is reassigned to the concat
|
|
549
561
|
// intermediate below (folder-input-brief.md) so nothing past that point has
|
|
550
562
|
// to know a folder was ever involved. `originalInput` keeps what the user
|
|
@@ -552,7 +564,7 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
552
564
|
// video" image lookup both used to read the REASSIGNED `input`, which for a
|
|
553
565
|
// folder run is a file inside the hidden workdir, not anything the user
|
|
554
566
|
// would recognise.
|
|
555
|
-
const originalInput = resolve(inputArg);
|
|
567
|
+
const originalInput = isAbsolute(inputArg) ? inputArg : resolve(baseCwd, inputArg);
|
|
556
568
|
let input = originalInput;
|
|
557
569
|
if (!existsSync(input)) throw new Error(`input not found: ${input}`);
|
|
558
570
|
|
|
@@ -700,15 +712,31 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
700
712
|
|
|
701
713
|
const audioPath = join(work, "audio.wav");
|
|
702
714
|
if (!existsSync(audioPath)) {
|
|
703
|
-
|
|
715
|
+
const audioExtractAnim = isInteractive()
|
|
716
|
+
? new StageAnimator(
|
|
717
|
+
"AUDIO STREAM",
|
|
718
|
+
"Extracting 16kHz uncompressed audio stream from source...",
|
|
719
|
+
"audio",
|
|
720
|
+
).start()
|
|
721
|
+
: null;
|
|
722
|
+
if (!audioExtractAnim) console.log("▸ extracting audio…");
|
|
704
723
|
await extractAudio(tools, input, audioPath);
|
|
724
|
+
if (audioExtractAnim) audioExtractAnim.stop();
|
|
705
725
|
}
|
|
706
726
|
|
|
707
727
|
// Letterbox detection (PLAN Task 7): a file's frame is not always its
|
|
708
728
|
// picture — bars baked into the pixels wasted most of the video slot on one
|
|
709
729
|
// real clip. Measured once, before anything geometric; every downstream
|
|
710
730
|
// pass crops to the content rect so the bars stop existing.
|
|
731
|
+
const letterboxAnim = isInteractive()
|
|
732
|
+
? new StageAnimator(
|
|
733
|
+
"LETTERBOX SCANNER",
|
|
734
|
+
"Scanning video for letterbox black bars & content boundaries...",
|
|
735
|
+
"render",
|
|
736
|
+
).start()
|
|
737
|
+
: null;
|
|
711
738
|
const detection = await detectContentRect(tools, input, sourceProbe, { cacheDir: work });
|
|
739
|
+
if (letterboxAnim) letterboxAnim.stop();
|
|
712
740
|
const contentTimeline = detection.timeline;
|
|
713
741
|
const cropVf = cropFilter(detection.uniform);
|
|
714
742
|
if (detection.uniform && !detection.uniform.full) {
|
|
@@ -771,7 +799,14 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
771
799
|
` curl -L -o ${modelPath} https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-${model}.bin`,
|
|
772
800
|
);
|
|
773
801
|
}
|
|
774
|
-
|
|
802
|
+
const whisperAnim = isInteractive()
|
|
803
|
+
? new StageAnimator(
|
|
804
|
+
"WHISPER ASR",
|
|
805
|
+
`Transcribing audio stream with ${basename(modelPath)}...`,
|
|
806
|
+
"whisper",
|
|
807
|
+
).start()
|
|
808
|
+
: null;
|
|
809
|
+
if (!whisperAnim) console.log(`▸ transcribing (${basename(modelPath)})…`);
|
|
775
810
|
transcript = await phases.time("transcribe", () =>
|
|
776
811
|
runWhisper(
|
|
777
812
|
{
|
|
@@ -783,6 +818,7 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
783
818
|
audioPath,
|
|
784
819
|
),
|
|
785
820
|
);
|
|
821
|
+
if (whisperAnim) whisperAnim.stop();
|
|
786
822
|
console.log(`▸ transcribed ${transcript.words.length} words`);
|
|
787
823
|
await writeFile(transcriptKeyPath, JSON.stringify(requestedKey, null, 2));
|
|
788
824
|
}
|
|
@@ -793,11 +829,19 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
793
829
|
`▸ levels: floor ${levels.floorDb.toFixed(1)} dB · speech ${levels.speechDb.toFixed(1)} dB ` +
|
|
794
830
|
`→ silence threshold ${levels.thresholdDb.toFixed(1)} dB`,
|
|
795
831
|
);
|
|
796
|
-
|
|
832
|
+
const silencesAnim = isInteractive()
|
|
833
|
+
? new StageAnimator(
|
|
834
|
+
"AUDIO SPECTRUM",
|
|
835
|
+
"Analyzing speech cadence and silence thresholds...",
|
|
836
|
+
"audio",
|
|
837
|
+
).start()
|
|
838
|
+
: null;
|
|
839
|
+
if (!silencesAnim) console.log("▸ analyzing silences…");
|
|
797
840
|
const silences = await detectSilences(
|
|
798
841
|
{ ffmpegPath: cfg.ffmpegPath, noiseDb: opts.noiseDb ?? levels.thresholdDb },
|
|
799
842
|
audioPath,
|
|
800
843
|
);
|
|
844
|
+
if (silencesAnim) silencesAnim.stop();
|
|
801
845
|
// `let`, not `const` (R19 §93): a clip run re-derives all three from the
|
|
802
846
|
// transcript sliced to the chosen window, further down.
|
|
803
847
|
let analysis: Analysis = analyze(transcript, silences, sourceProbe.duration, levels);
|
|
@@ -917,6 +961,13 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
917
961
|
).transcript;
|
|
918
962
|
console.log(`▸ repairs cached (${repairs.filter((r) => r.applied).length})`);
|
|
919
963
|
} else {
|
|
964
|
+
const repairAnim = isInteractive()
|
|
965
|
+
? new StageAnimator(
|
|
966
|
+
"PHONETIC REPAIR",
|
|
967
|
+
`Correcting ASR mishearings with ${providerName === "gemini" ? "Gemini 3.7 Flash" : providerName}...`,
|
|
968
|
+
"ai",
|
|
969
|
+
).start()
|
|
970
|
+
: null;
|
|
920
971
|
const result = await phases.time("llm", () =>
|
|
921
972
|
repairTranscript(provider!, rawTranscript, {
|
|
922
973
|
speaker: opts.speaker ?? cfg.speaker,
|
|
@@ -927,6 +978,7 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
927
978
|
),
|
|
928
979
|
}),
|
|
929
980
|
);
|
|
981
|
+
if (repairAnim) repairAnim.stop();
|
|
930
982
|
transcript = result.transcript;
|
|
931
983
|
repairs = result.applied;
|
|
932
984
|
if (result.error) {
|
|
@@ -983,6 +1035,13 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
983
1035
|
// across mixed framings averages two coordinate systems and points the
|
|
984
1036
|
// crop at neither (the old C5 gap; it put the eyes at the top of the
|
|
985
1037
|
// frame on the motivating clip).
|
|
1038
|
+
const faceMeasureAnim = isInteractive()
|
|
1039
|
+
? new StageAnimator(
|
|
1040
|
+
"FACE TRACKING & FRAMING",
|
|
1041
|
+
`Measuring face geometry across ${contentTimeline.length} shot segments...`,
|
|
1042
|
+
"render",
|
|
1043
|
+
).start()
|
|
1044
|
+
: null;
|
|
986
1045
|
const segmentFaces = await measureFaceInWindows(
|
|
987
1046
|
tools,
|
|
988
1047
|
input,
|
|
@@ -993,6 +1052,7 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
993
1052
|
})),
|
|
994
1053
|
{ workDir: work },
|
|
995
1054
|
);
|
|
1055
|
+
if (faceMeasureAnim) faceMeasureAnim.stop();
|
|
996
1056
|
framingPlan = planNormalization(contentTimeline, segmentFaces, frame);
|
|
997
1057
|
}
|
|
998
1058
|
|
|
@@ -1197,8 +1257,14 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
1197
1257
|
graphicsLine = cached.graphics;
|
|
1198
1258
|
beatIssues = cached.issues ?? [];
|
|
1199
1259
|
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1260
|
+
const aiAnim = isInteractive()
|
|
1261
|
+
? new StageAnimator(
|
|
1262
|
+
"AI SCENE SYNTHESIZER",
|
|
1263
|
+
`Planning editorial beats & graphics with ${providerName === "gemini" ? "Gemini 3.7 Flash" : providerName}...`,
|
|
1264
|
+
"ai",
|
|
1265
|
+
).start()
|
|
1266
|
+
: null;
|
|
1267
|
+
if (!aiAnim) console.log(`▸ producing scenes (${providerName})…`);
|
|
1202
1268
|
if (opts.forceComponent) console.log(`▸ forcing every graphic to ${opts.forceComponent}`);
|
|
1203
1269
|
const result = await phases.time("llm", () =>
|
|
1204
1270
|
produceScenes(provider!, {
|
|
@@ -1211,6 +1277,7 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
1211
1277
|
aspect: landscape ? "16:9" : "9:16",
|
|
1212
1278
|
}),
|
|
1213
1279
|
);
|
|
1280
|
+
if (aiAnim) aiAnim.stop();
|
|
1214
1281
|
scenes = result.scenes;
|
|
1215
1282
|
beatSheet = { hook: result.beatSheet.hook, coverText: result.beatSheet.coverText };
|
|
1216
1283
|
console.log(`▸ hook: ${result.beatSheet.hook}`);
|
|
@@ -1456,12 +1523,20 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
1456
1523
|
// Face measurement (FINDINGS §13): one static crop offset per source,
|
|
1457
1524
|
// measured rather than guessed; cached in the workdir like the transcript.
|
|
1458
1525
|
const faceSamples = 9;
|
|
1526
|
+
const faceSampleAnim = isInteractive()
|
|
1527
|
+
? new StageAnimator(
|
|
1528
|
+
"FACE POSITIONING",
|
|
1529
|
+
`Sampling ${faceSamples} frames for speaker eye-line & crop framing...`,
|
|
1530
|
+
"render",
|
|
1531
|
+
).start()
|
|
1532
|
+
: null;
|
|
1459
1533
|
const faceBox = await measureFace(tools, analysisInput, analysisProbe.duration, {
|
|
1460
1534
|
cacheDir: work,
|
|
1461
1535
|
cropVf: analysisCropVf,
|
|
1462
1536
|
cacheTag,
|
|
1463
1537
|
samples: faceSamples,
|
|
1464
1538
|
});
|
|
1539
|
+
if (faceSampleAnim) faceSampleAnim.stop();
|
|
1465
1540
|
console.log(
|
|
1466
1541
|
faceBox
|
|
1467
1542
|
? `▸ face at ${(faceBox.centerYFrac * 100).toFixed(0)}% down the frame, ` +
|
|
@@ -1993,12 +2068,15 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
1993
2068
|
// One move per cut-free clip: ramp in, then hold. The clip starts ARE the
|
|
1994
2069
|
// cuts — every point the source jumps — so a take that removed nothing is
|
|
1995
2070
|
// one clip and gets exactly one slow push.
|
|
2071
|
+
const zoomOff = opts.zoom === false;
|
|
1996
2072
|
const zoom = buildZoomPlan(map.outputDuration, {
|
|
1997
2073
|
clipStarts: map.spans.map((s) => s.outIn),
|
|
1998
2074
|
});
|
|
1999
2075
|
console.log(
|
|
2000
|
-
|
|
2001
|
-
|
|
2076
|
+
zoomOff
|
|
2077
|
+
? "▸ zoom: off (--no-zoom) — static camera; jump cuts land unconcealed"
|
|
2078
|
+
: `▸ zoom: ${zoom.clips} clip(s), ${zoom.rampSec}s push then hold ` +
|
|
2079
|
+
`(${zoom.segments.length} segments)`,
|
|
2002
2080
|
);
|
|
2003
2081
|
|
|
2004
2082
|
// ---- Per-scene framing (plan step D) ------------------------------------
|
|
@@ -2069,12 +2147,24 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
2069
2147
|
if (mezzanineWillBuild) {
|
|
2070
2148
|
const mezz = join(work, contentRect.full ? "mezzanine.mp4" : "mezzanine-content.mp4");
|
|
2071
2149
|
if (!existsSync(mezz)) {
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2150
|
+
const mezzAnim = isInteractive()
|
|
2151
|
+
? new StageAnimator(
|
|
2152
|
+
"MEZZANINE ENCODE",
|
|
2153
|
+
contentRect.full
|
|
2154
|
+
? "Building dense-keyframe mezzanine stream..."
|
|
2155
|
+
: "Building dense-keyframe mezzanine stream (trimming letterbox)...",
|
|
2156
|
+
"render",
|
|
2157
|
+
).start()
|
|
2158
|
+
: null;
|
|
2159
|
+
if (!mezzAnim) {
|
|
2160
|
+
console.log(
|
|
2161
|
+
contentRect.full
|
|
2162
|
+
? "▸ building mezzanine (dense keyframes)…"
|
|
2163
|
+
: "▸ building mezzanine (dense keyframes, letterbox bars trimmed)…",
|
|
2164
|
+
);
|
|
2165
|
+
}
|
|
2077
2166
|
await makeMezzanine(tools, input, mezz, { cropVf: cropVf || undefined });
|
|
2167
|
+
if (mezzAnim) mezzAnim.stop();
|
|
2078
2168
|
}
|
|
2079
2169
|
renderVideo = mezz;
|
|
2080
2170
|
}
|
|
@@ -2163,7 +2253,14 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
2163
2253
|
sourceAspect: content.height > 0 ? content.width / content.height : undefined,
|
|
2164
2254
|
}
|
|
2165
2255
|
: null,
|
|
2166
|
-
|
|
2256
|
+
// Emptied, not flattened-to-1: a plan of flat segments still reads as "a
|
|
2257
|
+
// plan exists" to every consumer, and the point of the flag is that no
|
|
2258
|
+
// motion layer exists at all.
|
|
2259
|
+
zoomPlan: zoomOff ? [] : zoom.segments,
|
|
2260
|
+
// Written only when OFF (watermark's absent-means-default contract): the
|
|
2261
|
+
// composition reads it to neutralise the cut punch-in — the second
|
|
2262
|
+
// motion driver, which zoomPlan alone can't reach.
|
|
2263
|
+
...(zoomOff ? { staticCamera: true } : {}),
|
|
2167
2264
|
ctaKeyword,
|
|
2168
2265
|
ctaWindow,
|
|
2169
2266
|
sourceTextRegions: textRegions,
|
|
@@ -2267,9 +2364,24 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
2267
2364
|
};
|
|
2268
2365
|
}
|
|
2269
2366
|
|
|
2270
|
-
const outPath =
|
|
2367
|
+
const outPath = opts.out
|
|
2368
|
+
? isAbsolute(opts.out)
|
|
2369
|
+
? opts.out
|
|
2370
|
+
: resolve(baseCwd, opts.out)
|
|
2371
|
+
: resolve(defaultOutPath(originalInput));
|
|
2271
2372
|
const rawPath = join(work, "render-raw.mp4");
|
|
2272
|
-
|
|
2373
|
+
const interactive = isInteractive();
|
|
2374
|
+
let renderHud: RenderTimelineHUD | null = null;
|
|
2375
|
+
if (interactive) {
|
|
2376
|
+
renderHud = new RenderTimelineHUD({
|
|
2377
|
+
totalDurationSec: map.outputDuration,
|
|
2378
|
+
sceneNames: scenes.map((s) => s.component),
|
|
2379
|
+
fps: 30,
|
|
2380
|
+
aspect: landscape ? "16:9" : "9:16",
|
|
2381
|
+
}).start();
|
|
2382
|
+
} else {
|
|
2383
|
+
console.log("▸ rendering…");
|
|
2384
|
+
}
|
|
2273
2385
|
let lastPct = -10;
|
|
2274
2386
|
await phases.time("render", () =>
|
|
2275
2387
|
renderProduction(props, {
|
|
@@ -2277,17 +2389,31 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
2277
2389
|
outPath: rawPath,
|
|
2278
2390
|
browserExecutable: cfg.browserExecutable,
|
|
2279
2391
|
onProgress: (p) => {
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2392
|
+
if (renderHud) {
|
|
2393
|
+
renderHud.setProgress(p);
|
|
2394
|
+
} else {
|
|
2395
|
+
const pct = Math.floor(p * 100);
|
|
2396
|
+
if (pct >= lastPct + 10) {
|
|
2397
|
+
lastPct = pct;
|
|
2398
|
+
process.stdout.write(` ${pct}%\n`);
|
|
2399
|
+
}
|
|
2284
2400
|
}
|
|
2285
2401
|
},
|
|
2286
2402
|
}),
|
|
2287
2403
|
);
|
|
2288
|
-
|
|
2404
|
+
if (renderHud) renderHud.stop();
|
|
2405
|
+
|
|
2406
|
+
const masterAnim = interactive
|
|
2407
|
+
? new StageAnimator(
|
|
2408
|
+
"MASTERING",
|
|
2409
|
+
"Dual-pass loudness normalization (EBU R128 broadcast standard)...",
|
|
2410
|
+
"master",
|
|
2411
|
+
).start()
|
|
2412
|
+
: null;
|
|
2413
|
+
if (!masterAnim) console.log("▸ normalizing loudness…");
|
|
2289
2414
|
const normPath = join(work, "render-norm.mp4");
|
|
2290
2415
|
await phases.time("ffmpeg", () => loudnorm(tools, rawPath, normPath));
|
|
2416
|
+
if (masterAnim) masterAnim.stop();
|
|
2291
2417
|
await rename(normPath, outPath);
|
|
2292
2418
|
|
|
2293
2419
|
// ---- Cover image (FINDINGS §31) -----------------------------------------
|
|
@@ -2447,10 +2573,19 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
2447
2573
|
// Every produce run is a project the picker should offer (R17 §83) —
|
|
2448
2574
|
// best-effort, so a read-only home dir never fails the render.
|
|
2449
2575
|
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
2576
|
console.log(formatPhaseLine(phases.timings(), phases.totalMs()));
|
|
2453
2577
|
console.log(`✓ done → ${outPath}`);
|
|
2578
|
+
if (isInteractive()) {
|
|
2579
|
+
printProductionCompleteBanner({
|
|
2580
|
+
outPath,
|
|
2581
|
+
coverPath: opts.cover !== false ? (typeof opts.cover === "string" ? opts.cover : outPath.replace(/(\.[^.]+)?$/, ".cover.jpg")) : undefined,
|
|
2582
|
+
sourceDurationSec: sourceProbe.duration,
|
|
2583
|
+
outputDurationSec: map.outputDuration,
|
|
2584
|
+
sceneCount: scenes.length,
|
|
2585
|
+
llmProvider: provider ? providerName : undefined,
|
|
2586
|
+
renderTimeSec: phases.timings().render ? phases.timings().render! / 1000 : undefined,
|
|
2587
|
+
});
|
|
2588
|
+
}
|
|
2454
2589
|
console.log(editHint(work));
|
|
2455
2590
|
return {
|
|
2456
2591
|
workdir: work,
|
package/src/program.ts
CHANGED
|
@@ -337,6 +337,11 @@ export function buildProgram(): Command {
|
|
|
337
337
|
"no burned-in captions. The CTA keyword styling rides the caption track, so it goes too",
|
|
338
338
|
)
|
|
339
339
|
.option("--no-cover", "skip the cover image written beside the video")
|
|
340
|
+
.option(
|
|
341
|
+
"--no-zoom",
|
|
342
|
+
"static camera: no idle push, no cut punch-in — for close framings where " +
|
|
343
|
+
"any motion crops the head. Per-scene control stays in the editor (autoZoom)",
|
|
344
|
+
)
|
|
340
345
|
.option("--cover <path>", "cover image output path (default: <out>.cover.jpg)")
|
|
341
346
|
.option("--open-editor", "open the editor when the run finishes")
|
|
342
347
|
.option(
|
|
@@ -441,6 +446,8 @@ export function buildProgram(): Command {
|
|
|
441
446
|
blooperMarker: opts.blooperMarker,
|
|
442
447
|
collapseRetakes: opts.collapseRetakes,
|
|
443
448
|
sourceFit,
|
|
449
|
+
// commander's --no-zoom default is true; produce only acts on false.
|
|
450
|
+
zoom: opts.zoom,
|
|
444
451
|
// undefined = "not typed", so produce can let the config decide.
|
|
445
452
|
watermark: opts.watermark,
|
|
446
453
|
// undefined = "not typed" here too — the default (ON) is applied at
|
|
@@ -548,8 +555,9 @@ export function buildProgram(): Command {
|
|
|
548
555
|
.argument("<input>", "input video file (or a folder of clips)")
|
|
549
556
|
.option(
|
|
550
557
|
"--format <format>",
|
|
551
|
-
"export format: fcpxml (
|
|
552
|
-
"
|
|
558
|
+
"export format: fcpxml (Final Cut Pro) | premiere-xml (Premiere markers) | " +
|
|
559
|
+
"premiere-project (Premiere, cuts applied + camera keyframes + .srt captions sidecar) | " +
|
|
560
|
+
"resolve-edl (Resolve coloured timeline markers — its fcpxml import drops markers)",
|
|
553
561
|
"fcpxml",
|
|
554
562
|
)
|
|
555
563
|
.option("--out <path>", "export file path (default: <input>.<format>)")
|