video-editing 1.3.1 → 3.0.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/dist/index.js CHANGED
@@ -1,300 +1,351 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  CAPABILITIES,
4
- COMPOSE_AUDIO_SRC,
5
- COMPOSE_ENTRY,
6
- COMPOSE_HEIGHT,
7
- COMPOSE_VIDEO_SRC,
8
- COMPOSE_WIDTH,
9
- DEFAULT_DESILENCE,
10
- PRESETS,
11
- agentizePrompt,
12
- artifactPaths,
13
- autoGradeForClip,
4
+ CAPTION_STYLE_DEFAULTS,
5
+ CloudClient,
6
+ CloudHttpError,
7
+ DEFAULT_DOWNLOAD_TIMEOUT_MS,
8
+ DEFAULT_TIMEOUT_MS,
9
+ INLINE_MAX_BYTES,
10
+ MAX_POLL_MS,
11
+ MAX_RUN_POLL_MS,
12
+ MULTIPART_THRESHOLD_BYTES,
13
+ RELAY_DIRECT_ATTEMPTS,
14
+ RELAY_SUBCHUNK_BYTES,
15
+ REVIEW_STAGES2,
16
+ assertRunCompleted,
17
+ assetContentType2,
18
+ assetsUrl,
19
+ bundledSkillDir,
20
+ bytesSource,
14
21
  checkCapabilities,
15
- compileTimeline,
16
- composeCounts,
17
- composeHtml,
18
- computeEnvelope,
19
- configDir,
20
- configEnvPath,
21
- cutBoundaries,
22
- decideGradeFilter,
23
- desilenceEdl,
24
- detectSilences,
25
- diagnosticsPath,
26
- duplicatedPhrase,
27
- duplicatedPhrases,
28
- duplicatedSpans,
29
- edlPath,
30
- ensureTranscribed,
31
- ensureVerticalMaster,
32
- findPhrase,
33
- fuzzySuggestions,
34
- gatherCompileCtx,
35
- generateQcImages,
36
- getPreset,
37
- learnedEditingGuardrails,
38
- loadApiKey,
39
- loadTimeline,
40
- loadWords,
41
- loadWordsBySource,
42
- normalizePhrase,
43
- normalizeToken,
44
- packTranscripts,
45
- parseSilenceDetect,
22
+ collectPlanAssetRefs2,
23
+ compileRemote,
24
+ compileUrl,
25
+ composeUrl,
26
+ defaultRenderOut,
27
+ defaultSkillTarget,
28
+ diagnosticsPath2,
29
+ downloadTo,
30
+ emptyDiagnostics,
31
+ filesUrl,
32
+ formatRunEvent,
33
+ framesPngPath,
34
+ ingestArtifactSet,
35
+ ingestUrl,
36
+ initCloudJob,
37
+ installSkill,
38
+ instructionPath2,
39
+ isTerminalRunStatus,
40
+ isTerminalStatus,
41
+ jobJsonPath,
42
+ joinSourceWindows,
43
+ loadPublicConfigEnv,
44
+ loadTimeline2,
45
+ meUrl,
46
+ multipartRelayUrl,
47
+ multipartUrl,
48
+ openFileSource,
49
+ outWindowToSourceWindows,
50
+ parseFileContent,
51
+ parseFileList,
52
+ parseFilePresign,
53
+ parseRenderCreate,
54
+ parseRenderState,
55
+ parseRunState,
46
56
  parseTimeline,
