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.
@@ -0,0 +1 @@
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}
@@ -4,7 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>ossclip editor</title>
7
- <script type="module" crossorigin src="/assets/index-otiVWeTH.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-Bo0WME7k.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-C8IPo60X.css">
8
9
  </head>
9
10
  <body>
10
11
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ossclip",
3
- "version": "0.1.7",
3
+ "version": "0.1.10",
4
4
  "description": "Local-first CLI video producer: cuts silence and fillers, word-timed captions, face-aware framing, and LLM-planned code-rendered graphics — transcription and rendering never leave your machine",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -36,9 +36,9 @@
36
36
  "commander": "^12.1.0",
37
37
  "tsx": "^4.19.0",
38
38
  "zod": "^3.25.76",
39
- "@ossclip/core": "0.1.7",
40
- "@ossclip/renderer": "0.1.7",
41
- "@ossclip/scenes": "0.1.7"
39
+ "@ossclip/core": "0.1.10",
40
+ "@ossclip/renderer": "0.1.10",
41
+ "@ossclip/scenes": "0.1.10"
42
42
  },
43
43
  "homepage": "https://github.com/AhsanAyaz/ossclip#readme",
44
44
  "bugs": {
package/src/edit.ts CHANGED
@@ -14,13 +14,23 @@ import { OverrideDocSchema, emptyOverrideDoc } from "@ossclip/core";
14
14
  * `ossclip edit` works with no build step), else the monorepo sibling's
15
15
  * `dist/` for a clone. Null when neither exists — callers own the loud
16
16
  * error, and `ossclip doctor` reports it as a check.
17
+ *
18
+ * When BOTH exist, the newer index.html wins (§126): in a clone, a stale
19
+ * `editor-dist/` left behind by a local prepack silently shadowed a fresh
20
+ * `pnpm build` for a whole field session — the user reported a feature
21
+ * missing that had shipped, because the server kept serving the old page.
22
+ * In an npm install only `editor-dist/` exists, so the mtime tiebreak
23
+ * never changes behavior there.
17
24
  */
18
25
  export function resolveEditorPageDir(): string | null {
19
26
  const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
20
- for (const candidate of [join(pkgRoot, "editor-dist"), join(pkgRoot, "../editor/dist")]) {
21
- if (existsSync(join(candidate, "index.html"))) return candidate;
22
- }
23
- return null;
27
+ const candidates = [join(pkgRoot, "editor-dist"), join(pkgRoot, "../editor/dist")]
28
+ .filter((dir) => existsSync(join(dir, "index.html")))
29
+ .sort(
30
+ (a, b) =>
31
+ statSync(join(b, "index.html")).mtimeMs - statSync(join(a, "index.html")).mtimeMs,
32
+ );
33
+ return candidates[0] ?? null;
24
34
  }
25
35
 
