ossclip 0.1.0
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/LICENSE +27 -0
- package/README.md +38 -0
- package/bin/ossclip.mjs +11 -0
- package/editor-dist/assets/index-CI9c6j62.js +159 -0
- package/editor-dist/index.html +12 -0
- package/package.json +50 -0
- package/src/doctor.ts +190 -0
- package/src/edit.ts +465 -0
- package/src/env.ts +70 -0
- package/src/index.ts +230 -0
- package/src/produce.ts +1407 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>ossclip editor</title>
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-CI9c6j62.js"></script>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div id="root"></div>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ossclip",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/AhsanAyaz/ossclip.git",
|
|
10
|
+
"directory": "apps/cli"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"video",
|
|
14
|
+
"cli",
|
|
15
|
+
"captions",
|
|
16
|
+
"remotion",
|
|
17
|
+
"whisper",
|
|
18
|
+
"shorts",
|
|
19
|
+
"video-editing",
|
|
20
|
+
"llm"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=22"
|
|
24
|
+
},
|
|
25
|
+
"bin": {
|
|
26
|
+
"ossclip": "./bin/ossclip.mjs"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"README.md",
|
|
30
|
+
"bin",
|
|
31
|
+
"src",
|
|
32
|
+
"editor-dist"
|
|
33
|
+
],
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"commander": "^12.1.0",
|
|
36
|
+
"tsx": "^4.19.0",
|
|
37
|
+
"zod": "^3.25.76",
|
|
38
|
+
"@ossclip/renderer": "0.1.0",
|
|
39
|
+
"@ossclip/scenes": "0.1.0",
|
|
40
|
+
"@ossclip/core": "0.1.0"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/AhsanAyaz/ossclip#readme",
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/AhsanAyaz/ossclip/issues"
|
|
45
|
+
},
|
|
46
|
+
"author": "Muhammad Ahsan Ayaz",
|
|
47
|
+
"scripts": {
|
|
48
|
+
"ossclip": "tsx src/index.ts"
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/doctor.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { isAbsolute, join } from "node:path";
|
|
4
|
+
import type { OssclipConfig } from "@ossclip/core";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* `ossclip doctor` (R18 §90a): check every prerequisite and print the exact
|
|
8
|
+
* fix — because the install cliff is the adoption ceiling. Seven
|
|
9
|
+
* preconditions stand between a first-time user and their first frame, and
|
|
10
|
+
* every one that fails silently becomes a GitHub issue instead of a
|
|
11
|
+
* one-line fix the tool could have printed itself.
|
|
12
|
+
*
|
|
13
|
+
* Checks are pure over injected probes so the table is unit-testable; the
|
|
14
|
+
* CLI wires the real spawn/existsSync in. The provider check MUST run after
|
|
15
|
+
* `loadEnvFiles` (R16 §77) or a key living in `.env` reports a false
|
|
16
|
+
* negative — `index.ts` loads env at module top, before any command runs.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export interface DoctorCheck {
|
|
20
|
+
name: string;
|
|
21
|
+
ok: boolean;
|
|
22
|
+
/** What was found (the path/provider that satisfied the check). */
|
|
23
|
+
detail: string;
|
|
24
|
+
/** The exact fix, present only when !ok. */
|
|
25
|
+
fix?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface DoctorProbes {
|
|
29
|
+
/** Does spawning this binary work at all (any exit code counts)? */
|
|
30
|
+
binRuns(bin: string, arg: string): Promise<boolean>;
|
|
31
|
+
exists(path: string): boolean;
|
|
32
|
+
env: NodeJS.ProcessEnv;
|
|
33
|
+
platform: NodeJS.Platform;
|
|
34
|
+
nodeMajor: number;
|
|
35
|
+
/** Resolved editor page dir, or null if no candidate exists. */
|
|
36
|
+
editorPageDir: string | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** brew on mac, apt elsewhere — the generic hint covers the rest. */
|
|
40
|
+
const installHint = (platform: NodeJS.Platform, brew: string, apt: string, generic: string): string =>
|
|
41
|
+
platform === "darwin" ? brew : platform === "linux" ? apt : generic;
|
|
42
|
+
|
|
43
|
+
export async function runDoctor(cfg: OssclipConfig, p: DoctorProbes): Promise<DoctorCheck[]> {
|
|
44
|
+
const checks: DoctorCheck[] = [];
|
|
45
|
+
|
|
46
|
+
checks.push({
|
|
47
|
+
name: "node",
|
|
48
|
+
ok: p.nodeMajor >= 22,
|
|
49
|
+
detail: `v${p.nodeMajor}`,
|
|
50
|
+
...(p.nodeMajor >= 22
|
|
51
|
+
? {}
|
|
52
|
+
: { fix: "ossclip needs Node ≥ 22 — install from https://nodejs.org or via your version manager" }),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const ffmpegOk = await p.binRuns(cfg.ffmpegPath, "-version");
|
|
56
|
+
checks.push({
|
|
57
|
+
name: "ffmpeg",
|
|
58
|
+
ok: ffmpegOk,
|
|
59
|
+
detail: cfg.ffmpegPath,
|
|
60
|
+
...(ffmpegOk
|
|
61
|
+
? {}
|
|
62
|
+
: {
|
|
63
|
+
fix:
|
|
64
|
+
installHint(
|
|
65
|
+
p.platform,
|
|
66
|
+
"brew install ffmpeg",
|
|
67
|
+
"sudo apt install ffmpeg",
|
|
68
|
+
"install ffmpeg from https://ffmpeg.org",
|
|
69
|
+
) + " — or point OSSCLIP_FFMPEG (or config.json ffmpegPath) at the binary",
|
|
70
|
+
}),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const ffprobeOk = await p.binRuns(cfg.ffprobePath, "-version");
|
|
74
|
+
checks.push({
|
|
75
|
+
name: "ffprobe",
|
|
76
|
+
ok: ffprobeOk,
|
|
77
|
+
detail: cfg.ffprobePath,
|
|
78
|
+
...(ffprobeOk
|
|
79
|
+
? {}
|
|
80
|
+
: {
|
|
81
|
+
fix:
|
|
82
|
+
installHint(
|
|
83
|
+
p.platform,
|
|
84
|
+
"brew install ffmpeg (provides ffprobe)",
|
|
85
|
+
"sudo apt install ffmpeg (provides ffprobe)",
|
|
86
|
+
"ffprobe ships with ffmpeg — https://ffmpeg.org",
|
|
87
|
+
) + " — or set OSSCLIP_FFPROBE",
|
|
88
|
+
}),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const whisperOk = await p.binRuns(cfg.whisperPath, "--help");
|
|
92
|
+
checks.push({
|
|
93
|
+
name: "whisper-cli",
|
|
94
|
+
ok: whisperOk,
|
|
95
|
+
detail: cfg.whisperPath,
|
|
96
|
+
...(whisperOk
|
|
97
|
+
? {}
|
|
98
|
+
: {
|
|
99
|
+
fix:
|
|
100
|
+
installHint(
|
|
101
|
+
p.platform,
|
|
102
|
+
"brew install whisper-cpp",
|
|
103
|
+
"build whisper.cpp from source: https://github.com/ggml-org/whisper.cpp",
|
|
104
|
+
"build whisper.cpp from source: https://github.com/ggml-org/whisper.cpp",
|
|
105
|
+
) + " — or point OSSCLIP_WHISPER at your whisper-cli",
|
|
106
|
+
}),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Same resolution `produce` uses: an absolute model is a file path, a bare
|
|
110
|
+
// name resolves inside modelDir as ggml-<name>.bin.
|
|
111
|
+
const modelPath = isAbsolute(cfg.model) ? cfg.model : join(cfg.modelDir, `ggml-${cfg.model}.bin`);
|
|
112
|
+
const modelOk = p.exists(modelPath);
|
|
113
|
+
checks.push({
|
|
114
|
+
name: `whisper model (${cfg.model})`,
|
|
115
|
+
ok: modelOk,
|
|
116
|
+
detail: modelPath,
|
|
117
|
+
...(modelOk
|
|
118
|
+
? {}
|
|
119
|
+
: {
|
|
120
|
+
fix:
|
|
121
|
+
`mkdir -p ${cfg.modelDir} && curl -L -o ${modelPath} ` +
|
|
122
|
+
`https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-${cfg.model}.bin`,
|
|
123
|
+
}),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Provider, in the same order auto-detection uses (gemini → claude →
|
|
127
|
+
// claude-cli). Needed for --produce only — the cut+captions path never
|
|
128
|
+
// touches an LLM — but doctor's contract is "ready for the whole thing".
|
|
129
|
+
const provider = p.env.GEMINI_API_KEY
|
|
130
|
+
? "gemini (GEMINI_API_KEY is set)"
|
|
131
|
+
: p.env.ANTHROPIC_API_KEY
|
|
132
|
+
? "claude (ANTHROPIC_API_KEY is set)"
|
|
133
|
+
: (await p.binRuns("claude", "--version"))
|
|
134
|
+
? "claude-cli (logged-in Claude Code)"
|
|
135
|
+
: null;
|
|
136
|
+
checks.push({
|
|
137
|
+
name: "LLM provider",
|
|
138
|
+
ok: provider !== null,
|
|
139
|
+
detail: provider ?? "no key set and no claude CLI on PATH — needed for --produce; cut+captions works without",
|
|
140
|
+
...(provider !== null
|
|
141
|
+
? {}
|
|
142
|
+
: {
|
|
143
|
+
fix:
|
|
144
|
+
"export ANTHROPIC_API_KEY or GEMINI_API_KEY (a .env file works — see README), " +
|
|
145
|
+
"or install Claude Code (https://claude.com/claude-code) and log in",
|
|
146
|
+
}),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
checks.push({
|
|
150
|
+
name: "editor page",
|
|
151
|
+
ok: p.editorPageDir !== null,
|
|
152
|
+
detail: p.editorPageDir ?? "not built — needed for `ossclip edit` only",
|
|
153
|
+
...(p.editorPageDir !== null
|
|
154
|
+
? {}
|
|
155
|
+
: { fix: "run `pnpm build` once in the ossclip checkout (npm installs ship it prebuilt)" }),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return checks;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function formatDoctor(checks: DoctorCheck[]): string {
|
|
162
|
+
const lines = checks.map((c) => {
|
|
163
|
+
const head = `${c.ok ? "✓" : "✗"} ${c.name.padEnd(24)} ${c.detail}`;
|
|
164
|
+
return c.fix ? `${head}\n ↳ ${c.fix}` : head;
|
|
165
|
+
});
|
|
166
|
+
const missing = checks.filter((c) => !c.ok).length;
|
|
167
|
+
lines.push(
|
|
168
|
+
missing === 0
|
|
169
|
+
? "\nAll checks passed — you're ready to `ossclip produce`."
|
|
170
|
+
: `\n${missing} check${missing === 1 ? "" : "s"} failed — fixes above, then re-run \`ossclip doctor\`.`,
|
|
171
|
+
);
|
|
172
|
+
return lines.join("\n");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The real probes the CLI wires in. */
|
|
176
|
+
export const realProbes = (editorPageDir: string | null): DoctorProbes => ({
|
|
177
|
+
binRuns: (bin, arg) =>
|
|
178
|
+
new Promise((resolve) => {
|
|
179
|
+
// Existence is the question, not exit code — whisper-cli --help exits
|
|
180
|
+
// nonzero and is still installed. Only a spawn error (ENOENT) fails.
|
|
181
|
+
const child = spawn(bin, [arg], { stdio: "ignore" });
|
|
182
|
+
child.on("error", () => resolve(false));
|
|
183
|
+
child.on("exit", () => resolve(true));
|
|
184
|
+
}),
|
|
185
|
+
exists: existsSync,
|
|
186
|
+
env: process.env,
|
|
187
|
+
platform: process.platform,
|
|
188
|
+
nodeMajor: Number.parseInt(process.versions.node, 10),
|
|
189
|
+
editorPageDir,
|
|
190
|
+
});
|