ossclip 0.1.11 → 0.1.13
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-Bo0WME7k.js → index-BBVLCP-N.js} +24 -24
- package/editor-dist/index.html +1 -1
- package/package.json +4 -4
- package/src/interactive/produce-argv.ts +9 -0
- package/src/interactive/produce-wizard.ts +133 -17
- package/src/produce.ts +211 -32
- package/src/program.ts +57 -3
- package/src/replay-argv.ts +19 -1
- package/src/stranded-overrides.ts +77 -0
package/src/program.ts
CHANGED
|
@@ -106,7 +106,17 @@ export function buildProgram(): Command {
|
|
|
106
106
|
}
|
|
107
107
|
const { produceWizard } = await import("./interactive/produce-wizard");
|
|
108
108
|
const { loadConfig } = await import("@ossclip/core");
|
|
109
|
-
const
|
|
109
|
+
const cfg = loadConfig();
|
|
110
|
+
// modelDir so the wizard can enumerate installed whisper models
|
|
111
|
+
// (Urdu field test 2026-08-05) — same resolution produce.ts uses.
|
|
112
|
+
// watermark so the extras entry can say when the config already has
|
|
113
|
+
// the credit on (unchecked ≠ off there — review, minor a).
|
|
114
|
+
const argv = await produceWizard({
|
|
115
|
+
speaker: cfg.speaker,
|
|
116
|
+
modelDir: cfg.modelDir,
|
|
117
|
+
input: path,
|
|
118
|
+
watermark: cfg.watermark,
|
|
119
|
+
});
|
|
110
120
|
console.log(`\n▸ running:\n ${renderCommand(argv)}\n`);
|
|
111
121
|
setReplayArgv(argv); // §129
|
|
112
122
|
await program.parseAsync(["node", "ossclip", ...argv]);
|
|
@@ -136,7 +146,12 @@ export function buildProgram(): Command {
|
|
|
136
146
|
}
|
|
137
147
|
const { produceWizard } = await import("./interactive/produce-wizard");
|
|
138
148
|
const { loadConfig } = await import("@ossclip/core");
|
|
139
|
-
const
|
|
149
|
+
const cfg = loadConfig();
|
|
150
|
+
const argv = await produceWizard({
|
|
151
|
+
speaker: cfg.speaker,
|
|
152
|
+
modelDir: cfg.modelDir,
|
|
153
|
+
watermark: cfg.watermark,
|
|
154
|
+
});
|
|
140
155
|
console.log(`\n▸ running:\n ${renderCommand(argv)}\n`);
|
|
141
156
|
setReplayArgv(argv); // §129
|
|
142
157
|
await program.parseAsync(["node", "ossclip", ...argv]);
|
|
@@ -215,6 +230,10 @@ export function buildProgram(): Command {
|
|
|
215
230
|
"skip the ASR mishearing repair pass (captions then show the raw transcription)",
|
|
216
231
|
)
|
|
217
232
|
.option("--whisper-model <name>", "transcription model for this run, e.g. base.en | small.en | medium.en")
|
|
233
|
+
.option(
|
|
234
|
+
"--whisper-language <code>",
|
|
235
|
+
"transcription language code for a multilingual model, e.g. ur | de | auto (whisper defaults to en)",
|
|
236
|
+
)
|
|
218
237
|
.option(
|
|
219
238
|
"--force-component <id>",
|
|
220
239
|
"debug: render every graphic with this component (e.g. FlowDiagram) to exercise it on real copy",
|
|
@@ -241,6 +260,16 @@ export function buildProgram(): Command {
|
|
|
241
260
|
"the last complete attempt — the flub the speaker did NOT mark. Off by default",
|
|
242
261
|
false,
|
|
243
262
|
)
|
|
263
|
+
// Declared as the same tri-state pair as --open-editor/--no-open-editor:
|
|
264
|
+
// positive first so commander's default stays undefined ("not typed"),
|
|
265
|
+
// which is what lets the config supply the default while a typed
|
|
266
|
+
// --no-watermark still beats a config-on.
|
|
267
|
+
.option(
|
|
268
|
+
"--watermark",
|
|
269
|
+
'credit the tool: a small "made with ossclip" wordmark in the top-left safe area ' +
|
|
270
|
+
"(set it once with watermark: true in ~/.ossclip/config.json)",
|
|
271
|
+
)
|
|
272
|
+
.option("--no-watermark", "no wordmark, even when the config turns it on")
|
|
244
273
|
.option("--no-cover", "skip the cover image written beside the video")
|
|
245
274
|
.option("--cover <path>", "cover image output path (default: <out>.cover.jpg)")
|
|
246
275
|
.option("--open-editor", "open the editor when the run finishes")
|
|
@@ -272,7 +301,12 @@ export function buildProgram(): Command {
|
|
|
272
301
|
const { produceWizard } = await import("./interactive/produce-wizard");
|
|
273
302
|
const { renderCommand } = await import("./interactive/render");
|
|
274
303
|
const { loadConfig } = await import("@ossclip/core");
|
|
275
|
-
const
|
|
304
|
+
const cfg = loadConfig();
|
|
305
|
+
const argv = await produceWizard({
|
|
306
|
+
speaker: cfg.speaker,
|
|
307
|
+
modelDir: cfg.modelDir,
|
|
308
|
+
watermark: cfg.watermark,
|
|
309
|
+
});
|
|
276
310
|
console.log(`\n▸ running:\n ${renderCommand(argv)}\n`);
|
|
277
311
|
// Re-entering the SAME parse the flags take: the zod checks below run
|
|
278
312
|
// on wizard output exactly as they do on a typed command line.
|
|
@@ -300,6 +334,14 @@ export function buildProgram(): Command {
|
|
|
300
334
|
// --sort" from "commander's own default filled it in" so produce() can
|
|
301
335
|
// say something when --sort is given for a file, where it does nothing.
|
|
302
336
|
const sortExplicit = command.getOptionValueSource("sort") === "cli";
|
|
337
|
+
// Not an enum — whisper accepts dozens of codes plus "auto" and the list
|
|
338
|
+
// grows with fine-tunes — but an empty string would reach whisper as a
|
|
339
|
+
// bare `-l` and must be an error naming the flag, not a silent English
|
|
340
|
+
// run over an Urdu model (Urdu field test 2026-08-05).
|
|
341
|
+
const whisperLanguage =
|
|
342
|
+
opts.whisperLanguage !== undefined
|
|
343
|
+
? z.string().trim().min(1, "--whisper-language needs a code, e.g. ur").parse(opts.whisperLanguage)
|
|
344
|
+
: undefined;
|
|
303
345
|
const result = await produce(input, {
|
|
304
346
|
out: opts.out,
|
|
305
347
|
cleanup,
|
|
@@ -320,6 +362,7 @@ export function buildProgram(): Command {
|
|
|
320
362
|
scenes: opts.scenes,
|
|
321
363
|
repair: opts.repair,
|
|
322
364
|
whisperModel: opts.whisperModel,
|
|
365
|
+
whisperLanguage,
|
|
323
366
|
forceComponent,
|
|
324
367
|
// commander gives `--no-cover` as cover:false and `--cover <path>` as a
|
|
325
368
|
// string on the same key.
|
|
@@ -327,6 +370,8 @@ export function buildProgram(): Command {
|
|
|
327
370
|
blooperMarker: opts.blooperMarker,
|
|
328
371
|
collapseRetakes: opts.collapseRetakes,
|
|
329
372
|
sourceFit,
|
|
373
|
+
// undefined = "not typed", so produce can let the config decide.
|
|
374
|
+
watermark: opts.watermark,
|
|
330
375
|
cover: opts.cover !== false,
|
|
331
376
|
coverPath: typeof opts.cover === "string" ? opts.cover : undefined,
|
|
332
377
|
clip: opts.clip,
|
|
@@ -345,6 +390,10 @@ export function buildProgram(): Command {
|
|
|
345
390
|
.option("--noise-db <db>", "override the measured silence threshold, e.g. -30", parseFloat)
|
|
346
391
|
.option("--workdir <dir>", "cache/work directory")
|
|
347
392
|
.option("--whisper-model <name>", "transcription model for this run, e.g. base.en | small.en | medium.en")
|
|
393
|
+
.option(
|
|
394
|
+
"--whisper-language <code>",
|
|
395
|
+
"transcription language code for a multilingual model, e.g. ur | de | auto (whisper defaults to en)",
|
|
396
|
+
)
|
|
348
397
|
.action(async (input: string, opts) => {
|
|
349
398
|
const cleanup = CleanupLevelSchema.parse(opts.cleanup);
|
|
350
399
|
await produce(input, {
|
|
@@ -355,6 +404,11 @@ export function buildProgram(): Command {
|
|
|
355
404
|
workdir: opts.workdir,
|
|
356
405
|
noiseDb: opts.noiseDb,
|
|
357
406
|
whisperModel: opts.whisperModel,
|
|
407
|
+
// Same guard as produce's: empty must error, not become a bare `-l`.
|
|
408
|
+
whisperLanguage:
|
|
409
|
+
opts.whisperLanguage !== undefined
|
|
410
|
+
? z.string().trim().min(1, "--whisper-language needs a code, e.g. ur").parse(opts.whisperLanguage)
|
|
411
|
+
: undefined,
|
|
358
412
|
});
|
|
359
413
|
});
|
|
360
414
|
|
package/src/replay-argv.ts
CHANGED
|
@@ -48,7 +48,11 @@ export function consumeReplayArgv(): string[] | null {
|
|
|
48
48
|
* `includes` so a flag the user actually typed — or a pin recorded by the
|
|
49
49
|
* run a replay is re-running — is never appended twice.
|
|
50
50
|
*/
|
|
51
|
-
export function recordedProduceArgs(pins: {
|
|
51
|
+
export function recordedProduceArgs(pins: {
|
|
52
|
+
llm?: string;
|
|
53
|
+
clipWindow?: string;
|
|
54
|
+
watermark?: boolean;
|
|
55
|
+
}): string[] {
|
|
52
56
|
const args = consumeReplayArgv() ?? process.argv.slice(2);
|
|
53
57
|
if (pins.llm !== undefined && !args.includes("--llm")) {
|
|
54
58
|
args.push("--llm", pins.llm);
|
|
@@ -56,5 +60,19 @@ export function recordedProduceArgs(pins: { llm?: string; clipWindow?: string })
|
|
|
56
60
|
if (pins.clipWindow !== undefined && !args.includes("--clip-window")) {
|
|
57
61
|
args.push("--clip-window", pins.clipWindow);
|
|
58
62
|
}
|
|
63
|
+
// The watermark, pinned like §75 pinned the provider — in BOTH directions
|
|
64
|
+
// (review, Important): the effective default is config-dependent, so "no
|
|
65
|
+
// flag in the argv" does not replay identically everywhere. An off-record
|
|
66
|
+
// left unpinned would silently GAIN a watermark the moment the replay runs
|
|
67
|
+
// under a config-on (`~/.ossclip/config.json` edited later, or the editor's
|
|
68
|
+
// Render on another machine) — the exact drift §75 exists to prevent, just
|
|
69
|
+
// mirrored. So every record carries the RESOLVED state: `--watermark` when
|
|
70
|
+
// on, `--no-watermark` when off, and a typed flag (already in the argv,
|
|
71
|
+
// caught by the includes-guard) is never doubled. One flag per command.json
|
|
72
|
+
// is the price; determinism is the contract, byte-identity of off-records
|
|
73
|
+
// was only ever a nicety.
|
|
74
|
+
if (pins.watermark !== undefined && !args.includes("--watermark") && !args.includes("--no-watermark")) {
|
|
75
|
+
args.push(pins.watermark ? "--watermark" : "--no-watermark");
|
|
76
|
+
}
|
|
59
77
|
return args;
|
|
60
78
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* §131's residue: renaming/adding/removing clips in a folder input re-keys
|
|
5
|
+
* the content hash, so produce derives a FRESH workdir — correct for every
|
|
6
|
+
* cache (a stale transcript against a different edit is the bug §131's
|
|
7
|
+
* manifest hashing exists to prevent), but the OLD workdir's editor edits
|
|
8
|
+
* (`overrides.json`) are user-owned work that silently stops being found.
|
|
9
|
+
* The user sees a clean produce and never learns their edits live one
|
|
10
|
+
* sibling directory over. This module is the pure half of the pointer that
|
|
11
|
+
* closes that gap: given the sibling directory listing, decide WHICH
|
|
12
|
+
* directories to point at. All filesystem reads stay in produce.ts so the
|
|
13
|
+
* cross-folder matching rules below are testable without a disk.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The `<basename>-<hash8>` naming's basename half, shared with
|
|
18
|
+
* `deriveWorkdir` (produce.ts) so the sibling scan can never disagree with
|
|
19
|
+
* the naming scheme it scans for — a drift between the two would make every
|
|
20
|
+
* pointer silently miss.
|
|
21
|
+
*/
|
|
22
|
+
export function workdirBaseName(identity: string): string {
|
|
23
|
+
return basename(identity).replace(/\.[^.]+$/, "");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SiblingWorkdirEntry {
|
|
27
|
+
/** Directory entry name inside the workdir root (not a full path). */
|
|
28
|
+
name: string;
|
|
29
|
+
hasOverrides: boolean;
|
|
30
|
+
/** mtime of the entry's overrides.json — 0 when it has none. */
|
|
31
|
+
mtimeMs: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Cap on printed pointers — a folder edited across many re-keys must not flood the run log. */
|
|
35
|
+
export const MAX_STRANDED_POINTERS = 3;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Which sibling workdirs hold stranded editor edits for this folder, newest
|
|
39
|
+
* edit first. Matching is prefix + exactly-8-lowercase-hex (sha1's own
|
|
40
|
+
* alphabet), NOT a loose startsWith: `MyClips2-bbbbbbbb` must never match
|
|
41
|
+
* base `MyClips` (a different folder entirely), and requiring the hash shape
|
|
42
|
+
* after the `-` is what rules it out — `2-bbbbbbbb` is not 8 hex. The
|
|
43
|
+
* optional `-16x9` tail mirrors deriveWorkdir's landscape suffix; edits
|
|
44
|
+
* stranded in either aspect's workdir are still this folder's edits. Same
|
|
45
|
+
* hash in the other aspect is excluded: that workdir is reachable by
|
|
46
|
+
* re-running with the other --aspect, not stranded by a re-key.
|
|
47
|
+
*/
|
|
48
|
+
export function strandedOverrideSiblings(p: {
|
|
49
|
+
base: string;
|
|
50
|
+
/** This run's 8-hex content hash — its own workdirs are not "previous". */
|
|
51
|
+
currentHash: string;
|
|
52
|
+
entries: SiblingWorkdirEntry[];
|
|
53
|
+
}): string[] {
|
|
54
|
+
const prefix = `${p.base}-`;
|
|
55
|
+
return p.entries
|
|
56
|
+
.filter((e) => {
|
|
57
|
+
if (!e.hasOverrides || !e.name.startsWith(prefix)) return false;
|
|
58
|
+
const m = /^([0-9a-f]{8})(-16x9)?$/.exec(e.name.slice(prefix.length));
|
|
59
|
+
return m !== null && m[1] !== p.currentHash;
|
|
60
|
+
})
|
|
61
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs)
|
|
62
|
+
.slice(0, MAX_STRANDED_POINTERS)
|
|
63
|
+
.map((e) => e.name);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The printed pointer. Says "for this folder" deliberately loosely — two
|
|
68
|
+
* different folders sharing a basename under one --workdir root can collide
|
|
69
|
+
* into this list, and the wording stays honest for that case rather than
|
|
70
|
+
* claiming a provenance the name alone can't prove (§131 brief).
|
|
71
|
+
*/
|
|
72
|
+
export function strandedPointerLine(path: string): string {
|
|
73
|
+
return (
|
|
74
|
+
`▸ previous project for this folder: ${path} ` +
|
|
75
|
+
`(has editor edits — they don't carry over; open it with: ossclip edit '${path}')`
|
|
76
|
+
);
|
|
77
|
+
}
|