26
36
  /**
@@ -12,6 +12,7 @@ export interface ProduceExtras {
12
12
  speaker?: string;
13
13
  whisperModel?: string;
14
14
  blooperMarker?: string;
15
+ collapseRetakes?: boolean;
15
16
  sourceIsEdited?: boolean;
16
17
  llm?: "claude" | "claude-cli" | "gemini" | "mock";
17
18
  }
@@ -50,6 +51,7 @@ export function produceArgv(a: ProduceAnswers): string[] {
50
51
  if (e.speaker) argv.push("--speaker", e.speaker);
51
52
  if (e.whisperModel) argv.push("--whisper-model", e.whisperModel);
52
53
  if (e.blooperMarker) argv.push("--blooper-marker", e.blooperMarker);
54
+ if (e.collapseRetakes === true) argv.push("--collapse-retakes");
53
55
  if (e.sourceIsEdited === true) argv.push("--source-is-edited");
54
56
  if (e.llm) argv.push("--llm", e.llm);
55
57
 
@@ -4,9 +4,17 @@ import { produceArgv, type ProduceAnswers, type ProduceExtras } from "./produce-
4
4
  import { assertInteractive, confirm, intro, multiselect, select, text, unwrap } from "./prompts";
5
5
 
6
6
  /**
7
- * The produce wizard. Twenty-five flags sorted into three tiers: six always
8
- * asked, seven behind one "anything else?" multiselect, and twelve that stay
9
- * flags-only because they are debug or internal surfaces.
7
+ * The produce wizard. Thirty flags (plus the positional input path) sorted
8
+ * into three tiers: six prompts asked directly — the input path, plus five
9
+ * flags (--out, --cleanup, --aspect, --produce, --intent) — eight behind one
10
+ * "anything else?" multiselect, and the remaining seventeen stay flags-only:
11
+ * debug/internal surfaces, replay-only fields, or (final-review fix wave,
12
+ * Finding 1) --sort. A folder's clip order only means anything once the
13
+ * folder has been enumerated, and that enumeration happens inside
14
+ * `produce()` — after the wizard has already returned argv — so there is
15
+ * nothing for a prompt to offer a choice about beforehand. --sort stays
16
+ * typed-only, same tier as --clip-window below, not a tenth multiselect
17
+ * entry.
10
18
  *
11
19
  * --clip-window is deliberately NOT offered: --clip runs write it into
12
20
  * command.json so the editor's Render replays the same window without an LLM
@@ -20,6 +28,11 @@ const EXTRAS = [
20
28
  { value: "speaker", label: "Say who is on camera", hint: "--speaker" },
21
29
  { value: "whisperModel", label: "Pick a transcription model", hint: "--whisper-model" },
22
30
  { value: "blooperMarker", label: "Cut flubbed takes on a spoken word", hint: "--blooper-marker" },
31
+ {
32
+ value: "collapseRetakes",
33
+ label: "Collapse repeated takes automatically",
34
+ hint: "--collapse-retakes",
35
+ },
23
36
  { value: "sourceIsEdited", label: "Source already has burned-in text", hint: "--source-is-edited" },
24
37
  { value: "llm", label: "Choose the LLM provider", hint: "--llm" },
25
38
  ] as const;
@@ -37,22 +50,36 @@ export function extrasFor(graphics: boolean): (typeof EXTRAS)[number][] {
37
50
  return graphics ? [...EXTRAS] : EXTRAS.filter((e) => e.value !== "graphicsClip");
38
51
  }
39
52
 
40
- export async function produceWizard(cfg: { speaker?: string } = {}): Promise<string[]> {
53
+ export async function produceWizard(cfg: { speaker?: string; input?: string } = {}): Promise<string[]> {
41
54
  assertInteractive("produce wizard");
42
55
  intro("ossclip produce");
43
56
 
44
- const input = unwrap(
45
- await text({
46
- message: "Video file",
47
- placeholder: "./raw/take1.mp4",
48
- validate: (v) => {
49
- if (!v) return "a path is required";
50
- if (!existsSync(v)) return `no such file: ${v}`;
51
- if (!statSync(v).isFile()) return `${v} is a directory, not a video file`;
52
- return undefined;
53
- },
54
- }),
55
- ) as string;
57
+ // Pre-supplied by bare `ossclip <path>` (0.1.9 first-contact, 2026-08-05):
58
+ // the user already TYPED the input on the command line, and the old flow
59
+ // dropped it and asked again — the re-ask is where "./Anyhropic c Compiler"
60
+ // became "./" (all of ~/Downloads). The router checks existence before the
61
+ // wizard ever opens, so a prefilled path skips the prompt entirely.
62
+ const input =
63
+ cfg.input ??
64
+ (unwrap(
65
+ await text({
66
+ // Finding 1 (final-review fix wave): `ossclip produce <folder>` shipped
67
+ // (folder-input-brief.md) but this prompt still rejected a directory —
68
+ // the wizard was the only way in that couldn't do what the CLI could.
69
+ // A folder is concatenated by name (codepoint order, like `ls`); --sort
70
+ // mtime reorders it but stays a typed flag, not a wizard question (see
71
+ // the file-level comment above for why).
72
+ message: "Video file, or a folder of clips to concatenate (by name; --sort mtime is a typed flag)",
73
+ placeholder: "./raw/take1.mp4",
74
+ validate: (v) => {
75
+ if (!v) return "a path is required";
76
+ if (!existsSync(v)) return `no such path: ${v}`;
77
+ const st = statSync(v);
78
+ if (!st.isFile() && !st.isDirectory()) return `${v} is neither a video file nor a folder`;
79
+ return undefined;
80
+ },
81
+ }),
82
+ ) as string);
56
83
 
57
84
  const aspect = unwrap(
58
85
  await select({
@@ -124,6 +151,7 @@ export async function produceWizard(cfg: { speaker?: string } = {}): Promise<str
124
151
  );
125
152
  }
126
153
  if (chosen.includes("sourceFit")) extras.sourceFit = "contain";
154
+ if (chosen.includes("collapseRetakes")) extras.collapseRetakes = true;
127
155
  if (chosen.includes("sourceIsEdited")) extras.sourceIsEdited = true;
128
156
  if (chosen.includes("speaker")) {
129
157
  extras.speaker = unwrap(