47
- planPath,
48
- readEdl,
49
- render,
50
- resolvePhrase,
51
- screenTakes,
52
- sha256File,
53
- silenceDetectArgs,
54
- statusPayload,
55
- straddlingGap,
56
- tightenRange,
57
- tightenRanges,
58
- timelineFromEdl,
59
- timelineHash,
60
- timelineJsonPath,
57
+ planPath2,
58
+ pollRender,
59
+ pollRun,
60
+ projectVideosUrl,
61
+ projectsUrl,
62
+ publicConfigDir,
63
+ publicConfigEnvPath,
64
+ pullFiles,
65
+ pullReview2,
66
+ pushComplete2,
67
+ pushFiles,
68
+ pushStage2,
69
+ pushTimeline2,
70
+ qaDir,
71
+ readJobFile,
72
+ readOutcomesFile2,
73
+ readPlanSegments,
74
+ readProbeDuration,
75
+ renderUrl,
76
+ rendersUrl,
77
+ resolveApiBase,
78
+ resolveApiKey,
79
+ resolveConfig,
80
+ resolveRunId2,
81
+ resolveUrl,
82
+ resolveVideoId,
83
+ reviewDir2,
84
+ reviewUrl,
85
+ roundJsonPath2,
86
+ runUrl,
87
+ sameSha,
88
+ sanitizeStem,
89
+ savePublicConfigKey,
90
+ sha256Hex,
91
+ shouldMultipart,
92
+ statAssetsForDoc,
93
+ stripTrailingSlash,
94
+ syncAssets2,
95
+ timelineJsonPath2,
61
96
  timelineV2Schema,
62
- timelineView,
63
- transcribeOne,
64
- transcriptFileForSource,
65
- updateCaptionsBlock,
97
+ toVfsPath,
98
+ transcribeArtifactSet,
99
+ transcribeCloud,
100
+ transcribeUrl,
101
+ uploadMultipart,
102
+ uploadVfsBytes,
103
+ uploadVfsPayload,
66
104
  usesFromDoc,
105
+ validateLocal,
67
106
  validateTimeline,
68
- verify,
69
- wordStream,
70
- writeAgentManifest,
71
- writeBoundaries,
72
- writeCompileArtifacts,
73
- writeCoverage,
74
- writeSilencePrompt,
75
- writeTranscriptArtifacts,
76
- writeVideoEditingPrompt,
77
- writeWarningsFromReview,
78
- writeWordDump
79
- } from "./chunk-63HIQBDT.js";
80
- import {
81
- CommandError,
82
- DEFAULT_EDIT_ROOT,
83
- VIDEO_EXTS,
84
- defaultJobDir,
85
- displayedDimensions,
86
- ffprobe,
87
- initJob,
88
- isoNow,
89
- loadJob,
90
- mediaDuration,
91
- pyFixed,
92
- pyFixedPad,
93
- pyRound,
94
- pySpacePad,
95
- requireFfmpeg,
96
- resolveJob,
97
- rotationDegrees,
98
- run,
99
- runSync,
100
- shown,
101
- slugify,
102
- sourceFromJob,
103
- transcriptPath,
104
- videoStream
105
- } from "./chunk-CU3NBKB4.js";
107
+ verifyCloud,
108
+ verifyUrl,
109
+ vfsContentType,
110
+ vfsLocalPath,
111
+ videoIdFromJob,
112
+ waveformPngPath,
113
+ waveformWordsPath,
114
+ writeJobFile
115
+ } from "./chunk-W4IIL2T2.js";
106
116
 
