ossclip 0.1.4 → 0.1.6
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-B5H_dKAW.js → index-DZhkQB1i.js} +24 -24
- package/editor-dist/index.html +1 -1
- package/package.json +5 -4
- package/src/doctor.ts +32 -10
- package/src/edit.ts +12 -2
- package/src/index.ts +6 -237
- package/src/interactive/edit-hint.ts +20 -0
- package/src/interactive/menu.ts +32 -0
- package/src/interactive/offer-editor.ts +71 -0
- package/src/interactive/pick-workdir.ts +21 -0
- package/src/interactive/prefs.ts +51 -0
- package/src/interactive/produce-argv.ts +57 -0
- package/src/interactive/produce-wizard.ts +180 -0
- package/src/interactive/prompts.ts +6 -0
- package/src/interactive/render.ts +30 -0
- package/src/interactive/resolve-workdir.ts +115 -0
- package/src/interactive/tty.ts +62 -0
- package/src/interactive/workdir-probe.ts +70 -0
- package/src/open.ts +30 -0
- package/src/produce.ts +138 -16
- package/src/program.ts +401 -0
- package/src/setup/download.ts +122 -0
- package/src/setup/extract.ts +91 -0
- package/src/setup/manifest.ts +159 -0
- package/src/setup/plan.ts +211 -0
- package/src/setup/provider.ts +51 -0
- package/src/setup/setup.ts +233 -0
package/src/produce.ts
CHANGED
|
@@ -37,6 +37,9 @@ import {
|
|
|
37
37
|
splitCues,
|
|
38
38
|
landscapeLayout,
|
|
39
39
|
formatCutReport,
|
|
40
|
+
formatGraphicsAccounting,
|
|
41
|
+
findBloopSpans,
|
|
42
|
+
formatBloopSpan,
|
|
40
43
|
formatUsageLine,
|
|
41
44
|
formatUsageReport,
|
|
42
45
|
loadConfig,
|
|
@@ -73,6 +76,7 @@ import {
|
|
|
73
76
|
sliceTranscript,
|
|
74
77
|
type Analysis,
|
|
75
78
|
type AppliedRepair,
|
|
79
|
+
type BeatsValidationIssue,
|
|
76
80
|
type CleanupLevel,
|
|
77
81
|
type ClipWindow,
|
|
78
82
|
type LlmProvider,
|
|
@@ -84,6 +88,7 @@ import {
|
|
|
84
88
|
type Transcript,
|
|
85
89
|
} from "@ossclip/core";
|
|
86
90
|
import { recordRecentProject } from "./edit";
|
|
91
|
+
import { editHint } from "./interactive/edit-hint";
|
|
87
92
|
import { renderCover, renderProduction } from "@ossclip/renderer";
|
|
88
93
|
import {
|
|
89
94
|
coverTextRect,
|
|
@@ -92,6 +97,17 @@ import {
|
|
|
92
97
|
routeAroundSourceText,
|
|
93
98
|
} from "@ossclip/scenes/geometry";
|
|
94
99
|
|
|
100
|
+
/**
|
|
101
|
+
* What a finished run tells its caller. The workdir is what the post-produce
|
|
102
|
+
* editor offer opens; `rendered` is false for a --no-render run, which has
|
|
103
|
+
* props but no video.
|
|
104
|
+
*/
|
|
105
|
+
export interface ProduceResult {
|
|
106
|
+
workdir: string;
|
|
107
|
+
out?: string;
|
|
108
|
+
rendered: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
95
111
|
export interface ProduceOptions {
|
|
96
112
|
out?: string;
|
|
97
113
|
cleanup: CleanupLevel;
|
|
@@ -125,6 +141,11 @@ export interface ProduceOptions {
|
|
|
125
141
|
coverPath?: string;
|
|
126
142
|
/** Treat the source as an already-edited reel with burned-in graphics. */
|
|
127
143
|
sourceIsEdited?: boolean;
|
|
144
|
+
/**
|
|
145
|
+
* Spoken blooper marker (R27 §122) — `--blooper-marker blooper`. Saying it
|
|
146
|
+
* on camera cuts the attempt it spoiled, back to that sentence's start.
|
|
147
|
+
*/
|
|
148
|
+
blooperMarker?: string;
|
|
128
149
|
/**
|
|
129
150
|
* How the source meets the vertical frame. `cover` (default) crops it to
|
|
130
151
|
* fill; `contain` shows the WHOLE frame inset against the backdrop, which is
|
|
@@ -181,7 +202,7 @@ async function preflight(bin: string, hint: string): Promise<void> {
|
|
|
181
202
|
}
|
|
182
203
|
}
|
|
183
204
|
|
|
184
|
-
export async function produce(inputArg: string, opts: ProduceOptions): Promise<
|
|
205
|
+
export async function produce(inputArg: string, opts: ProduceOptions): Promise<ProduceResult> {
|
|
185
206
|
const cfg = loadConfig();
|
|
186
207
|
const input = resolve(inputArg);
|
|
187
208
|
if (!existsSync(input)) throw new Error(`input not found: ${input}`);
|
|
@@ -206,8 +227,8 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<v
|
|
|
206
227
|
throw new Error("--clip-window is recorded by --clip runs for replay — pass --clip too.");
|
|
207
228
|
}
|
|
208
229
|
|
|
209
|
-
await preflight(cfg.ffmpegPath, "
|
|
210
|
-
await preflight(cfg.ffprobePath, "
|
|
230
|
+
await preflight(cfg.ffmpegPath, "Run `ossclip setup`, install ffmpeg yourself (brew/apt/winget), or set OSSCLIP_FFMPEG.");
|
|
231
|
+
await preflight(cfg.ffprobePath, "Run `ossclip setup`, install ffmpeg (provides ffprobe), or set OSSCLIP_FFPROBE.");
|
|
211
232
|
|
|
212
233
|
// The output frame — every rect downstream is a fraction of THIS, and the
|
|
213
234
|
// stage geometry now takes it as an argument rather than assuming portrait.
|
|
@@ -278,14 +299,15 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<v
|
|
|
278
299
|
} else {
|
|
279
300
|
await preflight(
|
|
280
301
|
cfg.whisperPath,
|
|
281
|
-
"
|
|
302
|
+
"Run `ossclip setup`, install whisper.cpp yourself (https://github.com/ggml-org/whisper.cpp), or set OSSCLIP_WHISPER.",
|
|
282
303
|
);
|
|
283
304
|
const model = opts.whisperModel ?? cfg.model;
|
|
284
305
|
const modelPath = isAbsolute(model) ? model : join(cfg.modelDir, `ggml-${model}.bin`);
|
|
285
306
|
if (!existsSync(modelPath)) {
|
|
286
307
|
throw new Error(
|
|
287
308
|
`whisper model not found at ${modelPath}.\n` +
|
|
288
|
-
`
|
|
309
|
+
`Run \`ossclip setup${model === cfg.model ? "" : ` --model ${model}`}\` to download it — or manually:\n` +
|
|
310
|
+
` curl -L -o ${modelPath} https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-${model}.bin`,
|
|
289
311
|
);
|
|
290
312
|
}
|
|
291
313
|
console.log(`▸ transcribing (${basename(modelPath)})…`);
|
|
@@ -310,11 +332,25 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<v
|
|
|
310
332
|
// `let`, not `const` (R19 §93): a clip run re-derives all three from the
|
|
311
333
|
// transcript sliced to the chosen window, further down.
|
|
312
334
|
let analysis: Analysis = analyze(transcript, silences, sourceProbe.duration, levels);
|
|
335
|
+
// Spoken blooper markers (R27 §122). Detected on the RAW transcript, before
|
|
336
|
+
// repair — the repair pass reads a bare "blooper." as an oddity and has
|
|
337
|
+
// already been observed proposing "break loop." for it. Detecting first
|
|
338
|
+
// means the marker cannot be rewritten out from under the detector.
|
|
339
|
+
let bloops = opts.blooperMarker ? findBloopSpans(transcript, opts.blooperMarker) : [];
|
|
340
|
+
if (opts.blooperMarker) {
|
|
341
|
+
console.log(
|
|
342
|
+
bloops.length > 0
|
|
343
|
+
? `▸ blooper marker "${opts.blooperMarker}": ${bloops.length} take(s) cut`
|
|
344
|
+
: `▸ blooper marker "${opts.blooperMarker}": never said — nothing cut`,
|
|
345
|
+
);
|
|
346
|
+
for (const b of bloops) console.log(` ▸ ${formatBloopSpan(transcript, b)}`);
|
|
347
|
+
}
|
|
313
348
|
let cutlist: Segment[] = buildCutlist({
|
|
314
349
|
transcript,
|
|
315
350
|
analysis,
|
|
316
351
|
duration: sourceProbe.duration,
|
|
317
352
|
level: opts.cleanup,
|
|
353
|
+
bloops,
|
|
318
354
|
});
|
|
319
355
|
let map = new TimeMap(cutlist);
|
|
320
356
|
|
|
@@ -446,6 +482,11 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<v
|
|
|
446
482
|
let scenes: Scene[] = [];
|
|
447
483
|
/** Editorial output kept for the cover (§31): hook + its thumbnail form. */
|
|
448
484
|
let beatSheet: { hook: string; coverText?: string } | undefined;
|
|
485
|
+
/** The graphics accounting line for report.txt (§118b), and the beat-sheet
|
|
486
|
+
* issues that explain it. Cached alongside the beat sheet so a cached
|
|
487
|
+
* re-run's report keeps the accounting instead of erasing it (§78). */
|
|
488
|
+
let graphicsLine: string | undefined;
|
|
489
|
+
let beatIssues: BeatsValidationIssue[] = [];
|
|
449
490
|
/** Who planned this run (R16 §78) — stamped into production.json below. */
|
|
450
491
|
let producerStamp: Production["producer"];
|
|
451
492
|
/** The resolved `--clip` window (R19 §93) — set only on a clip run; feeds
|
|
@@ -543,12 +584,16 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<v
|
|
|
543
584
|
rawTranscript = rawSlice.transcript;
|
|
544
585
|
transcript = sliceTranscript(transcript, clipWindow);
|
|
545
586
|
analysis = analyze(rawTranscript, silences, sourceProbe.duration, levels);
|
|
587
|
+
// Re-detect on the SLICE: word indices moved, so the spans found against
|
|
588
|
+
// the full take no longer address the same words.
|
|
589
|
+
bloops = opts.blooperMarker ? findBloopSpans(rawTranscript, opts.blooperMarker) : [];
|
|
546
590
|
cutlist = boundCutlistToWindow(
|
|
547
591
|
buildCutlist({
|
|
548
592
|
transcript: rawTranscript,
|
|
549
593
|
analysis,
|
|
550
594
|
duration: sourceProbe.duration,
|
|
551
595
|
level: opts.cleanup,
|
|
596
|
+
bloops,
|
|
552
597
|
}),
|
|
553
598
|
clipWindow,
|
|
554
599
|
sourceProbe.duration,
|
|
@@ -599,13 +644,33 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<v
|
|
|
599
644
|
for (const issue of clipFresh.beatIssues) {
|
|
600
645
|
console.log(` ⚠ moment ${issue.moment}: ${issue.issue}`);
|
|
601
646
|
}
|
|
647
|
+
beatIssues = clipFresh.beatIssues;
|
|
648
|
+
// `transcript` is the slice here — the space the accounting was made in.
|
|
649
|
+
graphicsLine = formatGraphicsAccounting(
|
|
650
|
+
clipFresh.graphics.delivered,
|
|
651
|
+
clipFresh.graphics.asked,
|
|
652
|
+
transcript,
|
|
653
|
+
);
|
|
602
654
|
await writeFile(sceneCache, JSON.stringify(scenes, null, 2));
|
|
603
|
-
await writeFile(
|
|
655
|
+
await writeFile(
|
|
656
|
+
beatCache,
|
|
657
|
+
JSON.stringify({ ...beatSheet, graphics: graphicsLine, issues: beatIssues }, null, 2),
|
|
658
|
+
);
|
|
604
659
|
} else if (existsSync(sceneCache)) {
|
|
605
660
|
scenes = z.array(SceneSchema).parse(JSON.parse(await readFile(sceneCache, "utf8")));
|
|
606
661
|
console.log(`▸ scenes cached (${scenes.length})`);
|
|
607
662
|
if (existsSync(beatCache)) {
|
|
608
|
-
|
|
663
|
+
// Pre-§118b caches carry no accounting — the report then simply
|
|
664
|
+
// omits the graphics section rather than guessing one.
|
|
665
|
+
const cached = JSON.parse(await readFile(beatCache, "utf8")) as {
|
|
666
|
+
hook: string;
|
|
667
|
+
coverText?: string;
|
|
668
|
+
graphics?: string;
|
|
669
|
+
issues?: BeatsValidationIssue[];
|
|
670
|
+
};
|
|
671
|
+
beatSheet = { hook: cached.hook, coverText: cached.coverText };
|
|
672
|
+
graphicsLine = cached.graphics;
|
|
673
|
+
beatIssues = cached.issues ?? [];
|
|
609
674
|
}
|
|
610
675
|
} else {
|
|
611
676
|
console.log(`▸ producing scenes (${providerName})…`);
|
|
@@ -629,11 +694,20 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<v
|
|
|
629
694
|
for (const issue of result.beatIssues) {
|
|
630
695
|
console.log(` ⚠ moment ${issue.moment}: ${issue.issue}`);
|
|
631
696
|
}
|
|
697
|
+
beatIssues = result.beatIssues;
|
|
698
|
+
graphicsLine = formatGraphicsAccounting(
|
|
699
|
+
result.graphics.delivered,
|
|
700
|
+
result.graphics.asked,
|
|
701
|
+
transcript,
|
|
702
|
+
);
|
|
632
703
|
// Cache props only — overrides are user-owned and live in overrides.json,
|
|
633
704
|
// never in production.json (that file is derived and every `produce`
|
|
634
705
|
// run overwrites it, per the merge rule in `overrides.ts`).
|
|
635
706
|
await writeFile(sceneCache, JSON.stringify(scenes, null, 2));
|
|
636
|
-
await writeFile(
|
|
707
|
+
await writeFile(
|
|
708
|
+
beatCache,
|
|
709
|
+
JSON.stringify({ ...beatSheet, graphics: graphicsLine, issues: beatIssues }, null, 2),
|
|
710
|
+
);
|
|
637
711
|
}
|
|
638
712
|
}
|
|
639
713
|
|
|
@@ -828,12 +902,23 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<v
|
|
|
828
902
|
// existing one — cropping through the source's title and then restating it
|
|
829
903
|
// underneath. Graphics move to a clear slot or are skipped; captions never
|
|
830
904
|
// are, they just relocate.
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
905
|
+
//
|
|
906
|
+
// Behind --source-is-edited since R27 §120. The detector cannot tell burned-in
|
|
907
|
+
// GRAPHICS from text that is simply in the room, and on a raw take at a desk
|
|
908
|
+
// it read the background monitors as 45 bands of "source text". The cost is
|
|
909
|
+
// not cosmetic: every graphic was then moved and SHRUNK to a free band —
|
|
910
|
+
// a BulletList pinned to its 36px font floor, a FlowDiagram's slot halved
|
|
911
|
+
// (0.54 → 0.27, type 71 → 35), and a ScreenshotFrame slid onto the speaker's
|
|
912
|
+
// face. Routing around a hazard only pays when there is a hazard, and only
|
|
913
|
+
// the user knows whether their source is already edited.
|
|
914
|
+
const sourceText = opts.sourceIsEdited
|
|
915
|
+
? await scanSourceText(tools, analysisInput, analysisProbe.duration, {
|
|
916
|
+
cacheDir: work,
|
|
917
|
+
assumeEdited: true,
|
|
918
|
+
cropVf: analysisCropVf,
|
|
919
|
+
cacheTag,
|
|
920
|
+
})
|
|
921
|
+
: { regions: [], assumed: false, framesSampled: 0 };
|
|
837
922
|
if (sourceText.regions.length > 0) {
|
|
838
923
|
console.log(
|
|
839
924
|
sourceText.assumed
|
|
@@ -965,7 +1050,21 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<v
|
|
|
965
1050
|
|
|
966
1051
|
// Grounding post-check (FINDINGS §14a): flags label tokens the take never
|
|
967
1052
|
// says — a hallucinated hook label is visible here without watching the video.
|
|
968
|
-
|
|
1053
|
+
//
|
|
1054
|
+
// Checked against the copy that will actually RENDER, overrides included
|
|
1055
|
+
// (R27 §124). It used to read the producer's raw scenes, so it was wrong in
|
|
1056
|
+
// both directions: it kept reporting invented copy the user had already
|
|
1057
|
+
// fixed by hand — the warning outlived the defect, which teaches people to
|
|
1058
|
+
// ignore warnings — and it never looked at copy the user typed themselves.
|
|
1059
|
+
// `checkGrounding` already merges a scene's `overrides` slot; nothing was
|
|
1060
|
+
// filling it from `overrides.json`.
|
|
1061
|
+
const scenesAsRendered = scenes.map((s) => {
|
|
1062
|
+
const edit = overrideDoc.scenes[s.id]?.props;
|
|
1063
|
+
return edit && Object.keys(edit).length > 0
|
|
1064
|
+
? { ...s, overrides: { ...s.overrides, ...edit } }
|
|
1065
|
+
: s;
|
|
1066
|
+
});
|
|
1067
|
+
const groundingIssues = checkGrounding(scenesAsRendered, transcript, opts.speaker ?? cfg.speaker);
|
|
969
1068
|
for (const g of groundingIssues) {
|
|
970
1069
|
console.log(` ⚠ grounding: ${g.component} ${g.sceneId} ${g.field} "${g.token}" — not in the take`);
|
|
971
1070
|
}
|
|
@@ -1050,6 +1149,14 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<v
|
|
|
1050
1149
|
`${((dur / sourceProbe.duration) * 100).toFixed(0)}% of the take)\n` +
|
|
1051
1150
|
` reason: ${clipWindow.reason}\n`;
|
|
1052
1151
|
}
|
|
1152
|
+
// §122: a cut that takes whole sentences owes the user the words it took —
|
|
1153
|
+
// the timestamps above say WHERE, not what was lost.
|
|
1154
|
+
if (bloops.length > 0) {
|
|
1155
|
+
report +=
|
|
1156
|
+
`\nbloopers cut (you said "${opts.blooperMarker}" — FINDINGS §122):\n` +
|
|
1157
|
+
bloops.map((b) => ` ${formatBloopSpan(rawTranscript, b)}`).join("\n") +
|
|
1158
|
+
"\n";
|
|
1159
|
+
}
|
|
1053
1160
|
const landed = repairs.filter((r) => r.applied);
|
|
1054
1161
|
if (landed.length > 0) {
|
|
1055
1162
|
report +=
|
|
@@ -1072,6 +1179,18 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<v
|
|
|
1072
1179
|
.join("\n") +
|
|
1073
1180
|
"\n";
|
|
1074
1181
|
}
|
|
1182
|
+
// §118b: the graphics count justified in the artefact, like every cut is —
|
|
1183
|
+
// delivered vs asked and why, then the scheduler's own account of what it
|
|
1184
|
+
// demoted. The shortfall issue repeats the accounting line, so it is the
|
|
1185
|
+
// one issue not reprinted here.
|
|
1186
|
+
if (graphicsLine) {
|
|
1187
|
+
report +=
|
|
1188
|
+
`\n${graphicsLine} (FINDINGS §118)\n` +
|
|
1189
|
+
beatIssues
|
|
1190
|
+
.filter((i) => !i.issue.startsWith("graphics:"))
|
|
1191
|
+
.map((i) => ` ⚠ moment ${i.moment}: ${i.issue}\n`)
|
|
1192
|
+
.join("");
|
|
1193
|
+
}
|
|
1075
1194
|
if (provider) {
|
|
1076
1195
|
report += formatUsageReport(provider.usage, cfg.pricing);
|
|
1077
1196
|
// A cached run has no usage block to print, and used to leave the report
|
|
@@ -1285,7 +1404,8 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<v
|
|
|
1285
1404
|
|
|
1286
1405
|
if (!opts.render) {
|
|
1287
1406
|
console.log(`▸ skipping render (--no-render). Props at ${join(work, "render-props.json")}`);
|
|
1288
|
-
|
|
1407
|
+
console.log(editHint(work));
|
|
1408
|
+
return { workdir: work, rendered: false };
|
|
1289
1409
|
}
|
|
1290
1410
|
|
|
1291
1411
|
const outPath = resolve(opts.out ?? input.replace(/(\.[^.]+)?$/, ".ossclip.mp4"));
|
|
@@ -1429,4 +1549,6 @@ export async function produce(inputArg: string, opts: ProduceOptions): Promise<v
|
|
|
1429
1549
|
// best-effort, so a read-only home dir never fails the render.
|
|
1430
1550
|
await recordRecentProject(work);
|
|
1431
1551
|
console.log(`✓ done → ${outPath}`);
|
|
1552
|
+
console.log(editHint(work));
|
|
1553
|
+
return { workdir: work, out: outPath, rendered: true };
|
|
1432
1554
|
}
|