ossclip 0.1.20 → 0.1.21

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/README.md CHANGED
@@ -28,7 +28,7 @@ ossclip produce input.mp4 -o out.mp4
28
28
  ossclip produce podcast.mp4 --produce --clip 60 -o clip.mp4
29
29
 
30
30
  # Keep your own editor: export the planned cuts as labelled markers, no render, no LLM
31
- ossclip analyse input.mp4 # writes input.fcpxml — imports into Resolve & Premiere
31
+ ossclip analyze input.mp4 --format premiere-xml # Premiere Pro; resolve-edl for Resolve, fcpxml for FCP
32
32
 
33
33
  # Edit what it produced — direct manipulation, in the browser
34
34
  ossclip edit "<work directory>"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ossclip",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
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/scenes": "0.1.20",
40
- "@ossclip/renderer": "0.1.20",
41
- "@ossclip/core": "0.1.20"
39
+ "@ossclip/renderer": "0.1.21",
40
+ "@ossclip/core": "0.1.21",
41
+ "@ossclip/scenes": "0.1.21"
42
42
  },
43
43
  "homepage": "https://github.com/AhsanAyaz/ossclip#readme",
44
44
  "bugs": {
@@ -4,14 +4,17 @@ import { z } from "zod/v4";
4
4
  import {
5
5
  ProductionSchema,
6
6
  buildFcpxmlMarkers,
7
+ buildPremiereXmlMarkers,
8
+ buildResolveMarkerEdl,
9
+ keptPauses,
7
10
  type CleanupLevel,
8
11
  } from "@ossclip/core";
9
12
  import { produce } from "./produce";
10
13
  import type { PhaseTimings } from "./phase-timing";
11
14
 
12
15
  /**
13
- * `ossclip analyse` (next-directions §2–3; design doc
14
- * 2026-08-12-analyse-fcpxml-export-design.md): the analyser without the
16
+ * `ossclip analyze` (next-directions §2–3; design doc
17
+ * 2026-08-12-analyse-fcpxml-export-design.md): the analyzer without the
15
18
  * renderer. §140 measured the render at 85% of wall time on two machines —
16
19
  * this command is the product of skipping it: the same pipeline up to the
17
20
  * cut report (no LLM, no Remotion), plus an export file an editor's own NLE
@@ -22,16 +25,30 @@ import type { PhaseTimings } from "./phase-timing";
22
25
  * zod enum, not a string: a typo'd `--format fcpxmll` must error naming the
23
26
  * choice, never fall back to a default that silently writes the wrong file
24
27
  * (CLAUDE.md's `--source-fit containn` rule, verbatim).
28
+ *
29
+ * `resolve-edl`, not `edl` (§142): it is Resolve's marker-EDL dialect —
30
+ * markers with colours, for `Timeline → Import → Timeline Markers from EDL`
31
+ * — not a cut EDL. A future cut EDL gets its own name; "edl" meaning either
32
+ * would be a permanent ambiguity. It exists because Resolve's FCPXML import
33
+ * silently drops clip markers (field-verified the day fcpxml shipped);
34
+ * Premiere reads the fcpxml markers fine.
25
35
  */
26
- export const ExportFormatSchema = z.enum(["fcpxml"]);
36
+ export const ExportFormatSchema = z.enum(["fcpxml", "resolve-edl", "premiere-xml"]);
27
37
  export type ExportFormat = z.infer<typeof ExportFormatSchema>;
28
38
 
39
+ /** The FILE extension per format — "demo.resolve-edl" would import nowhere. */
40
+ const FORMAT_EXTENSIONS: Record<ExportFormat, string> = {
41
+ fcpxml: "fcpxml",
42
+ "resolve-edl": "edl",
43
+ "premiere-xml": "xml",
44
+ };
45
+
29
46
  /** Same shape as produce's `defaultOutPath`: beside the input, new extension. */
30
47
  export function defaultExportPath(input: string, format: ExportFormat): string {
31
- return input.replace(/(\.[^.]+)?$/, `.${format}`);
48
+ return input.replace(/(\.[^.]+)?$/, `.${FORMAT_EXTENSIONS[format]}`);
32
49
  }
33
50
 
34
- export interface AnalyseOptions {
51
+ export interface AnalyzeOptions {
35
52
  cleanup: CleanupLevel;
36
53
  format: ExportFormat;
37
54
  out?: string;
@@ -46,10 +63,14 @@ export interface AnalyseOptions {
46
63
  sortExplicit?: boolean;
47
64
  }
48
65
 
49
- export interface AnalyseResult {
66
+ export interface AnalyzeResult {
50
67
  workdir: string;
51
68
  outPath: string;
69
+ /** Suggested-cut markers — one per remove segment. */
52
70
  markerCount: number;
71
+ /** Informational kept-pause markers (§142 round 2) — counted separately
72
+ * so the CLI and telemetry can say which is which. */
73
+ pauseCount: number;
53
74
  sourceDurationSec: number;
54
75
  phaseTimings: PhaseTimings;
55
76
  }
@@ -61,10 +82,10 @@ export interface AnalyseResult {
61
82
  * run just wrote it — the file is user-visible and hand-editable, and a
62
83
  * truncated or tweaked one must error here, not export garbage markers.
63
84
  */
64
- export async function runAnalyse(
85
+ export async function runAnalyze(
65
86
  inputArg: string,
66
- opts: AnalyseOptions,
67
- ): Promise<AnalyseResult> {
87
+ opts: AnalyzeOptions,
88
+ ): Promise<AnalyzeResult> {
68
89
  const result = await produce(inputArg, {
69
90
  cleanup: opts.cleanup,
70
91
  transcript: opts.transcript,
@@ -83,18 +104,34 @@ export async function runAnalyse(
83
104
  const production = ProductionSchema.parse(
84
105
  JSON.parse(await readFile(join(result.workdir, "production.json"), "utf8")),
85
106
  );
86
- const xml = buildFcpxmlMarkers(production);
107
+ // §142, learned the hard way in one field-test hour: each NLE gets its OWN
108
+ // dialect. Premiere rejects modern fcpxml outright, Resolve's fcpxml import
109
+ // silently drops markers — so fcpxml is for actual Final Cut Pro.
110
+ const content =
111
+ opts.format === "resolve-edl"
112
+ ? buildResolveMarkerEdl(production)
113
+ : opts.format === "premiere-xml"
114
+ ? buildPremiereXmlMarkers(production)
115
+ : buildFcpxmlMarkers(production);
87
116
  const markerCount = (production.cutlist ?? []).filter((s) => s.kind === "remove").length;
117
+ const pauseCount = keptPauses(production).length;
88
118
  const outPath = resolve(opts.out ?? defaultExportPath(resolve(inputArg), opts.format));
89
- await writeFile(outPath, xml);
119
+ await writeFile(outPath, content);
120
+ const detail =
121
+ opts.format === "resolve-edl"
122
+ ? "in Resolve: Media Pool → right-click your timeline → Timelines → Import → Timeline Markers from EDL"
123
+ : opts.format === "premiere-xml"
124
+ ? "in Premiere: File → Import, pick the .xml, relink media if offline"
125
+ : "for Final Cut Pro; Premiere needs --format premiere-xml, Resolve --format resolve-edl";
90
126
  console.log(
91
- `✓ ${opts.format} → ${outPath} (${markerCount} marker${markerCount === 1 ? "" : "s"} — ` +
92
- "import into Resolve/Premiere and review before cutting)",
127
+ `✓ ${opts.format} → ${outPath} (${markerCount} cut marker${markerCount === 1 ? "" : "s"}` +
128
+ `${pauseCount > 0 ? ` + ${pauseCount} kept-pause marker${pauseCount === 1 ? "" : "s"}` : ""} — ${detail})`,
93
129
  );
94
130
  return {
95
131
  workdir: result.workdir,
96
132
  outPath,
97
133
  markerCount,
134
+ pauseCount,
98
135
  sourceDurationSec: result.sourceDurationSec,
99
136
  phaseTimings: result.phaseTimings,
100
137
  };
package/src/program.ts CHANGED
@@ -6,7 +6,7 @@ import { z } from "zod/v4";
6
6
  import { CleanupLevelSchema, SceneComponentIdSchema } from "@ossclip/core";
7
7
  import { STUDIO_ENTRY } from "@ossclip/renderer";
8
8
  import { loadEnvFiles } from "./env";
9
- import { ExportFormatSchema, runAnalyse } from "./analyse";
9
+ import { ExportFormatSchema, runAnalyze } from "./analyze";
10
10
  import { phaseBucketProps } from "./phase-timing";
11
11
  import { produce } from "./produce";
12
12
  // The one interactive import that is STATIC rather than `await import()`: the
@@ -536,14 +536,22 @@ export function buildProgram(): Command {
536
536
  });
537
537
 
538
538
  program
539
- .command("analyse")
540
- .alias("analyze")
539
+ // American spelling is primary (house style, 2026-08-12); the British
540
+ // alias stays because a command someone has already learned must not
541
+ // become an "unknown command" over an s/z.
542
+ .command("analyze")
543
+ .alias("analyse")
541
544
  .description(
542
- "analyse a take and export the planned cuts as labelled NLE markers — no render, no LLM " +
545
+ "analyze a take and export the planned cuts as labelled NLE markers — no render, no LLM " +
543
546
  "(FCPXML imports into Resolve and Premiere; review the markers, then cut in your own editor)",
544
547
  )
545
548
  .argument("<input>", "input video file (or a folder of clips)")
546
- .option("--format <format>", "export format: fcpxml", "fcpxml")
549
+ .option(
550
+ "--format <format>",
551
+ "export format: fcpxml (Premiere) | resolve-edl (Resolve coloured timeline markers — " +
552
+ "its fcpxml import drops markers)",
553
+ "fcpxml",
554
+ )
547
555
  .option("--out <path>", "export file path (default: <input>.<format>)")
548
556
  .option("--cleanup <level>", "exact | light | standard | aggressive", "standard")
549
557
  .option("--transcript <path>", "inject a transcript JSON instead of running whisper")
@@ -570,7 +578,7 @@ export function buildProgram(): Command {
570
578
  // error naming the flag, never silently export a different file.
571
579
  const format = ExportFormatSchema.parse(opts.format);
572
580
  try {
573
- const result = await runAnalyse(input, {
581
+ const result = await runAnalyze(input, {
574
582
  cleanup,
575
583
  format,
576
584
  out: opts.out,
@@ -589,16 +597,17 @@ export function buildProgram(): Command {
589
597
  });
590
598
  // Counts, buckets and names only (§134) — the format is an enum name,
591
599
  // never a path; per-phase buckets ride along like produce's.
592
- telemetry.record("analyse_completed", {
600
+ telemetry.record("analyze_completed", {
593
601
  format,
594
602
  cleanup_level: cleanup,
595
603
  source_duration_bucket: durationBucket(result.sourceDurationSec),
596
604
  markers: result.markerCount,
605
+ kept_pauses: result.pauseCount,
597
606
  ...phaseBucketProps(result.phaseTimings),
598
607
  });
599
608
  } catch (err) {
600
609
  // Constructor name only, like produce_failed: messages quote paths.
601
- telemetry.record("analyse_failed", {
610
+ telemetry.record("analyze_failed", {
602
611
  error_class: err instanceof Error ? err.constructor.name : "NonError",
603
612
  });
604
613
  throw err;