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 +1 -1
- package/package.json +4 -4
- package/src/{analyse.ts → analyze.ts} +50 -13
- package/src/program.ts +17 -8
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
|
|
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.
|
|
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/
|
|
40
|
-
"@ossclip/
|
|
41
|
-
"@ossclip/
|
|
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
|
|
14
|
-
* 2026-08-12-analyse-fcpxml-export-design.md): 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
|
|
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
|
|
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
|
|
85
|
+
export async function runAnalyze(
|
|
65
86
|
inputArg: string,
|
|
66
|
-
opts:
|
|
67
|
-
): Promise<
|
|
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
|
-
|
|
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,
|
|
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
|
-
"
|
|
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,
|
|
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
|
-
|
|
540
|
-
|
|
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
|
-
"
|
|
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(
|
|
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
|
|
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("
|
|
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("
|
|
610
|
+
telemetry.record("analyze_failed", {
|
|
602
611
|
error_class: err instanceof Error ? err.constructor.name : "NonError",
|
|
603
612
|
});
|
|
604
613
|
throw err;
|