ossclip 0.1.7 → 0.1.10
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 +159 -0
- package/editor-dist/assets/index-C8IPo60X.css +1 -0
- package/editor-dist/index.html +2 -1
- package/package.json +4 -4
- package/src/edit.ts +14 -4
- package/src/interactive/produce-argv.ts +2 -0
- package/src/interactive/produce-wizard.ts +44 -16
- package/src/produce.ts +600 -40
- package/src/program.ts +104 -27
- package/editor-dist/assets/index-otiVWeTH.js +0 -159
package/src/program.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { Command, InvalidArgumentError } from "commander";
|
|
5
5
|
import { z } from "zod/v4";
|
|
@@ -51,31 +51,83 @@ export function buildProgram(): Command {
|
|
|
51
51
|
// Bare `ossclip` at a TTY opens the menu. Piped or in CI it prints help,
|
|
52
52
|
// byte for byte what it printed before — a front door must not become a
|
|
53
53
|
// hang for a script.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
54
|
+
//
|
|
55
|
+
// Bare `ossclip <path>` ROUTES (0.1.9 first-contact, 2026-08-05): with no
|
|
56
|
+
// argument declared here, commander's default allow-excess-args silently
|
|
57
|
+
// DROPPED the positional — `ossclip "./Anyhropic c Compiler"` opened the
|
|
58
|
+
// menu as if nothing had been typed, and the user re-answered the wizard's
|
|
59
|
+
// input prompt by hand, wrongly, with all of ~/Downloads. Commander
|
|
60
|
+
// dispatches registered subcommand names before this action ever runs, so
|
|
61
|
+
// `produce`/`edit`/… always win over path interpretation (a folder that
|
|
62
|
+
// happens to be NAMED `doctor` goes to the subcommand — acceptable);
|
|
63
|
+
// anything that lands here is a path or a typo, and a typo must be a loud
|
|
64
|
+
// error naming what was tried, never the menu.
|
|
65
|
+
program
|
|
66
|
+
.argument(
|
|
67
|
+
"[path]",
|
|
68
|
+
"an existing video file or clips folder — shorthand for `ossclip produce <path>`, " +
|
|
69
|
+
"with the wizard asking the remaining questions",
|
|
70
|
+
)
|
|
71
|
+
// Commander 12 allows excess args by default, which is the SECOND half of
|
|
72
|
+
// the field failure (review, Important): the report's path was typed
|
|
73
|
+
// UNQUOTED — `ossclip ./Anyhropic c Compiler` — and with excess args
|
|
74
|
+
// allowed, `c` and `Compiler` would vanish wordlessly the moment
|
|
75
|
+
// `./Anyhropic` happened to exist, producing the wrong scope. An unquoted
|
|
76
|
+
// multi-word path must be a loud error, never a partial run.
|
|
77
|
+
.allowExcessArguments(false)
|
|
78
|
+
.action(async (path: string | undefined) => {
|
|
79
|
+
const { isInteractive } = await import("./interactive/tty");
|
|
80
|
+
if (path !== undefined) {
|
|
81
|
+
if (!existsSync(path)) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`no such file or directory: ${path}\n` +
|
|
84
|
+
" bare `ossclip <path>` produces an existing video file or clips folder;\n" +
|
|
85
|
+
" run `ossclip --help` for the commands.",
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const { renderCommand } = await import("./interactive/render");
|
|
89
|
+
// Both branches hand argv back through `program.parseAsync` — the same
|
|
90
|
+
// re-entry shape as the wizard paths below, for the same reason: the
|
|
91
|
+
// zod checks in the produce action must run on this input exactly as
|
|
92
|
+
// they do on a typed command line.
|
|
93
|
+
if (!isInteractive()) {
|
|
94
|
+
// No TTY means no wizard for the remaining questions, but the input
|
|
95
|
+
// IS supplied — a piped `ossclip <path>` is `ossclip produce <path>`.
|
|
96
|
+
const direct = ["produce", path];
|
|
97
|
+
console.log(`\n▸ running:\n ${renderCommand(direct)}\n`);
|
|
98
|
+
await program.parseAsync(["node", "ossclip", ...direct]);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const { produceWizard } = await import("./interactive/produce-wizard");
|
|
102
|
+
const { loadConfig } = await import("@ossclip/core");
|
|
103
|
+
const argv = await produceWizard({ speaker: loadConfig().speaker, input: path });
|
|
104
|
+
console.log(`\n▸ running:\n ${renderCommand(argv)}\n`);
|
|
105
|
+
await program.parseAsync(["node", "ossclip", ...argv]);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (!isInteractive()) {
|
|
109
|
+
program.outputHelp();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const { chooseFromMenu, menuArgv } = await import("./interactive/menu");
|
|
113
|
+
const choice = await chooseFromMenu();
|
|
114
|
+
const direct = menuArgv(choice);
|
|
115
|
+
const { renderCommand } = await import("./interactive/render");
|
|
116
|
+
if (direct !== null) {
|
|
117
|
+
// Echoed for the same reason the wizard echoes: the README promises
|
|
118
|
+
// "every choice prints the equivalent command before it runs, so the
|
|
119
|
+
// menu is also how you learn the flags", and three of the four entries
|
|
120
|
+
// printed nothing. The menu's whole pedagogical point is this line.
|
|
121
|
+
console.log(`\n▸ running:\n ${renderCommand(direct)}\n`);
|
|
122
|
+
await program.parseAsync(["node", "ossclip", ...direct]);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const { produceWizard } = await import("./interactive/produce-wizard");
|
|
126
|
+
const { loadConfig } = await import("@ossclip/core");
|
|
127
|
+
const argv = await produceWizard({ speaker: loadConfig().speaker });
|
|
128
|
+
console.log(`\n▸ running:\n ${renderCommand(argv)}\n`);
|
|
129
|
+
await program.parseAsync(["node", "ossclip", ...argv]);
|
|
130
|
+
});
|
|
79
131
|
|
|
80
132
|
program
|
|
81
133
|
.command("produce")
|
|
@@ -83,7 +135,10 @@ export function buildProgram(): Command {
|
|
|
83
135
|
// OPTIONAL so a bare `ossclip produce` at a TTY opens the wizard instead of
|
|
84
136
|
// printing a usage error at somebody who does not yet know the flags. A
|
|
85
137
|
// non-interactive run still gets commander's "missing required argument".
|
|
86
|
-
.argument(
|
|
138
|
+
.argument(
|
|
139
|
+
"[input]",
|
|
140
|
+
"input video file, or a folder of clips to concatenate (by name; see --sort)",
|
|
141
|
+
)
|
|
87
142
|
.option("-o, --out <path>", "output video path (default: <input>.ossclip.mp4)")
|
|
88
143
|
.option("--cleanup <level>", "exact | light | standard | aggressive", "standard")
|
|
89
144
|
.option("--transcript <path>", "inject a transcript JSON instead of running whisper")
|
|
@@ -95,6 +150,12 @@ export function buildProgram(): Command {
|
|
|
95
150
|
)
|
|
96
151
|
.option("--noise-db <db>", "override the measured silence threshold, e.g. -30", parseFloat)
|
|
97
152
|
.option("--workdir <dir>", "cache/work directory (default: <input dir>/.ossclip)")
|
|
153
|
+
.option(
|
|
154
|
+
"--sort <order>",
|
|
155
|
+
"when <input> is a folder: order its clips before concatenating them — " +
|
|
156
|
+
"name (default, plain codepoint sort, matches `ls`) or mtime (oldest first)",
|
|
157
|
+
"name",
|
|
158
|
+
)
|
|
98
159
|
.option(
|
|
99
160
|
"--aspect <ratio>",
|
|
100
161
|
"output shape: 9:16 (vertical, default) or 16:9 (landscape, 1920x1080)",
|
|
@@ -161,6 +222,12 @@ export function buildProgram(): Command {
|
|
|
161
222
|
"cut the flubbed take whenever you say this word out loud (e.g. blooper): " +
|
|
162
223
|
"removal runs back to the start of the sentence it spoiled. Off unless given",
|
|
163
224
|
)
|
|
225
|
+
.option(
|
|
226
|
+
"--collapse-retakes",
|
|
227
|
+
"deterministically collapse consecutive near-identical sentences, keeping only " +
|
|
228
|
+
"the last complete attempt — the flub the speaker did NOT mark. Off by default",
|
|
229
|
+
false,
|
|
230
|
+
)
|
|
164
231
|
.option("--no-cover", "skip the cover image written beside the video")
|
|
165
232
|
.option("--cover <path>", "cover image output path (default: <out>.cover.jpg)")
|
|
166
233
|
.option("--open-editor", "open the editor when the run finishes")
|
|
@@ -212,6 +279,13 @@ export function buildProgram(): Command {
|
|
|
212
279
|
// Parsed, not coerced: a typo'd `--source-fit containn` silently falling
|
|
213
280
|
// back to cover is exactly the crop the flag exists to prevent.
|
|
214
281
|
const sourceFit = z.enum(["cover", "contain"]).parse(opts.sourceFit);
|
|
282
|
+
// Same reasoning as --source-fit: a typo'd --sort dat must not silently
|
|
283
|
+
// become the default rather than an error naming the mistake.
|
|
284
|
+
const sort = z.enum(["name", "mtime"]).parse(opts.sort);
|
|
285
|
+
// Final-review fix wave, cheap minor c: distinguishes "the user typed
|
|
286
|
+
// --sort" from "commander's own default filled it in" so produce() can
|
|
287
|
+
// say something when --sort is given for a file, where it does nothing.
|
|
288
|
+
const sortExplicit = command.getOptionValueSource("sort") === "cli";
|
|
215
289
|
const result = await produce(input, {
|
|
216
290
|
out: opts.out,
|
|
217
291
|
cleanup,
|
|
@@ -219,6 +293,8 @@ export function buildProgram(): Command {
|
|
|
219
293
|
render: opts.render,
|
|
220
294
|
mezzanine: opts.mezzanine,
|
|
221
295
|
workdir: opts.workdir,
|
|
296
|
+
sort,
|
|
297
|
+
sortExplicit,
|
|
222
298
|
aspect: opts.aspect === "16:9" ? "16:9" : "9:16",
|
|
223
299
|
noiseDb: opts.noiseDb,
|
|
224
300
|
produce: opts.produce,
|
|
@@ -235,6 +311,7 @@ export function buildProgram(): Command {
|
|
|
235
311
|
// string on the same key.
|
|
236
312
|
sourceIsEdited: opts.sourceIsEdited === true,
|
|
237
313
|
blooperMarker: opts.blooperMarker,
|
|
314
|
+
collapseRetakes: opts.collapseRetakes,
|
|
238
315
|
sourceFit,
|
|
239
316
|
cover: opts.cover !== false,
|
|
240
317
|
coverPath: typeof opts.cover === "string" ? opts.cover : undefined,
|