ossclip 0.1.12 → 0.1.14
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/editor-dist/assets/{index-BBVLCP-N.js → index-CKpBGMBB.js} +16 -16
- package/editor-dist/index.html +1 -1
- package/package.json +4 -4
- package/src/interactive/produce-argv.ts +8 -0
- package/src/interactive/produce-wizard.ts +10 -3
- package/src/produce.ts +112 -25
- package/src/program.ts +17 -0
- package/src/replay-argv.ts +15 -0
package/editor-dist/index.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>ossclip editor</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-CKpBGMBB.js"></script>
|
|
8
8
|
<link rel="stylesheet" crossorigin href="/assets/index-C8IPo60X.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ossclip",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "Local-first CLI video producer: cuts silence and fillers, word-timed captions, face-aware framing, and LLM-planned code-rendered graphics — transcription and rendering never leave your machine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"commander": "^12.1.0",
|
|
37
37
|
"tsx": "^4.19.0",
|
|
38
38
|
"zod": "^3.25.76",
|
|
39
|
-
"@ossclip/core": "0.1.
|
|
40
|
-
"@ossclip/
|
|
41
|
-
"@ossclip/
|
|
39
|
+
"@ossclip/core": "0.1.14",
|
|
40
|
+
"@ossclip/scenes": "0.1.14",
|
|
41
|
+
"@ossclip/renderer": "0.1.14"
|
|
42
42
|
},
|
|
43
43
|
"homepage": "https://github.com/AhsanAyaz/ossclip#readme",
|
|
44
44
|
"bugs": {
|
|
@@ -19,6 +19,11 @@ export interface ProduceExtras {
|
|
|
19
19
|
* off is the default, and a config-on user who wants it off for one run
|
|
20
20
|
* types `--no-watermark`, a flags-only surface like --sort. */
|
|
21
21
|
watermark?: boolean;
|
|
22
|
+
/** Captions tri-state, the watermark's mirror image: ON is the default,
|
|
23
|
+
* so the wizard only ever turns them OFF (`false` → `--no-captions`) and
|
|
24
|
+
* the positive `--captions` stays flags-only — it exists for replay
|
|
25
|
+
* pinning, and emitting it here would restate the default. */
|
|
26
|
+
captions?: boolean;
|
|
22
27
|
llm?: "claude" | "claude-cli" | "gemini" | "mock";
|
|
23
28
|
}
|
|
24
29
|
|
|
@@ -62,6 +67,9 @@ export function produceArgv(a: ProduceAnswers): string[] {
|
|
|
62
67
|
if (e.collapseRetakes === true) argv.push("--collapse-retakes");
|
|
63
68
|
if (e.sourceIsEdited === true) argv.push("--source-is-edited");
|
|
64
69
|
if (e.watermark === true) argv.push("--watermark");
|
|
70
|
+
// Strict `=== false` (never `!e.captions`): undefined means "the default,
|
|
71
|
+
// on" and must emit nothing per the elision rule above.
|
|
72
|
+
if (e.captions === false) argv.push("--no-captions");
|
|
65
73
|
if (e.llm) argv.push("--llm", e.llm);
|
|
66
74
|
|
|
67
75
|
return argv;
|
|
@@ -4,12 +4,15 @@ import { produceArgv, type ProduceAnswers, type ProduceExtras } from "./produce-
|
|
|
4
4
|
import { assertInteractive, confirm, intro, multiselect, select, text, unwrap } from "./prompts";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* The produce wizard. Thirty-
|
|
7
|
+
* The produce wizard. Thirty-four flags (plus the positional input path)
|
|
8
8
|
* sorted into three tiers: six prompts asked directly — the input path, plus
|
|
9
|
-
* five flags (--out, --cleanup, --aspect, --produce, --intent) —
|
|
9
|
+
* five flags (--out, --cleanup, --aspect, --produce, --intent) — ten behind
|
|
10
10
|
* one "anything else?" multiselect, and the remaining stay flags-only:
|
|
11
11
|
* debug/internal surfaces, replay-only fields, --no-watermark (the
|
|
12
|
-
* multiselect only turns the credit ON; off is already the default),
|
|
12
|
+
* multiselect only turns the credit ON; off is already the default),
|
|
13
|
+
* --captions (the mirror case: ON is already the default, so the
|
|
14
|
+
* multiselect entry is the OFF switch and the positive flag exists only for
|
|
15
|
+
* replay pinning), or
|
|
13
16
|
* (final-review fix wave, Finding 1) --sort. A folder's clip order only means anything once the
|
|
14
17
|
* folder has been enumerated, and that enumeration happens inside
|
|
15
18
|
* `produce()` — after the wizard has already returned argv — so there is
|
|
@@ -35,6 +38,7 @@ const EXTRAS = [
|
|
|
35
38
|
hint: "--collapse-retakes",
|
|
36
39
|
},
|
|
37
40
|
{ value: "sourceIsEdited", label: "Source already has burned-in text", hint: "--source-is-edited" },
|
|
41
|
+
{ value: "captionsOff", label: "Turn the burned-in captions off", hint: "--no-captions" },
|
|
38
42
|
{ value: "watermark", label: 'Credit the tool with a small "made with ossclip"', hint: "--watermark" },
|
|
39
43
|
{ value: "llm", label: "Choose the LLM provider", hint: "--llm" },
|
|
40
44
|
] as const;
|
|
@@ -235,6 +239,9 @@ export async function produceWizard(
|
|
|
235
239
|
if (chosen.includes("sourceFit")) extras.sourceFit = "contain";
|
|
236
240
|
if (chosen.includes("collapseRetakes")) extras.collapseRetakes = true;
|
|
237
241
|
if (chosen.includes("sourceIsEdited")) extras.sourceIsEdited = true;
|
|
242
|
+
// The entry is the OFF switch (captions default ON — see EXTRAS), so a
|
|
243
|
+
// tick maps to `captions: false` and produceArgv emits `--no-captions`.
|
|
244
|
+
if (chosen.includes("captionsOff")) extras.captions = false;
|
|
238
245
|
if (chosen.includes("watermark")) extras.watermark = true;
|
|
239
246
|
if (chosen.includes("speaker")) {
|
|
240
247
|
extras.speaker = unwrap(
|
package/src/produce.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
concatFolder,
|
|
25
25
|
folderManifestKey,
|
|
26
26
|
listFolderVideos,
|
|
27
|
+
coverDecision,
|
|
27
28
|
coverHeadline,
|
|
28
29
|
cropFilter,
|
|
29
30
|
detectContentRect,
|
|
@@ -246,6 +247,15 @@ export interface ProduceOptions {
|
|
|
246
247
|
* a free-tier limitation; this is voluntary attribution.
|
|
247
248
|
*/
|
|
248
249
|
watermark?: boolean;
|
|
250
|
+
/**
|
|
251
|
+
* `--captions` / `--no-captions` tri-state: true/false when TYPED,
|
|
252
|
+
* undefined when not. Unlike `watermark` above there is no config key —
|
|
253
|
+
* undefined simply means the default, which is ON. Kept tri-state anyway
|
|
254
|
+
* so command.json can pin the resolved flag state (`recordedProduceArgs`)
|
|
255
|
+
* and a future config key or default change can never re-resolve an old
|
|
256
|
+
* record differently.
|
|
257
|
+
*/
|
|
258
|
+
captions?: boolean;
|
|
249
259
|
/**
|
|
250
260
|
* `<input>` a DIRECTORY: order its clips before concatenating them into the
|
|
251
261
|
* source produce runs on (folder-input-brief.md). `name` (default) is a
|
|
@@ -280,6 +290,26 @@ export function resolveWatermark(
|
|
|
280
290
|
return flag ?? configValue === true;
|
|
281
291
|
}
|
|
282
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Whether captions are hidden this run: the flag saying OFF, or the editor's
|
|
295
|
+
* doc-global `captionsHidden` override saying hidden. An OR, deliberately
|
|
296
|
+
* NOT resolveWatermark's flag-beats-config precedence: the override is the
|
|
297
|
+
* user's own saved edit, not a machine-supplied default a typed flag should
|
|
298
|
+
* outrank — un-hiding belongs to the editor that wrote the override, so a
|
|
299
|
+
* typed `--captions` cannot force captions back on over it. Strict
|
|
300
|
+
* `=== false`/`=== true` on both sides: the override arrives zod-parsed,
|
|
301
|
+
* but the flag is a tri-state where undefined means "not typed" — and
|
|
302
|
+
* captions defaulting ON means anything short of an explicit off must read
|
|
303
|
+
* as visible. Pure so the whole flag × override matrix is testable without
|
|
304
|
+
* a workdir or an overrides.json on disk.
|
|
305
|
+
*/
|
|
306
|
+
export function resolveCaptionsHidden(
|
|
307
|
+
flag: boolean | undefined,
|
|
308
|
+
overrideHidden: boolean | undefined,
|
|
309
|
+
): boolean {
|
|
310
|
+
return flag === false || overrideHidden === true;
|
|
311
|
+
}
|
|
312
|
+
|
|
283
313
|
function sha1File(path: string): Promise<string> {
|
|
284
314
|
return new Promise((res, rej) => {
|
|
285
315
|
const h = createHash("sha1");
|
|
@@ -2006,6 +2036,21 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
2006
2036
|
// that assembleScenes dropped, and the caption track knows exactly when the
|
|
2007
2037
|
// ask is on screen. Quoting marks the word you type in the comments — every
|
|
2008
2038
|
// other time the speaker merely says it, it must render plainly.
|
|
2039
|
+
// Captions are ON by default and stay so — only the OFF path announces
|
|
2040
|
+
// itself, naming WHICH surface turned them off: a silent-captions upload
|
|
2041
|
+
// must never leave the author guessing whether they typed the flag or the
|
|
2042
|
+
// editor's toggle did it. The flag reason wins the message when both are
|
|
2043
|
+
// true — it is the one visible in the command line being run. Resolved
|
|
2044
|
+
// BEFORE the CTA block below, because the CTA line's promise depends on it.
|
|
2045
|
+
const captionsHidden = resolveCaptionsHidden(opts.captions, overrideDoc.captionsHidden);
|
|
2046
|
+
if (captionsHidden) {
|
|
2047
|
+
console.log(
|
|
2048
|
+
opts.captions === false
|
|
2049
|
+
? "▸ captions: off (--no-captions)"
|
|
2050
|
+
: "▸ captions: hidden by editor override",
|
|
2051
|
+
);
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2009
2054
|
const ctaCue = [...graphicCues]
|
|
2010
2055
|
.reverse()
|
|
2011
2056
|
.find((c) => typeof c.props?.keyword === "string" && (c.props.keyword as string).length > 0);
|
|
@@ -2014,9 +2059,15 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
2014
2059
|
? { startSec: ctaCue.startSec, endSec: ctaCue.endSec }
|
|
2015
2060
|
: undefined;
|
|
2016
2061
|
if (ctaKeyword) {
|
|
2062
|
+
// Gated on the captions resolution (review minor 1): "styled only at
|
|
2063
|
+
// X–Ys" on a captions-hidden run is a false promise — the styling rides
|
|
2064
|
+
// the caption track (ProductionComposition's mount gate), so the moment
|
|
2065
|
+
// the trade bites is the moment to say so.
|
|
2017
2066
|
console.log(
|
|
2018
|
-
|
|
2019
|
-
|
|
2067
|
+
captionsHidden
|
|
2068
|
+
? `▸ CTA keyword "${ctaKeyword}" styling skipped — captions hidden`
|
|
2069
|
+
: `▸ CTA keyword "${ctaKeyword}" styled only at ` +
|
|
2070
|
+
`${ctaWindow!.startSec.toFixed(1)}–${ctaWindow!.endSec.toFixed(1)}s`,
|
|
2020
2071
|
);
|
|
2021
2072
|
}
|
|
2022
2073
|
|
|
@@ -2091,6 +2142,18 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
2091
2142
|
// an off run's render-props.json stays byte-identical to a pre-watermark
|
|
2092
2143
|
// one, so nothing downstream can tell the feature ever shipped.
|
|
2093
2144
|
...(watermark ? { watermark: true } : {}),
|
|
2145
|
+
// Same absent-means-default contract, polarity flipped (captions default
|
|
2146
|
+
// ON): written only when hidden, so a normal run's render-props.json
|
|
2147
|
+
// stays byte-identical to a pre-feature one. `captionsHiddenByFlag` is
|
|
2148
|
+
// the flag-only part, split out for the EDITOR alone: `captionsHidden`
|
|
2149
|
+
// bakes the override doc in, and the editor re-applies the CURRENT doc
|
|
2150
|
+
// onto pristine bases (see `baseSceneCues` above) — without the split,
|
|
2151
|
+
// its live preview either couldn't take a doc-sourced hide back after
|
|
2152
|
+
// an un-toggle (the add-only trap those bases exist for) or would show
|
|
2153
|
+
// captions that a command.json pinned with --no-captions will never
|
|
2154
|
+
// actually render.
|
|
2155
|
+
...(captionsHidden ? { captionsHidden: true } : {}),
|
|
2156
|
+
...(opts.captions === false ? { captionsHiddenByFlag: true } : {}),
|
|
2094
2157
|
};
|
|
2095
2158
|
await writeFile(join(work, "render-props.json"), JSON.stringify(props, null, 2));
|
|
2096
2159
|
|
|
@@ -2161,13 +2224,16 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
2161
2224
|
// A separate file, not a burned-in intro: both platforms accept a custom
|
|
2162
2225
|
// cover, so nothing has to be pickable from the video — and spending the
|
|
2163
2226
|
// opening seconds on a title card fights the hook-in-2s policy directly.
|
|
2164
|
-
|
|
2227
|
+
{
|
|
2165
2228
|
// §35's cap applies here too: a cached beat sheet from before the fix, or
|
|
2166
2229
|
// the hook fallback, must not slip a 13-word paragraph onto a thumbnail.
|
|
2167
2230
|
const coverText = coverHeadline(beatSheet?.coverText ?? beatSheet?.hook ?? "");
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2231
|
+
// Urdu field run 2026-08-05: a run without --produce has no hook text,
|
|
2232
|
+
// and skipping the cover for that threw away the part that never needed
|
|
2233
|
+
// text — the sharpness-scored face frame. No headline now means a bare
|
|
2234
|
+
// frame, not no cover; see `coverDecision`'s doc comment.
|
|
2235
|
+
const cover = coverDecision(opts.cover !== false, coverText);
|
|
2236
|
+
if (cover !== "none") {
|
|
2171
2237
|
const detector = await createFaceDetector();
|
|
2172
2238
|
const pick = await pickCoverFrame(tools, analysisInput, analysisProbe.duration, {
|
|
2173
2239
|
cacheDir: work,
|
|
@@ -2195,31 +2261,45 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
2195
2261
|
const coverPath = resolve(
|
|
2196
2262
|
opts.coverPath ?? outPath.replace(/(\.[^.]+)?$/, ".cover.jpg"),
|
|
2197
2263
|
);
|
|
2198
|
-
// §34
|
|
2199
|
-
//
|
|
2200
|
-
//
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2264
|
+
// The §34 dedupe check and the band-placement log exist only to
|
|
2265
|
+
// route a banner around the frame's contents — a textless cover
|
|
2266
|
+
// (Urdu field run 2026-08-05) has no banner to place, so both are
|
|
2267
|
+
// skipped rather than run against text that isn't there.
|
|
2268
|
+
let bannerText = "";
|
|
2269
|
+
if (cover === "banner") {
|
|
2270
|
+
// §34: if the source's own title is up at this instant, the frame
|
|
2271
|
+
// already has a headline. Adding ours states the same claim twice in
|
|
2272
|
+
// one image — a cover with one title beats a cover with two.
|
|
2273
|
+
const sourceTitled = regionsDuring(
|
|
2274
|
+
sourceText.regions,
|
|
2275
|
+
pick.timeSec - 0.5,
|
|
2276
|
+
pick.timeSec + 0.5,
|
|
2277
|
+
).length > 0;
|
|
2278
|
+
console.log(
|
|
2279
|
+
`▸ cover from ${pick.timeSec.toFixed(1)}s ` +
|
|
2280
|
+
`(${pick.hasFace ? "face" : "no face"}, sharpness ${pick.sharpness.toFixed(0)})…`,
|
|
2281
|
+
);
|
|
2282
|
+
if (sourceTitled) {
|
|
2283
|
+
console.log(" ▸ source already has a title in this frame — shipping it without a banner");
|
|
2284
|
+
} else if (pick.face) {
|
|
2285
|
+
const band = coverTextRect(pick.face, frame);
|
|
2286
|
+
console.log(
|
|
2287
|
+
` ▸ banner in the ${band.y + band.h / 2 < pick.face.centerYFrac ? "band above" : "band below"} ` +
|
|
2288
|
+
`the face (${(band.y * 100).toFixed(0)}-${((band.y + band.h) * 100).toFixed(0)}%)`,
|
|
2289
|
+
);
|
|
2290
|
+
}
|
|
2291
|
+
bannerText = sourceTitled ? "" : coverText;
|
|
2292
|
+
} else {
|
|
2214
2293
|
console.log(
|
|
2215
|
-
|
|
2216
|
-
`
|
|
2294
|
+
`▸ cover from ${pick.timeSec.toFixed(1)}s ` +
|
|
2295
|
+
`(${pick.hasFace ? "face" : "no face"}, sharpness ${pick.sharpness.toFixed(0)}) ` +
|
|
2296
|
+
`— no banner text (run --produce for one)`,
|
|
2217
2297
|
);
|
|
2218
2298
|
}
|
|
2219
2299
|
await renderCover(
|
|
2220
2300
|
{
|
|
2221
2301
|
frameFileName: frameName,
|
|
2222
|
-
text:
|
|
2302
|
+
text: bannerText,
|
|
2223
2303
|
theme,
|
|
2224
2304
|
face: pick.face,
|
|
2225
2305
|
// The cover is the OUTPUT's thumbnail — a landscape render gets a
|
|
@@ -2267,10 +2347,17 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
|
|
|
2267
2347
|
// under a later/foreign config-on, an on-run would silently lose it. The
|
|
2268
2348
|
// RESOLVED state is always pinned; a typed flag is already in the argv and
|
|
2269
2349
|
// the includes-guard leaves it alone.
|
|
2350
|
+
// Captions pin: the FLAG's resolved state (`opts.captions ?? true`), never
|
|
2351
|
+
// the override-inclusive `captionsHidden` — overrides.json travels with
|
|
2352
|
+
// the workdir and is re-read on every replay, so pinning --no-captions
|
|
2353
|
+
// because the EDITOR hid them would freeze an edit the user may later
|
|
2354
|
+
// undo in that same editor. See recordedProduceArgs for why the pin is
|
|
2355
|
+
// unconditional even though captions' default is config-independent today.
|
|
2270
2356
|
const recordedArgs = recordedProduceArgs({
|
|
2271
2357
|
llm: provider ? providerName : undefined,
|
|
2272
2358
|
clipWindow: clipWindow ? `${clipWindow.startWord}:${clipWindow.endWord}` : undefined,
|
|
2273
2359
|
watermark,
|
|
2360
|
+
captions: opts.captions ?? true,
|
|
2274
2361
|
});
|
|
2275
2362
|
await writeFile(
|
|
2276
2363
|
join(work, "command.json"),
|
package/src/program.ts
CHANGED
|
@@ -270,6 +270,20 @@ export function buildProgram(): Command {
|
|
|
270
270
|
"(set it once with watermark: true in ~/.ossclip/config.json)",
|
|
271
271
|
)
|
|
272
272
|
.option("--no-watermark", "no wordmark, even when the config turns it on")
|
|
273
|
+
// Same tri-state shape as --watermark above (positive declared first so
|
|
274
|
+
// commander's default stays undefined = "not typed"), though captions
|
|
275
|
+
// have no config key to fill the gap: the tri-state exists so
|
|
276
|
+
// command.json can pin the resolved flag state for replay determinism
|
|
277
|
+
// (recordedProduceArgs) — a bare boolean default would make "not typed"
|
|
278
|
+
// and "typed --captions" indistinguishable to the pin.
|
|
279
|
+
.option(
|
|
280
|
+
"--captions",
|
|
281
|
+
"burned-in captions — already the default; exists so a recorded replay can pin the state",
|
|
282
|
+
)
|
|
283
|
+
.option(
|
|
284
|
+
"--no-captions",
|
|
285
|
+
"no burned-in captions. The CTA keyword styling rides the caption track, so it goes too",
|
|
286
|
+
)
|
|
273
287
|
.option("--no-cover", "skip the cover image written beside the video")
|
|
274
288
|
.option("--cover <path>", "cover image output path (default: <out>.cover.jpg)")
|
|
275
289
|
.option("--open-editor", "open the editor when the run finishes")
|
|
@@ -372,6 +386,9 @@ export function buildProgram(): Command {
|
|
|
372
386
|
sourceFit,
|
|
373
387
|
// undefined = "not typed", so produce can let the config decide.
|
|
374
388
|
watermark: opts.watermark,
|
|
389
|
+
// undefined = "not typed" here too — the default (ON) is applied at
|
|
390
|
+
// the pin site, not coerced in transit.
|
|
391
|
+
captions: opts.captions,
|
|
375
392
|
cover: opts.cover !== false,
|
|
376
393
|
coverPath: typeof opts.cover === "string" ? opts.cover : undefined,
|
|
377
394
|
clip: opts.clip,
|
package/src/replay-argv.ts
CHANGED
|
@@ -52,6 +52,7 @@ export function recordedProduceArgs(pins: {
|
|
|
52
52
|
llm?: string;
|
|
53
53
|
clipWindow?: string;
|
|
54
54
|
watermark?: boolean;
|
|
55
|
+
captions?: boolean;
|
|
55
56
|
}): string[] {
|
|
56
57
|
const args = consumeReplayArgv() ?? process.argv.slice(2);
|
|
57
58
|
if (pins.llm !== undefined && !args.includes("--llm")) {
|
|
@@ -74,5 +75,19 @@ export function recordedProduceArgs(pins: {
|
|
|
74
75
|
if (pins.watermark !== undefined && !args.includes("--watermark") && !args.includes("--no-watermark")) {
|
|
75
76
|
args.push(pins.watermark ? "--watermark" : "--no-watermark");
|
|
76
77
|
}
|
|
78
|
+
// The captions flag, pinned both ways like the watermark above — but here
|
|
79
|
+
// as future-proofing plus consistency rather than a live bug: captions'
|
|
80
|
+
// default is ON independent of any config today, so an argv without the
|
|
81
|
+
// flag would currently replay identically everywhere. Pinned anyway
|
|
82
|
+
// because (a) the watermark review already made "every record carries the
|
|
83
|
+
// RESOLVED state" §75's rule, and one unpinned tri-state would turn
|
|
84
|
+
// command.json's contract into per-flag trivia; and (b) the moment a
|
|
85
|
+
// config key or a changed default ever appears, every unpinned old record
|
|
86
|
+
// would silently re-resolve under it — the watermark's exact drift, just
|
|
87
|
+
// deferred. Note this pins the FLAG's state only; the editor's
|
|
88
|
+
// `captionsHidden` override is not folded in (see produce.ts's call site).
|
|
89
|
+
if (pins.captions !== undefined && !args.includes("--captions") && !args.includes("--no-captions")) {
|
|
90
|
+
args.push(pins.captions ? "--captions" : "--no-captions");
|
|
91
|
+
}
|
|
77
92
|
return args;
|
|
78
93
|
}
|