video-editing 1.3.1 → 2.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,337 @@
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,
4
+ CAPTION_STYLE_DEFAULTS,
5
+ CloudClient,
6
+ CloudHttpError,
9
7
  DEFAULT_DESILENCE,
10
- PRESETS,
11
- agentizePrompt,
12
- artifactPaths,
13
- autoGradeForClip,
8
+ DEFAULT_DOWNLOAD_TIMEOUT_MS,
9
+ DEFAULT_TIMEOUT_MS,
10
+ INLINE_MAX_BYTES,
11
+ MAX_POLL_MS,
12
+ MAX_RUN_POLL_MS,
13
+ REVIEW_STAGES2,
14
+ assertRunCompleted,
15
+ assetContentType2,
16
+ assetsUrl,
17
+ buildMediaEntry,
18
+ bundledSkillDir,
14
19
  checkCapabilities,
15
- compileTimeline,
16
- composeCounts,
17
- composeHtml,
18
- computeEnvelope,
19
- configDir,
20
- configEnvPath,
21
- cutBoundaries,
22
- decideGradeFilter,
23
- desilenceEdl,
20
+ collectPlanAssetRefs2,
21
+ compileRemote,
22
+ compileUrl,
23
+ composeCloud,
24
+ composeUrl,
25
+ defaultRenderOut,
26
+ defaultSkillTarget,
24
27
  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,
28
+ detectSilencesForDoc,
29
+ diagnosticsPath2,
30
+ downloadTo,
31
+ emptyDiagnostics,
32
+ extractGuardedWav,
33
+ filesUrl,
34
+ findLocalTranscript,
35
+ formatRunEvent,
36
+ initCloudJob,
37
+ installSkill,
38
+ instructionPath2,
39
+ isTerminalRunStatus,
40
+ isTerminalStatus,
41
+ jobJsonPath,
42
+ loadPublicConfigEnv,
43
+ loadTimeline2,
44
+ meUrl,
45
+ parseFileContent,
46
+ parseFileList,
47
+ parseFilePresign,
48
+ parseRenderCreate,
49
+ parseRenderState,
50
+ parseRunState,
45
51
  parseSilenceDetect,
46
52
  parseTimeline,
47
- planPath,
48
- readEdl,
49
- render,
50
- resolvePhrase,
51
- screenTakes,
52
- sha256File,
53
+ planPath2,
54
+ pollRender,
55
+ pollRun,
56
+ projectVideosUrl,
57
+ projectsUrl,
58
+ publicConfigDir,
59
+ publicConfigEnvPath,
60
+ pullFiles,
61
+ pullReview2,
62
+ pushComplete2,
63
+ pushFiles,
64
+ pushStage2,
65
+ pushTimeline2,
66
+ qaFramesArgs,
67
+ qaFramesLocal,
68
+ qaWaveformArgs,
69
+ qaWaveformLocal,
70
+ qaWordsForWindow,
71
+ readJobFile,
72
+ readOutcomesFile2,
73
+ renderUrl,
74
+ rendersUrl,
75
+ resolveApiBase,
76
+ resolveApiKey,
77
+ resolveConfig,
78
+ resolveRunId2,
79
+ resolveUrl,
80
+ resolveVideoId,
81
+ reviewDir2,
82
+ reviewUrl,
83
+ roundJsonPath2,
84
+ runUrl,
85
+ sameSha,
86
+ savePublicConfigKey,
87
+ sha256Hex,
53
88
  silenceDetectArgs,
54
- statusPayload,
55
- straddlingGap,
56
- tightenRange,
57
- tightenRanges,
58
- timelineFromEdl,
59
- timelineHash,
60
- timelineJsonPath,
89
+ statAssetsForDoc,
90
+ stripTrailingSlash,
91
+ syncAssets2,
92
+ timelineJsonPath2,
61
93
  timelineV2Schema,
62
- timelineView,
63
- transcribeOne,
64
- transcriptFileForSource,
65
- updateCaptionsBlock,
94
+ toVfsPath,
95
+ transcribeArtifactSet,
96
+ transcribeCloud,
97
+ transcribeUrl,
98
+ uploadVfsBytes,
66
99
  usesFromDoc,
