ossclip 0.1.13 → 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.
@@ -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-BBVLCP-N.js"></script>
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.13",
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.13",
40
- "@ossclip/renderer": "0.1.13",
41
- "@ossclip/scenes": "0.1.13"
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-two flags (plus the positional input path)
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) — nine behind
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), or
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
@@ -247,6 +247,15 @@ export interface ProduceOptions {
247
247
  * a free-tier limitation; this is voluntary attribution.
248
248
  */
249
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;
250
259
  /**
251
260
  * `<input>` a DIRECTORY: order its clips before concatenating them into the
252
261
  * source produce runs on (folder-input-brief.md). `name` (default) is a
@@ -281,6 +290,26 @@ export function resolveWatermark(
281
290
  return flag ?? configValue === true;
282
291
  }
283
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
+
284
313
  function sha1File(path: string): Promise<string> {
285
314
  return new Promise((res, rej) => {
286
315
  const h = createHash("sha1");
@@ -2007,6 +2036,21 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
2007
2036
  // that assembleScenes dropped, and the caption track knows exactly when the
2008
2037
  // ask is on screen. Quoting marks the word you type in the comments — every
2009
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
+
2010
2054
  const ctaCue = [...graphicCues]
2011
2055
  .reverse()
2012
2056
  .find((c) => typeof c.props?.keyword === "string" && (c.props.keyword as string).length > 0);
@@ -2015,9 +2059,15 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
2015
2059
  ? { startSec: ctaCue.startSec, endSec: ctaCue.endSec }
2016
2060
  : undefined;
2017
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.
2018
2066
  console.log(
2019
- `▸ CTA keyword "${ctaKeyword}" styled only at ` +
2020
- `${ctaWindow!.startSec.toFixed(1)}–${ctaWindow!.endSec.toFixed(1)}s`,
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`,
2021
2071
  );
2022
2072
  }
2023
2073
 
@@ -2092,6 +2142,18 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
2092
2142
  // an off run's render-props.json stays byte-identical to a pre-watermark
2093
2143
  // one, so nothing downstream can tell the feature ever shipped.
2094
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 } : {}),
2095
2157
  };
2096
2158
  await writeFile(join(work, "render-props.json"), JSON.stringify(props, null, 2));
2097
2159
 
@@ -2285,10 +2347,17 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<P
2285
2347
  // under a later/foreign config-on, an on-run would silently lose it. The
2286
2348
  // RESOLVED state is always pinned; a typed flag is already in the argv and
2287
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.
2288
2356
  const recordedArgs = recordedProduceArgs({
2289
2357
  llm: provider ? providerName : undefined,
2290
2358
  clipWindow: clipWindow ? `${clipWindow.startWord}:${clipWindow.endWord}` : undefined,
2291
2359
  watermark,
2360
+ captions: opts.captions ?? true,
2292
2361
  });
2293
2362
  await writeFile(
2294
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,
@@ -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
  }