ossclip 0.1.23 → 0.1.25
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-C9n6EfII.js +163 -0
- package/editor-dist/index.html +1 -1
- package/package.json +4 -4
- package/src/analyze.ts +17 -1
- package/src/doctor.ts +7 -5
- package/src/edit.ts +496 -4
- package/src/interactive/ask-input.ts +11 -3
- package/src/interactive/pick-save-path.ts +214 -0
- package/src/interactive/produce-argv.ts +39 -2
- package/src/interactive/produce-wizard.ts +236 -35
- package/src/interactive/thumbnail-approve.ts +217 -0
- package/src/open.ts +26 -8
- package/src/paths.ts +88 -0
- package/src/portrait-override.ts +86 -0
- package/src/produce.ts +1607 -133
- package/src/program.ts +104 -6
- package/src/replay-argv.ts +75 -0
- package/src/setup/manifest.ts +93 -4
- package/src/setup/plan.ts +17 -6
- package/src/setup/setup.ts +28 -7
- package/src/thumbnail-panel.ts +167 -0
- package/src/ui/animation.ts +18 -4
- package/editor-dist/assets/index-MLGz89mM.js +0 -163
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { THUMBNAIL_MODEL_DEFAULT, thumbnailDecision } from "@ossclip/core";
|
|
2
|
+
import { resolvePortrait, type PortraitSource } from "./portrait-override";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The editor thumbnail panel's availability decision (2026-08-17), pure so
|
|
6
|
+
* the whole pins × config × env matrix is a table test with no server, no
|
|
7
|
+
* filesystem and no real ~/.ossclip. The I/O half lives in edit.ts's
|
|
8
|
+
* /api/thumbnail handler, which gathers command.json, loadConfig() and the
|
|
9
|
+
* env and hands them here.
|
|
10
|
+
*
|
|
11
|
+
* Resolution reads command.json's recorded args FIRST: produce pins the
|
|
12
|
+
* RESOLVED `--youtube`/`--no-youtube` and `--portrait` into every record
|
|
13
|
+
* (replay-argv.ts's determinism contract), so the pins are the replay truth
|
|
14
|
+
* — what the editor's Render would actually do — and the config is only the
|
|
15
|
+
* fallback for a workdir with no record (or a pre-pin legacy one).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The last value following any of `names` in a recorded argv — last wins,
|
|
20
|
+
* commander's own rule, so a typed flag plus a pinned one resolves the way
|
|
21
|
+
* the replay would. `undefined` when no occurrence carries a value.
|
|
22
|
+
*/
|
|
23
|
+
export function lastFlagValue(
|
|
24
|
+
args: readonly string[],
|
|
25
|
+
names: readonly string[],
|
|
26
|
+
): string | undefined {
|
|
27
|
+
for (let i = args.length - 2; i >= 0; i--) {
|
|
28
|
+
if (names.includes(args[i]!)) return args[i + 1];
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The last of an on/off flag pair in a recorded argv, or `undefined` when
|
|
35
|
+
* neither appears (the config decides). Last wins, as above.
|
|
36
|
+
*/
|
|
37
|
+
export function lastBoolFlag(
|
|
38
|
+
args: readonly string[],
|
|
39
|
+
on: string,
|
|
40
|
+
off: string,
|
|
41
|
+
): boolean | undefined {
|
|
42
|
+
for (let i = args.length - 1; i >= 0; i--) {
|
|
43
|
+
if (args[i] === on) return true;
|
|
44
|
+
if (args[i] === off) return false;
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The panel's reason vocabulary — why the controls are absent (unavailable),
|
|
50
|
+
* why nothing shows yet (never-generated), or why the user opted out
|
|
51
|
+
* (skip-file). */
|
|
52
|
+
export type ThumbnailPanelReason =
|
|
53
|
+
| "no-youtube"
|
|
54
|
+
| "no-portrait"
|
|
55
|
+
| "no-key"
|
|
56
|
+
| "portrait-missing"
|
|
57
|
+
| "skip-file"
|
|
58
|
+
| "never-generated";
|
|
59
|
+
|
|
60
|
+
export interface ThumbnailPanelState {
|
|
61
|
+
/** ready = controls shown; skipped = user declined at approval (controls
|
|
62
|
+
* still shown — regenerating replaces the decision); unavailable = the
|
|
63
|
+
* preconditions for generating are missing, reason says which. */
|
|
64
|
+
status: "ready" | "skipped" | "unavailable";
|
|
65
|
+
reason?: ThumbnailPanelReason;
|
|
66
|
+
/** The resolved image model slug, reported so the panel can name it. */
|
|
67
|
+
model: string;
|
|
68
|
+
/** The resolved portrait path, when one resolved — the regenerate
|
|
69
|
+
* endpoint's likeness reference. */
|
|
70
|
+
portraitPath?: string;
|
|
71
|
+
/** Which portrait won (override > flag > config) — the panel's swap strip
|
|
72
|
+
* labels itself from this. Present exactly when portraitPath is. */
|
|
73
|
+
portraitSource?: PortraitSource;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ThumbnailPanelInputs {
|
|
77
|
+
/** command.json's recorded args, or null when the workdir has none. */
|
|
78
|
+
commandArgs: readonly string[] | null;
|
|
79
|
+
/** loadConfig()'s relevant keys — hand-edited JSON, validated HERE (the
|
|
80
|
+
* `portrait` posture: `typeof`/`=== true`, never truthiness). */
|
|
81
|
+
cfg: { youtube?: unknown; portrait?: unknown; thumbnailModel?: unknown };
|
|
82
|
+
/** GEMINI_API_KEY present and non-empty — env-only, secrets never in
|
|
83
|
+
* config.json (env.ts rule). */
|
|
84
|
+
hasKey: boolean;
|
|
85
|
+
/** The approved file holds `{skip: true}` — the user declined this one. */
|
|
86
|
+
approvedSkip: boolean;
|
|
87
|
+
/** Whether ANY concept exists (approved or cached) — with hasImage below,
|
|
88
|
+
* distinguishes "ready" from "ready but nothing ever generated". */
|
|
89
|
+
hasConcept: boolean;
|
|
90
|
+
/** Whether a generated thumbnail image exists (dest or workdir cache). */
|
|
91
|
+
hasImage: boolean;
|
|
92
|
+
/** Injected existence check so the matrix needs no filesystem. */
|
|
93
|
+
portraitExists: (path: string) => boolean;
|
|
94
|
+
/** The workdir's portrait-override file when one exists (editor face swap,
|
|
95
|
+
* 2026-08-17) — found by the CALLER via portraitOverridePath, so this
|
|
96
|
+
* matrix stays filesystem-free. When set it outranks the pin AND the
|
|
97
|
+
* config, and it alone satisfies the no-portrait gate: a project whose
|
|
98
|
+
* only portrait is a swapped face must still be able to regenerate. */
|
|
99
|
+
overridePortraitPath?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function thumbnailPanelState(inputs: ThumbnailPanelInputs): ThumbnailPanelState {
|
|
103
|
+
const args = inputs.commandArgs ?? [];
|
|
104
|
+
// resolveYoutube's exact rule (produce.ts), restated rather than imported —
|
|
105
|
+
// edit.ts must not pull produce.ts's import graph (see paths.ts's
|
|
106
|
+
// artifactPath note): pin beats config, and the config side is `=== true`,
|
|
107
|
+
// never truthiness, because a typo'd `"youtube": "no"` must read as off.
|
|
108
|
+
//
|
|
109
|
+
// Existing artifacts beat flag archaeology (field case 2026-08-17): a
|
|
110
|
+
// produce run that CRASHED between the thumbnail step and the command.json
|
|
111
|
+
// write leaves a workdir with a concept + image but no pins — the very
|
|
112
|
+
// first wizard run did exactly that (tilde-path rename crash), and the
|
|
113
|
+
// panel answered "no-youtube" about a thumbnail sitting right there. A
|
|
114
|
+
// concept or image on disk is direct evidence the pack was on.
|
|
115
|
+
const youtube =
|
|
116
|
+
lastBoolFlag(args, "--youtube", "--no-youtube") ??
|
|
117
|
+
(inputs.cfg.youtube === true || inputs.hasConcept || inputs.hasImage);
|
|
118
|
+
// resolvePortrait is the ONE precedence rule (override > flag > config —
|
|
119
|
+
// portrait-override.ts has the why), shared with produce.ts so the panel
|
|
120
|
+
// can never disagree with what a replay would actually prompt with. It
|
|
121
|
+
// also carries produce.ts's expandHome treatment of the flag/config paths
|
|
122
|
+
// — a `~/Pictures/me.jpg` must resolve identically here.
|
|
123
|
+
const resolvedPortrait = resolvePortrait({
|
|
124
|
+
overridePath: inputs.overridePortraitPath ?? null,
|
|
125
|
+
flagPortrait: lastFlagValue(args, ["--portrait"]),
|
|
126
|
+
cfgPortrait: inputs.cfg.portrait,
|
|
127
|
+
});
|
|
128
|
+
const portraitPath = resolvedPortrait?.path;
|
|
129
|
+
const model =
|
|
130
|
+
typeof inputs.cfg.thumbnailModel === "string"
|
|
131
|
+
? inputs.cfg.thumbnailModel
|
|
132
|
+
: THUMBNAIL_MODEL_DEFAULT;
|
|
133
|
+
const decision = thumbnailDecision(
|
|
134
|
+
youtube,
|
|
135
|
+
portraitPath,
|
|
136
|
+
inputs.hasKey,
|
|
137
|
+
portraitPath !== undefined && inputs.portraitExists(portraitPath),
|
|
138
|
+
);
|
|
139
|
+
const base = {
|
|
140
|
+
model,
|
|
141
|
+
...(resolvedPortrait !== undefined
|
|
142
|
+
? { portraitPath: resolvedPortrait.path, portraitSource: resolvedPortrait.source }
|
|
143
|
+
: {}),
|
|
144
|
+
};
|
|
145
|
+
if (decision !== "generate") {
|
|
146
|
+
const reason: ThumbnailPanelReason =
|
|
147
|
+
decision === "skip-no-youtube"
|
|
148
|
+
? "no-youtube"
|
|
149
|
+
: decision === "skip-no-portrait"
|
|
150
|
+
? "no-portrait"
|
|
151
|
+
: decision === "skip-no-key"
|
|
152
|
+
? "no-key"
|
|
153
|
+
: "portrait-missing";
|
|
154
|
+
return { status: "unavailable", reason, ...base };
|
|
155
|
+
}
|
|
156
|
+
// The skip file ranks BELOW unavailability: a decision about a thumbnail
|
|
157
|
+
// that cannot generate anyway would show controls that cannot work.
|
|
158
|
+
if (inputs.approvedSkip) return { status: "skipped", reason: "skip-file", ...base };
|
|
159
|
+
return {
|
|
160
|
+
status: "ready",
|
|
161
|
+
// Ready with nothing on disk: the produce run never reached (or never
|
|
162
|
+
// finished) the thumbnail step. The panel shows empty fields and a
|
|
163
|
+
// placeholder instead of pretending an image exists.
|
|
164
|
+
...(inputs.hasConcept || inputs.hasImage ? {} : { reason: "never-generated" as const }),
|
|
165
|
+
...base,
|
|
166
|
+
};
|
|
167
|
+
}
|
package/src/ui/animation.ts
CHANGED
|
@@ -215,12 +215,11 @@ export class StageAnimator {
|
|
|
215
215
|
`${ansi.green}${eq}${ansi.reset} ${ansi.dim}(${elapsedSec}s)${ansi.reset}\n` +
|
|
216
216
|
` ${ansi.cyan}${dots}${ansi.reset} ${ansi.brightWhite}${clamp(this.subtitle)}${ansi.reset}`;
|
|
217
217
|
} else if (this.type === "audio") {
|
|
218
|
-
const synth = renderSynthWave(this.frameIndex,
|
|
218
|
+
const synth = renderSynthWave(this.frameIndex, 12);
|
|
219
219
|
visual =
|
|
220
220
|
`${ansi.bold}${ansi.brightYellow}🔊 ANALOG SYNTH AUDIO OSCILLOSCOPE${ansi.reset} ` +
|
|
221
221
|
`[${synth}] ${ansi.dim}(${elapsedSec}s)${ansi.reset}\n` +
|
|
222
|
-
` ${ansi.brightYellow}▸${ansi.reset} ${ansi.brightWhite}${clamp(this.subtitle)}${ansi.reset}
|
|
223
|
-
`${ansi.dim}48kHz 24-bit PCM │ EBU R128 (-14.0 LUFS)${ansi.reset}`;
|
|
222
|
+
` ${ansi.brightYellow}▸${ansi.reset} ${ansi.brightWhite}${clamp(this.subtitle)}${ansi.reset}`;
|
|
224
223
|
} else if (this.type === "master") {
|
|
225
224
|
const dots = SPINNERS.dots[this.frameIndex % SPINNERS.dots.length];
|
|
226
225
|
visual =
|
|
@@ -236,7 +235,12 @@ export class StageAnimator {
|
|
|
236
235
|
}
|
|
237
236
|
|
|
238
237
|
this.clearRenderedLines();
|
|
239
|
-
const
|
|
238
|
+
const rawLines = visual.split("\n");
|
|
239
|
+
const lines = rawLines.map((l) => {
|
|
240
|
+
const vis = stripAnsi(l);
|
|
241
|
+
if (vis.length <= maxVisualWidth) return l;
|
|
242
|
+
return l.slice(0, maxVisualWidth - 3) + "..." + ansi.reset;
|
|
243
|
+
});
|
|
240
244
|
for (const l of lines) {
|
|
241
245
|
process.stdout.write(`${l}\n`);
|
|
242
246
|
}
|
|
@@ -420,6 +424,10 @@ export class RenderTimelineHUD {
|
|
|
420
424
|
export function printProductionCompleteBanner(summary: {
|
|
421
425
|
outPath: string;
|
|
422
426
|
coverPath?: string;
|
|
427
|
+
/** The `<out>.youtube.md` SEO pack, when the run wrote one (Y2). */
|
|
428
|
+
youtubePath?: string;
|
|
429
|
+
/** The `<out>.thumbnail.png` AI thumbnail, when the run wrote one (Y3). */
|
|
430
|
+
thumbnailPath?: string;
|
|
423
431
|
sourceDurationSec: number;
|
|
424
432
|
outputDurationSec: number;
|
|
425
433
|
sceneCount: number;
|
|
@@ -447,6 +455,12 @@ export function printProductionCompleteBanner(summary: {
|
|
|
447
455
|
...(summary.coverPath
|
|
448
456
|
? [`${ansi.bold}║${ansi.reset} ${ansi.brightMagenta}▸ Cover:${ansi.reset} ${ansi.white}${summary.coverPath}${ansi.reset}`]
|
|
449
457
|
: []),
|
|
458
|
+
...(summary.youtubePath
|
|
459
|
+
? [`${ansi.bold}║${ansi.reset} ${ansi.brightRed}▸ YouTube:${ansi.reset} ${ansi.white}${summary.youtubePath}${ansi.reset}`]
|
|
460
|
+
: []),
|
|
461
|
+
...(summary.thumbnailPath
|
|
462
|
+
? [`${ansi.bold}║${ansi.reset} ${ansi.brightYellow}▸ Thumb:${ansi.reset} ${ansi.white}${summary.thumbnailPath}${ansi.reset}`]
|
|
463
|
+
: []),
|
|
450
464
|
`${ansi.bold}║${ansi.reset} ${ansi.green}▸ Engine:${ansi.reset} ${ansi.bold}${ansi.brightCyan}${providerLabel}${ansi.reset} + ${ansi.brightWhite}Remotion${ansi.reset} + ${ansi.brightWhite}Whisper.cpp${ansi.reset}`,
|
|
451
465
|
`${ansi.bold}║${ansi.reset} ${ansi.yellow}▸ Cut:${ansi.reset} ${ansi.bold}${summary.outputDurationSec.toFixed(1)}s${ansi.reset} (trimmed from ${summary.sourceDurationSec.toFixed(1)}s · ${ansi.green}-${cutPct}% dead air/flubs${ansi.reset})`,
|
|
452
466
|
`${ansi.bold}║${ansi.reset} ${ansi.blue}▸ Scenes:${ansi.reset} ${ansi.bold}${summary.sceneCount}${ansi.reset} dynamic AI graphic overlay${summary.sceneCount === 1 ? "" : "s"} placed`,
|