ossclip 0.1.24 → 0.1.26
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-Bx2VQLP8.css +1 -0
- package/editor-dist/assets/index-DVI51_2u.js +163 -0
- package/editor-dist/index.html +2 -2
- package/package.json +4 -4
- package/src/analyze.ts +17 -1
- package/src/caption-report.ts +141 -3
- package/src/doctor.ts +7 -5
- package/src/edit.ts +571 -3
- 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 +64 -8
- package/src/paths.ts +88 -0
- package/src/portrait-override.ts +86 -0
- package/src/produce.ts +1971 -161
- package/src/program.ts +142 -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 +10 -0
- package/editor-dist/assets/index-ChRVBVLj.css +0 -1
- 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
|
@@ -424,6 +424,10 @@ export class RenderTimelineHUD {
|
|
|
424
424
|
export function printProductionCompleteBanner(summary: {
|
|
425
425
|
outPath: string;
|
|
426
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;
|
|
427
431
|
sourceDurationSec: number;
|
|
428
432
|
outputDurationSec: number;
|
|
429
433
|
sceneCount: number;
|
|
@@ -451,6 +455,12 @@ export function printProductionCompleteBanner(summary: {
|
|
|
451
455
|
...(summary.coverPath
|
|
452
456
|
? [`${ansi.bold}║${ansi.reset} ${ansi.brightMagenta}▸ Cover:${ansi.reset} ${ansi.white}${summary.coverPath}${ansi.reset}`]
|
|
453
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
|
+
: []),
|
|
454
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}`,
|
|
455
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})`,
|
|
456
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`,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
.ossclip-scroll-list{scrollbar-width:thin;scrollbar-color:#3a3a44 transparent}.ossclip-scroll-list::-webkit-scrollbar{width:8px}.ossclip-scroll-list::-webkit-scrollbar-track{background:transparent}.ossclip-scroll-list::-webkit-scrollbar-thumb{background:#3a3a44;border-radius:4px}.ossclip-picker-row{color:#c9c9d4;background:transparent;border:1px solid transparent}.ossclip-picker-row.is-workdir{color:#ededf2;background:#1a1a21;border-color:#2a2a33}.ossclip-picker-row:disabled{cursor:default;opacity:.5}.ossclip-picker-row:hover:not(:disabled){background:#1f1f28;border-color:#3a3a48}.ossclip-picker-row:focus{outline:none;background:#23232e;border-color:#8ab4f8}.ossclip-picker-row:active:not(:disabled){background:#2a2a36}
|