107
- // src/transcribe-batch.ts
108
- import { existsSync, mkdirSync, readdirSync, statSync } from "fs";
109
- import { basename, extname, join } from "path";
110
- import pLimit from "p-limit";
111
- var VIDEO_EXTS2 = /* @__PURE__ */ new Set([
112
- ".mp4",
113
- ".MP4",
114
- ".mov",
115
- ".MOV",
116
- ".mkv",
117
- ".MKV",
118
- ".avi",
119
- ".AVI",
120
- ".m4v"
121
- ]);
122
- function findVideos(videosDir) {
123
- const entries = readdirSync(videosDir).map((name) => join(videosDir, name)).filter((p) => {
124
- let isFile;
125
- try {
126
- isFile = statSync(p).isFile();
127
- } catch {
128
- return false;
129
- }
130
- return isFile && VIDEO_EXTS2.has(extname(p));
117
+ // src/timeline-v2/anchors.ts
118
+ function normalizeToken(text) {
119
+ return text.trim().toLowerCase().replace(/^[^\p{L}\p{N}]+/gu, "").replace(/[^\p{L}\p{N}]+$/gu, "");
120
+ }
121
+ function normalizePhrase(phrase) {
122
+ return phrase.split(/\s+/).map(normalizeToken).filter((t) => t.length > 0);
123
+ }
124
+ function wordStream(transcript) {
125
+ const words = [];
126
+ for (const w of transcript.words ?? []) {
127
+ if ((w.type ?? "word") !== "word") continue;
128
+ if (typeof w.start !== "number" || typeof w.end !== "number") continue;
129
+ words.push(w);
130
+ }
131
+ words.sort((a, b) => Number(a.start) - Number(b.start) || Number(a.end) - Number(b.end));
132
+ return words.map((w, i) => {
133
+ const text = (w.text ?? "").trim();
134
+ return { index: i, start: Number(w.start), end: Number(w.end), text, norm: normalizeToken(text) };
131
135
  });
132
- entries.sort();
133
- return entries;
134
136
  }
135
- function stemOf(p) {
136
- return basename(p, extname(p));
137
+ function contextAround(stream, from, to, pad = 5) {
138
+ const before = stream.slice(Math.max(0, from - pad), from).map((w) => w.text);
139
+ const matched = stream.slice(from, to + 1).map((w) => w.text);
140
+ const after = stream.slice(to + 1, to + 1 + pad).map((w) => w.text);
141
+ const parts = [];
142
+ if (from - pad > 0) parts.push("\u2026");
143
+ parts.push(...before, `\xAB${matched.join(" ")}\xBB`, ...after);
144
+ if (to + 1 + pad < stream.length) parts.push("\u2026");
145
+ return parts.join(" ");
137
146
  }
138
- async function transcribeBatch(videosDir, opts = {}) {
139
- const { workers = 4, language, numSpeakers, apiKey } = opts;
140
- if (!existsSync(videosDir) || !statSync(videosDir).isDirectory()) {
141
- throw new Error(`not a directory: ${videosDir}`);
147
+ function findPhrase(stream, phrase) {
148
+ const tokens = normalizePhrase(phrase);
149
+ if (tokens.length === 0) return [];
150
+ const matches = [];
151
+ for (let i = 0; i + tokens.length <= stream.length; i++) {
152
+ let ok = true;
153
+ for (let j = 0; j < tokens.length; j++) {
154
+ if (stream[i + j].norm !== tokens[j]) {
155
+ ok = false;
156
+ break;
157
+ }
158
+ }
159
+ if (!ok) continue;
160
+ const last = i + tokens.length - 1;
161
+ matches.push({
162
+ occurrence: matches.length + 1,
163
+ start: stream[i].start,
164
+ end: stream[last].end,
165
+ words: stream.slice(i, last + 1).map((w) => ({ start: w.start, end: w.end, text: w.text })),
166
+ context: contextAround(stream, i, last),
167
+ wordIndex: i
168
+ });
142
169
  }
143
- const editDir = opts.editDir ?? join(videosDir, "edit");
144
- mkdirSync(join(editDir, "transcripts"), { recursive: true });
145
- const videos = findVideos(videosDir);
146
- if (videos.length === 0) {
147
- throw new Error(`no videos found in ${videosDir}`);
170
+ return matches;
171
+ }
172
+ function levenshtein(a, b) {
173
+ const m = a.length;
174
+ const n = b.length;
175
+ if (m === 0) return n;
176
+ if (n === 0) return m;
177
+ let prev = new Array(n + 1);
178
+ let cur = new Array(n + 1);
179
+ for (let j = 0; j <= n; j++) prev[j] = j;
180
+ for (let i = 1; i <= m; i++) {
181
+ cur[0] = i;
182
+ for (let j = 1; j <= n; j++) {
183
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
184
+ cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost);
185
+ }
186
+ [prev, cur] = [cur, prev];
187
+ }
188
+ return prev[n];
189
+ }
190
+ function fuzzySuggestions(stream, phrase, limit = 3) {
191
+ const tokens = normalizePhrase(phrase);
192
+ if (tokens.length === 0 || stream.length === 0) return [];
193
+ const target = tokens.join(" ");
194
+ const n = Math.min(tokens.length, stream.length);
195
+ const scored = [];
196
+ for (let i = 0; i + n <= stream.length; i++) {
197
+ const window = stream.slice(i, i + n);
198
+ const joined = window.map((w) => w.norm).join(" ");
199
+ const dist = levenshtein(target, joined);
200
+ const similarity = 1 - dist / Math.max(target.length, joined.length, 1);
201
+ scored.push({
202
+ start: window[0].start,
203
+ end: window[n - 1].end,
204
+ text: window.map((w) => w.text).join(" "),
205
+ similarity: Math.round(similarity * 1e3) / 1e3
206
+ });
207
+ }
208
+ scored.sort((a, b) => b.similarity - a.similarity || a.start - b.start);
209
+ return scored.slice(0, limit).filter((s) => s.similarity > 0);
210
+ }
211
+ function resolvePhrase(stream, phrase, opts = {}) {
212
+ const matches = findPhrase(stream, phrase);
213
+ if (matches.length === 0) {
214
+ return { status: "not-found", suggestions: fuzzySuggestions(stream, phrase) };
148
215
  }
149
- const alreadyCached = videos.filter(
150
- (v) => existsSync(join(editDir, "transcripts", `${stemOf(v)}.json`))
151
- );
152
- const cachedSet = new Set(alreadyCached);
153
- const pending = videos.filter((v) => !cachedSet.has(v));
154
- console.error(
155
- `found ${videos.length} videos (${alreadyCached.length} cached, ${pending.length} to transcribe)`
156
- );
157
- if (pending.length === 0) {
158
- console.error("nothing to do");
159
- return { videos, cached: alreadyCached, transcribed: [], errors: [] };
216
+ if (opts.occurrence !== void 0) {
217
+ const m = matches[opts.occurrence - 1];
218
+ if (!m) return { status: "ambiguous", candidates: matches };
219
+ return { status: "ok", match: m, matches };
160
220
  }
161
- console.error(`transcribing ${pending.length} files with ${workers} parallel workers`);
162
- const t0 = Date.now();
163
- const errors = [];
164
- const transcribed = [];
165
- const limit = pLimit(workers);
166
- await Promise.all(
167
- pending.map(
168
- (v) => limit(async () => {
169
- try {
170
- const out = await transcribeOne(v, {
171
- editDir,
172
- language,
173
- numSpeakers,
174
- apiKey,
175
- verbose: false
176
- });
177
- transcribed.push(out);
178
- console.error(` + ${stemOf(v)} -> ${basename(out)}`);
179
- } catch (e) {
180
- const message = String(e instanceof Error ? e.message : e);
181
- errors.push({ video: v, message });
182
- console.error(` x ${stemOf(v)} FAILED: ${message}`);
183
- }
184
- })
185
- )
186
- );
187
- const dt = (Date.now() - t0) / 1e3;
188
- console.error(`
189
- done in ${dt.toFixed(1)}s`);
190
- if (errors.length) {
191
- console.error(`${errors.length} failures:`);
192
- for (const { video, message } of errors) {
193
- console.error(` ${basename(video)}: ${message}`);
221
+ if (matches.length === 1) return { status: "ok", match: matches[0], matches };
222
+ if (opts.nearS !== void 0) {
223
+ const near = opts.nearS;
224
+ let best = matches[0];
225
+ for (const m of matches) {
226
+ if (Math.abs(m.start - near) < Math.abs(best.start - near)) best = m;
194
227
  }
228
+ return { status: "ok", match: best, matches };
195
229
  }
196
- return { videos, cached: alreadyCached, transcribed, errors };
230
+ return { status: "ambiguous", candidates: matches };
197
231
  }
198
232
  export {
199
233
  CAPABILITIES,
200
- COMPOSE_AUDIO_SRC,
201
- COMPOSE_ENTRY,
202
- COMPOSE_HEIGHT,
203
- COMPOSE_VIDEO_SRC,
204
- COMPOSE_WIDTH,
205
- CommandError,
206
- DEFAULT_DESILENCE,
207
- DEFAULT_EDIT_ROOT,
208
- PRESETS,
209
- VIDEO_EXTS,
210
- agentizePrompt,
211
- artifactPaths,
212
- autoGradeForClip,
234
+ CAPTION_STYLE_DEFAULTS,
235
+ CloudClient,
236
+ CloudHttpError,
237
+ DEFAULT_DOWNLOAD_TIMEOUT_MS,
238
+ DEFAULT_TIMEOUT_MS,
239
+ INLINE_MAX_BYTES,
240
+ MAX_POLL_MS,
241
+ MAX_RUN_POLL_MS,
242
+ MULTIPART_THRESHOLD_BYTES,
243
+ RELAY_DIRECT_ATTEMPTS,
244
+ RELAY_SUBCHUNK_BYTES,
245
+ REVIEW_STAGES2,
246
+ assertRunCompleted,
247
+ assetContentType2,
248
+ assetsUrl,
249
+ bundledSkillDir,
250
+ bytesSource,
213
251
  checkCapabilities,
214
- compileTimeline,
215
- composeCounts,
216
- composeHtml,
217
- computeEnvelope,
218
- configDir,
219
- configEnvPath,
220
- cutBoundaries,
221
- decideGradeFilter,
222
- defaultJobDir,
223
- desilenceEdl,
224
- detectSilences,
225
- diagnosticsPath,
226
- displayedDimensions,
227
- duplicatedPhrase,
228
- duplicatedPhrases,
229
- duplicatedSpans,
230
- edlPath,
231
- ensureTranscribed,
232
- ensureVerticalMaster,
233
- ffprobe,
252
+ collectPlanAssetRefs2,
253
+ compileRemote,
254
+ compileUrl,
255
+ composeUrl,
256
+ defaultRenderOut,
257
+ defaultSkillTarget,
258
+ diagnosticsPath2,
259
+ downloadTo,
260
+ emptyDiagnostics,
261
+ filesUrl,
234
262
  findPhrase,
263
+ formatRunEvent,
264
+ framesPngPath,
235
265
  fuzzySuggestions,
236
- gatherCompileCtx,
237
- generateQcImages,
238
- getPreset,
239
- initJob,
240
- isoNow,
241
- learnedEditingGuardrails,
242
- loadApiKey,
243
- loadJob,
244
- loadTimeline,
245
- loadWords,
246
- loadWordsBySource,
247
- mediaDuration,
266
+ ingestArtifactSet,
267
+ ingestUrl,
268
+ initCloudJob,
269
+ installSkill,
270
+ instructionPath2,
271
+ isTerminalRunStatus,
272
+ isTerminalStatus,
273
+ jobJsonPath,
274
+ joinSourceWindows,
275
+ loadPublicConfigEnv,
276
+ loadTimeline2,
277
+ meUrl,
278
+ multipartRelayUrl,
279
+ multipartUrl,
248
280
  normalizePhrase,
249
281
  normalizeToken,
250
- packTranscripts,
251
- parseSilenceDetect,
282
+ openFileSource,
283
+ outWindowToSourceWindows,
284
+ parseFileContent,
285
+ parseFileList,
286
+ parseFilePresign,
287
+ parseRenderCreate,
288
+ parseRenderState,
289
+ parseRunState,
252
290
  parseTimeline,
253
- planPath,
254
- pyFixed,
255
- pyFixedPad,
256
- pyRound,
257
- pySpacePad,
258
- readEdl,
259
- render,
260
- requireFfmpeg,
261
- resolveJob,
291
+ planPath2,
292
+ pollRender,
293
+ pollRun,
294
+ projectVideosUrl,
295
+ projectsUrl,
296
+ publicConfigDir,
297
+ publicConfigEnvPath,
298
+ pullFiles,
299
+ pullReview2,
300
+ pushComplete2,
301
+ pushFiles,
302
+ pushStage2,
303
+ pushTimeline2,
304
+ qaDir,
305
+ readJobFile,
306
+ readOutcomesFile2,
307
+ readPlanSegments,
308
+ readProbeDuration,
309
+ renderUrl,
310
+ rendersUrl,
311
+ resolveApiBase,
312
+ resolveApiKey,
313
+ resolveConfig,
262
314
  resolvePhrase,
263
- rotationDegrees,
264
- run,
265
- runSync,
266
- screenTakes,
267
- sha256File,
268
- shown,
269
- silenceDetectArgs,
270
- slugify,
271
- sourceFromJob,
272
- statusPayload,
273
- straddlingGap,
274
- tightenRange,
275
- tightenRanges,
276
- timelineFromEdl,
277
- timelineHash,
278
- timelineJsonPath,
315
+ resolveRunId2,
316
+ resolveUrl,
317
+ resolveVideoId,
318
+ reviewDir2,
319
+ reviewUrl,
320
+ roundJsonPath2,
321
+ runUrl,
322
+ sameSha,
323
+ sanitizeStem,
324
+ savePublicConfigKey,
325
+ sha256Hex,
326
+ shouldMultipart,
327
+ statAssetsForDoc,
328
+ stripTrailingSlash,
329
+ syncAssets2,
330
+ timelineJsonPath2,
279
331
  timelineV2Schema,
280
- timelineView,
281
- transcribeBatch,
282
- transcribeOne,
283
- transcriptFileForSource,
284
- transcriptPath,
285
- updateCaptionsBlock,
332
+ toVfsPath,
333
+ transcribeArtifactSet,
334
+ transcribeCloud,
335
+ transcribeUrl,
336
+ uploadMultipart,
337
+ uploadVfsBytes,
338
+ uploadVfsPayload,
286
339
  usesFromDoc,
340
+ validateLocal,
287
341
  validateTimeline,
288
- verify,
289
- videoStream,
342
+ verifyCloud,
343
+ verifyUrl,
344
+ vfsContentType,
345
+ vfsLocalPath,
346
+ videoIdFromJob,
347
+ waveformPngPath,
348
+ waveformWordsPath,
290
349
  wordStream,
291
- writeAgentManifest,
292
- writeBoundaries,
293
- writeCompileArtifacts,
294
- writeCoverage,
295
- writeSilencePrompt,
296
- writeTranscriptArtifacts,
297
- writeVideoEditingPrompt,
298
- writeWarningsFromReview,
299
- writeWordDump
350
+ writeJobFile
300
351
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "video-editing",
3
- "version": "1.3.1",
4
- "description": "Standalone TypeScript library + CLI for editing talking-head reels: transcribe, screen, cut (EDL), render vertical 9:16, QC, content-verify.",
3
+ "version": "3.0.0",
4
+ "description": "Pure-API cloud CLI + client for editing talking-head reels with clipready: upload once, then transcription, timeline authoring, compile, render, verify and QA all run in the cloud. Node ≥ 20 + an API key — no ffmpeg.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
7
7
  "author": "Itay Elgazar",
@@ -45,25 +45,8 @@
45
45
  "skills",
46
46
  "LICENSE"
47
47
  ],
48
- "scripts": {
49
- "build": "tsup",
50
- "dev": "tsup --watch",
51
- "typecheck": "tsc --noEmit",
52
- "test": "vitest run",
53
- "test:watch": "vitest",
54
- "prepublishOnly": "pnpm build"
55
- },
56
48
  "dependencies": {
57
- "@napi-rs/canvas": "^0.1.65",
58
49
  "commander": "^12.1.0",
59
- "p-limit": "^6.1.0",
60
50
  "zod": "^3"
61
- },
62
- "devDependencies": {
63
- "@types/node": "^22.10.0",
64
- "tsup": "^8.3.5",
65
- "tsx": "^4.19.2",
66
- "typescript": "^5.7.2",
67
- "vitest": "^2.1.8"
68
51
  }
69
52
  }