100
+ validateLocal,
67
101
  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";
102
+ verifyCloud,
103
+ verifyUrl,
104
+ vfsContentType,
105
+ vfsLocalPath,
106
+ videoIdFromJob,
107
+ writeJobFile
108
+ } from "./chunk-SSXAUKGV.js";
106
109
 
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));
110
+ // src/timeline-v2/anchors.ts
111
+ function normalizeToken(text) {
112
+ return text.trim().toLowerCase().replace(/^[^\p{L}\p{N}]+/gu, "").replace(/[^\p{L}\p{N}]+$/gu, "");
113
+ }
114
+ function normalizePhrase(phrase) {
115
+ return phrase.split(/\s+/).map(normalizeToken).filter((t) => t.length > 0);
116
+ }
117
+ function wordStream(transcript) {
118
+ const words = [];
119
+ for (const w of transcript.words ?? []) {
120
+ if ((w.type ?? "word") !== "word") continue;
121
+ if (typeof w.start !== "number" || typeof w.end !== "number") continue;
122
+ words.push(w);
123
+ }
124
+ words.sort((a, b) => Number(a.start) - Number(b.start) || Number(a.end) - Number(b.end));
125
+ return words.map((w, i) => {
126
+ const text = (w.text ?? "").trim();
127
+ return { index: i, start: Number(w.start), end: Number(w.end), text, norm: normalizeToken(text) };
131
128
  });
132
- entries.sort();
133
- return entries;
134
129
  }
