ossclip 0.1.13 → 0.1.15
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-CdwzWN3j.js} +28 -25
- 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 +71 -2
- package/src/program.ts +23 -2
- 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-CdwzWN3j.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.15",
|
|
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/renderer": "0.1.
|
|
41
|
-
"@ossclip/scenes": "0.1.
|
|
39
|
+
"@ossclip/core": "0.1.15",
|
|
40
|
+
"@ossclip/renderer": "0.1.15",
|
|
41
|
+
"@ossclip/scenes": "0.1.15"
|
|
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
|
@@ -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
|
-
|
|
2020
|
-
|
|
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
|
@@ -212,8 +212,12 @@ export function buildProgram(): Command {
|
|
|
212
212
|
.option("--intent <text>", "what the video should be ('educational video about agents…')")
|
|
213
213
|
.option(
|
|
214
214
|
"--llm <provider>",
|
|
215
|
-
|
|
216
|
-
|
|
215
|
+
// Must state `defaultProviderName`'s real order — the old text omitted
|
|
216
|
+
// the GEMINI-first branch and promised claude-first (field report
|
|
217
|
+
// 2026-08-07); the drift test in llm-help.test.ts pins the agreement.
|
|
218
|
+
"claude | claude-cli | gemini | mock. Default: gemini if GEMINI_API_KEY is set, " +
|
|
219
|
+
"else claude if ANTHROPIC_API_KEY is set, else claude-cli (your logged-in " +
|
|
220
|
+
"Claude Code — Pro/Max subscription, no API charges)",
|
|
217
221
|
)
|
|
218
222
|
.option("--llm-model <id>", "override the provider's default model")
|
|
219
223
|
.option(
|
|
@@ -270,6 +274,20 @@ export function buildProgram(): Command {
|
|
|
270
274
|
"(set it once with watermark: true in ~/.ossclip/config.json)",
|
|
271
275
|
)
|
|
272
276
|
.option("--no-watermark", "no wordmark, even when the config turns it on")
|
|
277
|
+
// Same tri-state shape as --watermark above (positive declared first so
|
|
278
|
+
// commander's default stays undefined = "not typed"), though captions
|
|
279
|
+
// have no config key to fill the gap: the tri-state exists so
|
|
280
|
+
// command.json can pin the resolved flag state for replay determinism
|
|
281
|
+
// (recordedProduceArgs) — a bare boolean default would make "not typed"
|
|
282
|
+
// and "typed --captions" indistinguishable to the pin.
|
|
283
|
+
.option(
|
|
284
|
+
"--captions",
|
|
285
|
+
"burned-in captions — already the default; exists so a recorded replay can pin the state",
|
|
286
|
+
)
|
|
287
|
+
.option(
|
|
288
|
+
"--no-captions",
|
|
289
|
+
"no burned-in captions. The CTA keyword styling rides the caption track, so it goes too",
|
|
290
|
+
)
|
|
273
291
|
.option("--no-cover", "skip the cover image written beside the video")
|
|
274
292
|
.option("--cover <path>", "cover image output path (default: <out>.cover.jpg)")
|
|
275
293
|
.option("--open-editor", "open the editor when the run finishes")
|
|
@@ -372,6 +390,9 @@ export function buildProgram(): Command {
|
|
|
372
390
|
sourceFit,
|
|
373
391
|
// undefined = "not typed", so produce can let the config decide.
|
|
374
392
|
watermark: opts.watermark,
|
|
393
|
+
// undefined = "not typed" here too — the default (ON) is applied at
|
|
394
|
+
// the pin site, not coerced in transit.
|
|
395
|
+
captions: opts.captions,
|
|
375
396
|
cover: opts.cover !== false,
|
|
376
397
|
coverPath: typeof opts.cover === "string" ? opts.cover : undefined,
|
|
377
398
|
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
|
}
|