135
- function stemOf(p) {
136
- return basename(p, extname(p));
130
+ function contextAround(stream, from, to, pad = 5) {
131
+ const before = stream.slice(Math.max(0, from - pad), from).map((w) => w.text);
132
+ const matched = stream.slice(from, to + 1).map((w) => w.text);
133
+ const after = stream.slice(to + 1, to + 1 + pad).map((w) => w.text);
134
+ const parts = [];
135
+ if (from - pad > 0) parts.push("\u2026");
136
+ parts.push(...before, `\xAB${matched.join(" ")}\xBB`, ...after);
137
+ if (to + 1 + pad < stream.length) parts.push("\u2026");
138
+ return parts.join(" ");
137
139
  }
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}`);
140
+ function findPhrase(stream, phrase) {
141
+ const tokens = normalizePhrase(phrase);
142
+ if (tokens.length === 0) return [];
143
+ const matches = [];
144
+ for (let i = 0; i + tokens.length <= stream.length; i++) {
145
+ let ok = true;
146
+ for (let j = 0; j < tokens.length; j++) {
147
+ if (stream[i + j].norm !== tokens[j]) {
148
+ ok = false;
149
+ break;
150
+ }
151
+ }
152
+ if (!ok) continue;
153
+ const last = i + tokens.length - 1;
154
+ matches.push({
155
+ occurrence: matches.length + 1,
156
+ start: stream[i].start,
157
+ end: stream[last].end,
158
+ words: stream.slice(i, last + 1).map((w) => ({ start: w.start, end: w.end, text: w.text })),
159
+ context: contextAround(stream, i, last),
160
+ wordIndex: i
161
+ });
142
162
  }
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}`);
163
+ return matches;
164
+ }
165
+ function levenshtein(a, b) {
166
+ const m = a.length;
167
+ const n = b.length;
168
+ if (m === 0) return n;
169
+ if (n === 0) return m;
170
+ let prev = new Array(n + 1);
171
+ let cur = new Array(n + 1);
172
+ for (let j = 0; j <= n; j++) prev[j] = j;
173
+ for (let i = 1; i <= m; i++) {
174
+ cur[0] = i;
175
+ for (let j = 1; j <= n; j++) {
176
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
177
+ cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + cost);
178
+ }
179
+ [prev, cur] = [cur, prev];
180
+ }
181
+ return prev[n];
182
+ }
183
+ function fuzzySuggestions(stream, phrase, limit = 3) {
184
+ const tokens = normalizePhrase(phrase);
185
+ if (tokens.length === 0 || stream.length === 0) return [];
186
+ const target = tokens.join(" ");
187
+ const n = Math.min(tokens.length, stream.length);
188
+ const scored = [];
189
+ for (let i = 0; i + n <= stream.length; i++) {
190
+ const window = stream.slice(i, i + n);
191
+ const joined = window.map((w) => w.norm).join(" ");
192
+ const dist = levenshtein(target, joined);
193
+ const similarity = 1 - dist / Math.max(target.length, joined.length, 1);
194
+ scored.push({
195
+ start: window[0].start,
196
+ end: window[n - 1].end,
197
+ text: window.map((w) => w.text).join(" "),
198
+ similarity: Math.round(similarity * 1e3) / 1e3
199
+ });
200
+ }
201
+ scored.sort((a, b) => b.similarity - a.similarity || a.start - b.start);
202
+ return scored.slice(0, limit).filter((s) => s.similarity > 0);
203
+ }
204
+ function resolvePhrase(stream, phrase, opts = {}) {
205
+ const matches = findPhrase(stream, phrase);
206
+ if (matches.length === 0) {
207
+ return { status: "not-found", suggestions: fuzzySuggestions(stream, phrase) };
148
208
  }
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: [] };
209
+ if (opts.occurrence !== void 0) {
210
+ const m = matches[opts.occurrence - 1];
211
+ if (!m) return { status: "ambiguous", candidates: matches };
212
+ return { status: "ok", match: m, matches };
160
213
  }
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}`);
214
+ if (matches.length === 1) return { status: "ok", match: matches[0], matches };
215
+ if (opts.nearS !== void 0) {
216
+ const near = opts.nearS;
217
+ let best = matches[0];
218
+ for (const m of matches) {
219
+ if (Math.abs(m.start - near) < Math.abs(best.start - near)) best = m;
194
220
  }
221
+ return { status: "ok", match: best, matches };
195
222
  }
196
- return { videos, cached: alreadyCached, transcribed, errors };
223
+ return { status: "ambiguous", candidates: matches };
197
224
  }
198
225
  export {
199
226
  CAPABILITIES,
200
- COMPOSE_AUDIO_SRC,
201
- COMPOSE_ENTRY,
202
- COMPOSE_HEIGHT,
203
- COMPOSE_VIDEO_SRC,
204
- COMPOSE_WIDTH,
205
- CommandError,
227
+ CAPTION_STYLE_DEFAULTS,
228
+ CloudClient,
229
+ CloudHttpError,
206
230
  DEFAULT_DESILENCE,
207
- DEFAULT_EDIT_ROOT,
208
- PRESETS,
209
- VIDEO_EXTS,
210
- agentizePrompt,
211
- artifactPaths,
212
- autoGradeForClip,
231
+ DEFAULT_DOWNLOAD_TIMEOUT_MS,
232
+ DEFAULT_TIMEOUT_MS,
233
+ INLINE_MAX_BYTES,
234
+ MAX_POLL_MS,
235
+ MAX_RUN_POLL_MS,
236
+ REVIEW_STAGES2,
237
+ assertRunCompleted,
238
+ assetContentType2,
239
+ assetsUrl,
240
+ buildMediaEntry,
241
+ bundledSkillDir,
213
242
  checkCapabilities,
214
- compileTimeline,
215
- composeCounts,
216
- composeHtml,
217
- computeEnvelope,
218
- configDir,
219
- configEnvPath,
220
- cutBoundaries,
221
- decideGradeFilter,
222
- defaultJobDir,
223
- desilenceEdl,
243
+ collectPlanAssetRefs2,
244
+ compileRemote,
245
+ compileUrl,
246
+ composeCloud,
247
+ composeUrl,
248
+ defaultRenderOut,
249
+ defaultSkillTarget,
224
250
  detectSilences,
225
- diagnosticsPath,
226
- displayedDimensions,
227
- duplicatedPhrase,
228
- duplicatedPhrases,
229
- duplicatedSpans,
230
- edlPath,
231
- ensureTranscribed,
232
- ensureVerticalMaster,
233
- ffprobe,
251
+ detectSilencesForDoc,
252
+ diagnosticsPath2,
253
+ downloadTo,
254
+ emptyDiagnostics,
255
+ extractGuardedWav,
256
+ filesUrl,
257
+ findLocalTranscript,
234
258
  findPhrase,
259
+ formatRunEvent,
235
260
  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,
261
+ initCloudJob,
262
+ installSkill,
263
+ instructionPath2,
264
+ isTerminalRunStatus,
265
+ isTerminalStatus,
266
+ jobJsonPath,
267
+ loadPublicConfigEnv,
268
+ loadTimeline2,
269
+ meUrl,
248
270
  normalizePhrase,
249
271
  normalizeToken,
250
- packTranscripts,
272
+ parseFileContent,
273
+ parseFileList,
274
+ parseFilePresign,
275
+ parseRenderCreate,
276
+ parseRenderState,
277
+ parseRunState,
251
278
  parseSilenceDetect,
252
279
  parseTimeline,
253
- planPath,
254
- pyFixed,
255
- pyFixedPad,
256
- pyRound,
257
- pySpacePad,
258
- readEdl,
259
- render,
260
- requireFfmpeg,
261
- resolveJob,
280
+ planPath2,
281
+ pollRender,
282
+ pollRun,
283
+ projectVideosUrl,
284
+ projectsUrl,
285
+ publicConfigDir,
286
+ publicConfigEnvPath,
287
+ pullFiles,
288
+ pullReview2,
289
+ pushComplete2,
290
+ pushFiles,
291
+ pushStage2,
292
+ pushTimeline2,
293
+ qaFramesArgs,
294
+ qaFramesLocal,
295
+ qaWaveformArgs,
296
+ qaWaveformLocal,
297
+ qaWordsForWindow,
298
+ readJobFile,
299
+ readOutcomesFile2,
300
+ renderUrl,
301
+ rendersUrl,
302
+ resolveApiBase,
303
+ resolveApiKey,
304
+ resolveConfig,
262
305
  resolvePhrase,
263
- rotationDegrees,
264
- run,
265
- runSync,
266
- screenTakes,
267
- sha256File,
268
- shown,
306
+ resolveRunId2,
307
+ resolveUrl,
308
+ resolveVideoId,
309
+ reviewDir2,
310
+ reviewUrl,
311
+ roundJsonPath2,
312
+ runUrl,
313
+ sameSha,
314
+ savePublicConfigKey,
315
+ sha256Hex,
269
316
  silenceDetectArgs,
270
- slugify,
271
- sourceFromJob,
272
- statusPayload,
273
- straddlingGap,
274
- tightenRange,
275
- tightenRanges,
276
- timelineFromEdl,
277
- timelineHash,
278
- timelineJsonPath,
317
+ statAssetsForDoc,
318
+ stripTrailingSlash,
319
+ syncAssets2,
320
+ timelineJsonPath2,
279
321
  timelineV2Schema,
280
- timelineView,
281
- transcribeBatch,
282
- transcribeOne,
283
- transcriptFileForSource,
284
- transcriptPath,
285
- updateCaptionsBlock,
322
+ toVfsPath,
323
+ transcribeArtifactSet,
324
+ transcribeCloud,
325
+ transcribeUrl,
326
+ uploadVfsBytes,
286
327
  usesFromDoc,
328
+ validateLocal,
287
329
  validateTimeline,
288
- verify,
289
- videoStream,
330
+ verifyCloud,
331
+ verifyUrl,
332
+ vfsContentType,
333
+ vfsLocalPath,
334
+ videoIdFromJob,
290
335
  wordStream,
291
- writeAgentManifest,
292
- writeBoundaries,
293
- writeCompileArtifacts,
294
- writeCoverage,
295
- writeSilencePrompt,
296
- writeTranscriptArtifacts,
297
- writeVideoEditingPrompt,
298
- writeWarningsFromReview,
299
- writeWordDump
336
+ writeJobFile
300
337
  };
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": "2.0.0",
4
+ "description": "Cloud CLI + client for editing talking-head reels with clipready: transcription, timeline authoring, verify, compose, QA and renders heavy lifting runs in the cloud.",
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
  }