@program-video/cli 0.1.12 → 0.2.1

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.
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ AVAILABLE_VOICES,
4
+ CATEGORIES,
5
+ CATEGORY_LABELS,
6
+ CURATED_TEXT_MODELS,
7
+ DEFAULT_IMAGE_MODEL,
8
+ DEFAULT_SPEECH_MODEL,
9
+ DEFAULT_VIDEO_MODEL,
10
+ GATEWAY_MODELS,
11
+ InferenceAuthError,
12
+ InferenceError,
13
+ InferenceInputError,
14
+ InferenceModelNotFoundError,
15
+ InferenceOutputError,
16
+ InferenceProviderError,
17
+ InferenceRateLimitError,
18
+ PRICING_UNITS,
19
+ PRICING_UNIT_LABELS,
20
+ REPLICATE_HARDWARE_COSTS,
21
+ REPLICATE_HARDWARE_OPTIONS,
22
+ SUPPORTED_MODELS,
23
+ SchemaError,
24
+ buildCostBreakdown,
25
+ calculateCharacterBasedCost,
26
+ calculateCredits,
27
+ calculateFieldBasedCost,
28
+ calculateImageBasedCost,
29
+ calculateMultiFieldBasedCost,
30
+ calculatePredictionCost,
31
+ calculateProviderCost,
32
+ calculateSellingCost,
33
+ calculateTokenBasedCost,
34
+ calculateVideoBasedCost,
35
+ clearSchemaCache,
36
+ detectProvider,
37
+ detectReferenceFields,
38
+ discoverModels,
39
+ discoverReplicateModels,
40
+ extractEnumValues,
41
+ extractHardwareType,
42
+ fetchCollection,
43
+ fetchModelInfo,
44
+ fetchModelSchema,
45
+ fetchModelVersion,
46
+ generateAudio,
47
+ generateImage,
48
+ generateVideo,
49
+ getAudioModels,
50
+ getCachedSchema,
51
+ getDefaultValue,
52
+ getHardwareCost,
53
+ getModel,
54
+ getModelPlanTier,
55
+ getOrderedProperties,
56
+ getReplicateClient,
57
+ getSchemaCacheStats,
58
+ getVideoModels,
59
+ handleReplicateError,
60
+ inferContentType,
61
+ invalidateSchema,
62
+ isFileInput,
63
+ isMultiFieldTier,
64
+ isProModel,
65
+ isSingleFieldTier,
66
+ isValidModel,
67
+ listModels,
68
+ parseAudioOutput,
69
+ parseImageOutput,
70
+ parseModelIdentifier,
71
+ parseModelSchema,
72
+ parseVideoOutput,
73
+ retry,
74
+ searchModels,
75
+ sleep,
76
+ transcribe,
77
+ withErrorHandling
78
+ } from "./chunk-C3UUQWKV.js";
79
+ import "./chunk-2H7UOFLK.js";
80
+ export {
81
+ AVAILABLE_VOICES,
82
+ CATEGORIES,
83
+ CATEGORY_LABELS,
84
+ CURATED_TEXT_MODELS,
85
+ DEFAULT_IMAGE_MODEL,
86
+ DEFAULT_SPEECH_MODEL,
87
+ DEFAULT_VIDEO_MODEL,
88
+ GATEWAY_MODELS,
89
+ InferenceAuthError,
90
+ InferenceError,
91
+ InferenceInputError,
92
+ InferenceModelNotFoundError,
93
+ InferenceOutputError,
94
+ InferenceProviderError,
95
+ InferenceRateLimitError,
96
+ PRICING_UNITS,
97
+ PRICING_UNIT_LABELS,
98
+ REPLICATE_HARDWARE_COSTS,
99
+ REPLICATE_HARDWARE_OPTIONS,
100
+ SUPPORTED_MODELS,
101
+ SchemaError,
102
+ buildCostBreakdown,
103
+ calculateCharacterBasedCost,
104
+ calculateCredits,
105
+ calculateFieldBasedCost,
106
+ calculateImageBasedCost,
107
+ calculateMultiFieldBasedCost,
108
+ calculatePredictionCost,
109
+ calculateProviderCost,
110
+ calculateSellingCost,
111
+ calculateTokenBasedCost,
112
+ calculateVideoBasedCost,
113
+ clearSchemaCache,
114
+ detectProvider,
115
+ detectReferenceFields,
116
+ discoverModels,
117
+ discoverReplicateModels,
118
+ extractEnumValues,
119
+ extractHardwareType,
120
+ fetchCollection,
121
+ fetchModelInfo,
122
+ fetchModelSchema,
123
+ fetchModelVersion,
124
+ generateAudio,
125
+ generateImage,
126
+ generateVideo,
127
+ getAudioModels,
128
+ getCachedSchema,
129
+ getDefaultValue,
130
+ getHardwareCost,
131
+ getModel,
132
+ getModelPlanTier,
133
+ getOrderedProperties,
134
+ getReplicateClient,
135
+ getSchemaCacheStats,
136
+ getVideoModels,
137
+ handleReplicateError,
138
+ inferContentType,
139
+ invalidateSchema,
140
+ isFileInput,
141
+ isMultiFieldTier,
142
+ isProModel,
143
+ isSingleFieldTier,
144
+ isValidModel,
145
+ listModels,
146
+ parseAudioOutput,
147
+ parseImageOutput,
148
+ parseModelIdentifier,
149
+ parseModelSchema,
150
+ parseVideoOutput,
151
+ retry,
152
+ searchModels,
153
+ sleep,
154
+ transcribe,
155
+ withErrorHandling
156
+ };
157
+ //# sourceMappingURL=src-JFTCRPDH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ generateUploadUrl,
4
+ getComposition,
5
+ getStorageUrl,
6
+ outputError,
7
+ outputSuccess,
8
+ updateComposition,
9
+ uploadToStorage
10
+ } from "./chunk-2PMNEXGG.js";
11
+ import "./chunk-2H7UOFLK.js";
12
+
13
+ // src/commands/audio/trim-silence.ts
14
+ import { execSync } from "child_process";
15
+ import { existsSync, mkdirSync, readFileSync, statSync } from "fs";
16
+ import path from "path";
17
+ import ora from "ora";
18
+ var AUDIO_CACHE_DIR = "/tmp/program-video-cache/audio";
19
+ function ensureFfmpeg() {
20
+ try {
21
+ execSync("ffmpeg -version", { stdio: "ignore" });
22
+ } catch {
23
+ throw new Error("ffmpeg is not installed. Install it with: brew install ffmpeg");
24
+ }
25
+ }
26
+ function getAudioDuration(filePath) {
27
+ const output = execSync(
28
+ `ffprobe -v quiet -show_entries format=duration -of csv=p=0 "${filePath}"`,
29
+ { encoding: "utf-8" }
30
+ ).trim();
31
+ return parseFloat(output);
32
+ }
33
+ function countSilences(filePath, threshold, minDuration) {
34
+ try {
35
+ const output = execSync(
36
+ `ffmpeg -i "${filePath}" -af silencedetect=noise=${threshold}:d=${minDuration} -f null - 2>&1`,
37
+ { encoding: "utf-8" }
38
+ );
39
+ return (output.match(/silence_start/g) ?? []).length;
40
+ } catch {
41
+ return 0;
42
+ }
43
+ }
44
+ function trimSilence(inputPath, outputPath, silenceDuration, threshold) {
45
+ execSync(
46
+ `ffmpeg -y -i "${inputPath}" -af "silenceremove=stop_periods=-1:stop_duration=${silenceDuration}:stop_threshold=${threshold}:stop_silence=${silenceDuration}" "${outputPath}"`,
47
+ { stdio: "ignore" }
48
+ );
49
+ }
50
+ async function trimSilenceCommand(compositionId, options) {
51
+ if (!compositionId) {
52
+ outputError("Composition ID is required", options);
53
+ process.exit(1);
54
+ }
55
+ const silenceDuration = parseFloat(options.silenceDuration ?? "0.25");
56
+ const threshold = options.threshold ?? "-30dB";
57
+ const showSpinner = !options.json && process.stdout.isTTY;
58
+ const spinner = showSpinner ? ora("Loading composition...").start() : null;
59
+ try {
60
+ ensureFfmpeg();
61
+ } catch (err) {
62
+ spinner?.stop();
63
+ outputError(err.message, options);
64
+ process.exit(1);
65
+ }
66
+ const composition = await getComposition(compositionId);
67
+ if (!composition) {
68
+ spinner?.stop();
69
+ outputError(`Composition not found: ${compositionId}`, options);
70
+ process.exit(1);
71
+ }
72
+ const scenes = composition.scenes ?? [];
73
+ const audioScenes = scenes.filter((s) => s.type === "audio_scene");
74
+ if (audioScenes.length === 0) {
75
+ spinner?.stop();
76
+ outputError("No audio scenes found in composition", options);
77
+ process.exit(1);
78
+ }
79
+ const targetScenes = options.scene ? audioScenes.filter((s) => s.id === options.scene) : audioScenes;
80
+ if (targetScenes.length === 0) {
81
+ spinner?.stop();
82
+ outputError(`Audio scene not found: ${options.scene}`, options);
83
+ process.exit(1);
84
+ }
85
+ mkdirSync(AUDIO_CACHE_DIR, { recursive: true });
86
+ const results = [];
87
+ let cumulativeTimeSaved = 0;
88
+ const allScenes = [...scenes].sort((a, b) => a.start - b.start);
89
+ for (const scene of targetScenes) {
90
+ const audioUrl = scene.config.audioUrl;
91
+ if (!audioUrl) {
92
+ if (spinner) spinner.text = `Skipping ${scene.id} \u2014 no URL`;
93
+ continue;
94
+ }
95
+ if (spinner) spinner.text = `Downloading ${scene.id}...`;
96
+ const originalPath = path.join(AUDIO_CACHE_DIR, `${scene.id}-original.wav`);
97
+ const trimmedPath = path.join(AUDIO_CACHE_DIR, `${scene.id}-trimmed.wav`);
98
+ const isLocalFile = audioUrl.startsWith("/") || audioUrl.startsWith("file://");
99
+ const isLocalCacheUrl = audioUrl.startsWith("/audio-cache/");
100
+ if (isLocalFile || isLocalCacheUrl) {
101
+ const localPath = isLocalCacheUrl ? path.join(AUDIO_CACHE_DIR, audioUrl.replace("/audio-cache/", "")) : audioUrl.replace("file://", "");
102
+ if (!existsSync(localPath)) {
103
+ if (spinner) spinner.text = `Skipping ${scene.id} \u2014 local file not found`;
104
+ continue;
105
+ }
106
+ execSync(`cp "${localPath}" "${originalPath}"`);
107
+ } else if (audioUrl.startsWith("http://") || audioUrl.startsWith("https://")) {
108
+ if (existsSync(originalPath) && statSync(originalPath).size > 1e3) {
109
+ if (spinner) spinner.text = `Using cached original for ${scene.id}`;
110
+ } else {
111
+ try {
112
+ execSync(`curl -sL "${audioUrl}" -o "${originalPath}"`, { timeout: 3e4 });
113
+ } catch {
114
+ if (spinner) spinner.text = `Failed to download ${scene.id}, skipping`;
115
+ continue;
116
+ }
117
+ const fileSize = statSync(originalPath).size;
118
+ if (fileSize < 1e3) {
119
+ if (spinner) spinner.text = `Download failed for ${scene.id} (URL may be expired), skipping`;
120
+ continue;
121
+ }
122
+ }
123
+ } else {
124
+ if (existsSync(originalPath) && statSync(originalPath).size > 1e3) {
125
+ if (spinner) spinner.text = `Using cached original for ${scene.id}`;
126
+ } else {
127
+ if (spinner) spinner.text = `Skipping ${scene.id} \u2014 unrecognized URL scheme`;
128
+ continue;
129
+ }
130
+ }
131
+ const originalDuration = getAudioDuration(originalPath);
132
+ const silenceCount = countSilences(originalPath, threshold, 0.3);
133
+ if (spinner) spinner.text = `Trimming ${scene.id} (${silenceCount} gaps found)...`;
134
+ trimSilence(originalPath, trimmedPath, silenceDuration, threshold);
135
+ const trimmedDuration = getAudioDuration(trimmedPath);
136
+ const savedSeconds = originalDuration - trimmedDuration;
137
+ if (spinner) spinner.text = `Uploading trimmed ${scene.id} to storage...`;
138
+ const fileData = readFileSync(trimmedPath);
139
+ const uploadUrl = await generateUploadUrl();
140
+ const storageId = await uploadToStorage(uploadUrl, fileData, "audio/wav");
141
+ const storageUrl = await getStorageUrl(storageId);
142
+ if (!storageUrl) {
143
+ if (spinner) spinner.text = `Failed to get storage URL for ${scene.id}, skipping`;
144
+ continue;
145
+ }
146
+ results.push({
147
+ sceneId: scene.id,
148
+ originalDuration,
149
+ trimmedDuration,
150
+ savedSeconds,
151
+ silenceCount,
152
+ outputPath: trimmedPath,
153
+ storageUrl,
154
+ storageId
155
+ });
156
+ }
157
+ if (results.length === 0) {
158
+ spinner?.stop();
159
+ outputError("No audio scenes could be processed", options);
160
+ process.exit(1);
161
+ }
162
+ if (options.dryRun) {
163
+ spinner?.stop();
164
+ if (options.json) {
165
+ outputSuccess({ dryRun: true, results }, options);
166
+ } else {
167
+ console.log("\nDry run results:");
168
+ for (const r of results) {
169
+ console.log(` ${r.sceneId}: ${r.originalDuration.toFixed(1)}s \u2192 ${r.trimmedDuration.toFixed(1)}s (saved ${r.savedSeconds.toFixed(1)}s, ${r.silenceCount} gaps)`);
170
+ }
171
+ const totalSaved = results.reduce((sum, r) => sum + r.savedSeconds, 0);
172
+ console.log(`
173
+ Total saved: ${totalSaved.toFixed(1)}s`);
174
+ }
175
+ return;
176
+ }
177
+ if (spinner) spinner.text = "Updating composition...";
178
+ const updatedScenes = allScenes.map((scene) => {
179
+ const result = results.find((r) => r.sceneId === scene.id);
180
+ if (result) {
181
+ const newStart = scene.start - cumulativeTimeSaved;
182
+ const newEnd = newStart + result.trimmedDuration;
183
+ cumulativeTimeSaved += result.savedSeconds;
184
+ return {
185
+ ...scene,
186
+ start: Math.max(0, newStart),
187
+ end: newEnd,
188
+ duration: result.trimmedDuration,
189
+ config: {
190
+ ...scene.config,
191
+ audioUrl: result.storageUrl,
192
+ storageId: result.storageId,
193
+ originalAudioUrl: scene.config.audioUrl
194
+ }
195
+ };
196
+ }
197
+ if (cumulativeTimeSaved > 0 && scene.start > 0) {
198
+ const newStart = Math.max(0, scene.start - cumulativeTimeSaved);
199
+ const newEnd = scene.end - cumulativeTimeSaved;
200
+ return {
201
+ ...scene,
202
+ start: newStart,
203
+ end: newEnd,
204
+ duration: newEnd - newStart
205
+ };
206
+ }
207
+ return scene;
208
+ });
209
+ const newTotalDuration = Math.max(...updatedScenes.map((s) => s.end));
210
+ await updateComposition(compositionId, {
211
+ scenes: updatedScenes,
212
+ duration: newTotalDuration
213
+ });
214
+ spinner?.stop();
215
+ if (options.json) {
216
+ outputSuccess({
217
+ compositionId,
218
+ results,
219
+ newDuration: newTotalDuration
220
+ }, options);
221
+ } else {
222
+ console.log("\nSilence trimmed:");
223
+ for (const r of results) {
224
+ console.log(` ${r.sceneId}: ${r.originalDuration.toFixed(1)}s \u2192 ${r.trimmedDuration.toFixed(1)}s (\u2212${r.savedSeconds.toFixed(1)}s, ${r.silenceCount} gaps)`);
225
+ }
226
+ const totalSaved = results.reduce((sum, r) => sum + r.savedSeconds, 0);
227
+ const oldDuration = Number(composition.duration ?? 0);
228
+ console.log(`
229
+ Composition: ${oldDuration.toFixed(1)}s \u2192 ${newTotalDuration.toFixed(1)}s (saved ${totalSaved.toFixed(1)}s)`);
230
+ console.log(` Silence retention: ${silenceDuration}s | Threshold: ${threshold}`);
231
+ console.log(`
232
+ Files saved to: ${AUDIO_CACHE_DIR}/`);
233
+ }
234
+ }
235
+ export {
236
+ trimSilenceCommand
237
+ };
238
+ //# sourceMappingURL=trim-silence-ELVHI2IN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/audio/trim-silence.ts"],"sourcesContent":["import { execSync } from \"node:child_process\";\nimport { existsSync, mkdirSync, readFileSync, statSync } from \"node:fs\";\nimport path from \"node:path\";\nimport ora from \"ora\";\n\nimport type { OutputOptions } from \"../../lib/output.js\";\nimport {\n generateUploadUrl,\n getComposition,\n getStorageUrl,\n updateComposition,\n uploadToStorage,\n} from \"../../lib/local-convex.js\";\nimport { outputError, outputSuccess } from \"../../lib/output.js\";\n\nconst AUDIO_CACHE_DIR = \"/tmp/program-video-cache/audio\";\n\nexport interface TrimSilenceOptions extends OutputOptions {\n scene?: string;\n silenceDuration?: string;\n threshold?: string;\n dryRun?: boolean;\n}\n\ninterface AudioScene {\n id: string;\n type: string;\n start: number;\n end: number;\n duration: number;\n order: number;\n config: { audioUrl: string };\n metadata?: Record<string, unknown>;\n}\n\nfunction ensureFfmpeg(): void {\n try {\n execSync(\"ffmpeg -version\", { stdio: \"ignore\" });\n } catch {\n throw new Error(\"ffmpeg is not installed. Install it with: brew install ffmpeg\");\n }\n}\n\nfunction getAudioDuration(filePath: string): number {\n const output = execSync(\n `ffprobe -v quiet -show_entries format=duration -of csv=p=0 \"${filePath}\"`,\n { encoding: \"utf-8\" },\n ).trim();\n return parseFloat(output);\n}\n\nfunction countSilences(filePath: string, threshold: string, minDuration: number): number {\n try {\n const output = execSync(\n `ffmpeg -i \"${filePath}\" -af silencedetect=noise=${threshold}:d=${minDuration} -f null - 2>&1`,\n { encoding: \"utf-8\" },\n );\n return (output.match(/silence_start/g) ?? []).length;\n } catch {\n return 0;\n }\n}\n\nfunction trimSilence(\n inputPath: string,\n outputPath: string,\n silenceDuration: number,\n threshold: string,\n): void {\n execSync(\n `ffmpeg -y -i \"${inputPath}\" -af \"silenceremove=stop_periods=-1:stop_duration=${silenceDuration}:stop_threshold=${threshold}:stop_silence=${silenceDuration}\" \"${outputPath}\"`,\n { stdio: \"ignore\" },\n );\n}\n\nexport async function trimSilenceCommand(\n compositionId: string | undefined,\n options: TrimSilenceOptions,\n): Promise<void> {\n if (!compositionId) {\n outputError(\"Composition ID is required\", options);\n process.exit(1);\n }\n\n const silenceDuration = parseFloat(options.silenceDuration ?? \"0.25\");\n const threshold = options.threshold ?? \"-30dB\";\n\n const showSpinner = !options.json && process.stdout.isTTY;\n const spinner = showSpinner ? ora(\"Loading composition...\").start() : null;\n\n try {\n ensureFfmpeg();\n } catch (err) {\n spinner?.stop();\n outputError((err as Error).message, options);\n process.exit(1);\n }\n\n const composition = await getComposition(compositionId);\n if (!composition) {\n spinner?.stop();\n outputError(`Composition not found: ${compositionId}`, options);\n process.exit(1);\n }\n\n const scenes = (composition.scenes ?? []) as AudioScene[];\n const audioScenes = scenes.filter((s) => s.type === \"audio_scene\");\n\n if (audioScenes.length === 0) {\n spinner?.stop();\n outputError(\"No audio scenes found in composition\", options);\n process.exit(1);\n }\n\n // Filter to specific scene if --scene provided\n const targetScenes = options.scene\n ? audioScenes.filter((s) => s.id === options.scene)\n : audioScenes;\n\n if (targetScenes.length === 0) {\n spinner?.stop();\n outputError(`Audio scene not found: ${options.scene}`, options);\n process.exit(1);\n }\n\n mkdirSync(AUDIO_CACHE_DIR, { recursive: true });\n\n const results: {\n sceneId: string;\n originalDuration: number;\n trimmedDuration: number;\n savedSeconds: number;\n silenceCount: number;\n outputPath: string;\n storageUrl: string;\n storageId: string;\n }[] = [];\n\n // Calculate time offset: how much earlier do subsequent scenes start?\n let cumulativeTimeSaved = 0;\n\n // Sort all scenes by start time for timing adjustment\n const allScenes = [...scenes].sort((a, b) => a.start - b.start);\n\n for (const scene of targetScenes) {\n const audioUrl = scene.config.audioUrl;\n if (!audioUrl) {\n if (spinner) spinner.text = `Skipping ${scene.id} — no URL`;\n continue;\n }\n\n if (spinner) spinner.text = `Downloading ${scene.id}...`;\n const originalPath = path.join(AUDIO_CACHE_DIR, `${scene.id}-original.wav`);\n const trimmedPath = path.join(AUDIO_CACHE_DIR, `${scene.id}-trimmed.wav`);\n\n // Support local file paths (from previous trims) and HTTP URLs\n const isLocalFile = audioUrl.startsWith(\"/\") || audioUrl.startsWith(\"file://\");\n const isLocalCacheUrl = audioUrl.startsWith(\"/audio-cache/\");\n\n if (isLocalFile || isLocalCacheUrl) {\n // Resolve to actual filesystem path\n const localPath = isLocalCacheUrl\n ? path.join(AUDIO_CACHE_DIR, audioUrl.replace(\"/audio-cache/\", \"\"))\n : audioUrl.replace(\"file://\", \"\");\n if (!existsSync(localPath)) {\n if (spinner) spinner.text = `Skipping ${scene.id} — local file not found`;\n continue;\n }\n execSync(`cp \"${localPath}\" \"${originalPath}\"`);\n } else if (audioUrl.startsWith(\"http://\") || audioUrl.startsWith(\"https://\")) {\n // Use cached original if we already have one\n if (existsSync(originalPath) && statSync(originalPath).size > 1000) {\n if (spinner) spinner.text = `Using cached original for ${scene.id}`;\n } else {\n try {\n execSync(`curl -sL \"${audioUrl}\" -o \"${originalPath}\"`, { timeout: 30000 });\n } catch {\n if (spinner) spinner.text = `Failed to download ${scene.id}, skipping`;\n continue;\n }\n const fileSize = statSync(originalPath).size;\n if (fileSize < 1000) {\n if (spinner) spinner.text = `Download failed for ${scene.id} (URL may be expired), skipping`;\n continue;\n }\n }\n } else {\n // Check if we already have a cached original from a previous run\n if (existsSync(originalPath) && statSync(originalPath).size > 1000) {\n if (spinner) spinner.text = `Using cached original for ${scene.id}`;\n } else {\n if (spinner) spinner.text = `Skipping ${scene.id} — unrecognized URL scheme`;\n continue;\n }\n }\n\n const originalDuration = getAudioDuration(originalPath);\n const silenceCount = countSilences(originalPath, threshold, 0.3);\n\n if (spinner) spinner.text = `Trimming ${scene.id} (${silenceCount} gaps found)...`;\n trimSilence(originalPath, trimmedPath, silenceDuration, threshold);\n\n const trimmedDuration = getAudioDuration(trimmedPath);\n const savedSeconds = originalDuration - trimmedDuration;\n\n // Upload trimmed audio to Convex storage\n if (spinner) spinner.text = `Uploading trimmed ${scene.id} to storage...`;\n const fileData = readFileSync(trimmedPath);\n const uploadUrl = await generateUploadUrl();\n const storageId = await uploadToStorage(uploadUrl, fileData, \"audio/wav\");\n const storageUrl = await getStorageUrl(storageId);\n if (!storageUrl) {\n if (spinner) spinner.text = `Failed to get storage URL for ${scene.id}, skipping`;\n continue;\n }\n\n results.push({\n sceneId: scene.id,\n originalDuration,\n trimmedDuration,\n savedSeconds,\n silenceCount,\n outputPath: trimmedPath,\n storageUrl,\n storageId,\n });\n }\n\n if (results.length === 0) {\n spinner?.stop();\n outputError(\"No audio scenes could be processed\", options);\n process.exit(1);\n }\n\n if (options.dryRun) {\n spinner?.stop();\n if (options.json) {\n outputSuccess({ dryRun: true, results }, options);\n } else {\n console.log(\"\\nDry run results:\");\n for (const r of results) {\n console.log(` ${r.sceneId}: ${r.originalDuration.toFixed(1)}s → ${r.trimmedDuration.toFixed(1)}s (saved ${r.savedSeconds.toFixed(1)}s, ${r.silenceCount} gaps)`);\n }\n const totalSaved = results.reduce((sum, r) => sum + r.savedSeconds, 0);\n console.log(`\\n Total saved: ${totalSaved.toFixed(1)}s`);\n }\n return;\n }\n\n // Update composition: adjust audio URLs and scene timing\n if (spinner) spinner.text = \"Updating composition...\";\n\n const updatedScenes = allScenes.map((scene) => {\n const result = results.find((r) => r.sceneId === scene.id);\n if (result) {\n // This is a trimmed audio scene\n const newStart = scene.start - cumulativeTimeSaved;\n const newEnd = newStart + result.trimmedDuration;\n cumulativeTimeSaved += result.savedSeconds;\n return {\n ...scene,\n start: Math.max(0, newStart),\n end: newEnd,\n duration: result.trimmedDuration,\n config: {\n ...scene.config,\n audioUrl: result.storageUrl,\n storageId: result.storageId,\n originalAudioUrl: scene.config.audioUrl,\n },\n };\n }\n\n // Non-audio scenes or untrimmed: shift timing by cumulative savings\n if (cumulativeTimeSaved > 0 && scene.start > 0) {\n const newStart = Math.max(0, scene.start - cumulativeTimeSaved);\n const newEnd = scene.end - cumulativeTimeSaved;\n return {\n ...scene,\n start: newStart,\n end: newEnd,\n duration: newEnd - newStart,\n };\n }\n\n return scene;\n });\n\n const newTotalDuration = Math.max(...updatedScenes.map((s) => s.end));\n\n await updateComposition(compositionId, {\n scenes: updatedScenes as unknown[],\n duration: newTotalDuration,\n });\n\n spinner?.stop();\n\n if (options.json) {\n outputSuccess({\n compositionId,\n results,\n newDuration: newTotalDuration,\n }, options);\n } else {\n console.log(\"\\nSilence trimmed:\");\n for (const r of results) {\n console.log(` ${r.sceneId}: ${r.originalDuration.toFixed(1)}s → ${r.trimmedDuration.toFixed(1)}s (−${r.savedSeconds.toFixed(1)}s, ${r.silenceCount} gaps)`);\n }\n const totalSaved = results.reduce((sum, r) => sum + r.savedSeconds, 0);\n const oldDuration = Number(composition.duration ?? 0);\n console.log(`\\n Composition: ${oldDuration.toFixed(1)}s → ${newTotalDuration.toFixed(1)}s (saved ${totalSaved.toFixed(1)}s)`);\n console.log(` Silence retention: ${silenceDuration}s | Threshold: ${threshold}`);\n console.log(`\\n Files saved to: ${AUDIO_CACHE_DIR}/`);\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@program-video/cli",
3
- "version": "0.1.12",
4
- "description": "CLI for Program Video",
3
+ "version": "0.2.1",
4
+ "description": "CLI for the Program desktop app (local-only)",
5
5
  "private": false,
6
6
  "type": "module",
7
7
  "license": "Proprietary",
@@ -32,14 +32,23 @@
32
32
  "dev": "tsup --watch",
33
33
  "clean": "rm -rf dist .cache",
34
34
  "lint": "eslint --cache --cache-location .cache/.eslintcache",
35
+ "test": "vitest run",
36
+ "test:watch": "vitest",
35
37
  "typecheck": "tsc --noEmit"
36
38
  },
37
39
  "dependencies": {
40
+ "@ai-sdk/fal": "catalog:",
41
+ "@ai-sdk/gateway": "catalog:",
42
+ "@ai-sdk/replicate": "catalog:",
43
+ "@apidevtools/json-schema-ref-parser": "^11.7.2",
44
+ "@program-video/inference": "workspace:*",
45
+ "@program-video/steps": "workspace:*",
46
+ "ai": "catalog:",
38
47
  "chalk": "^5.4.1",
39
48
  "commander": "^13.1.0",
40
49
  "conf": "^13.1.0",
41
- "open": "^10.1.0",
42
- "ora": "^8.2.0"
50
+ "ora": "^8.2.0",
51
+ "replicate": "^1.0.4"
43
52
  },
44
53
  "devDependencies": {
45
54
  "@program-video/eslint-config": "workspace:*",
@@ -47,6 +56,7 @@
47
56
  "@types/node": "catalog:",
48
57
  "eslint": "catalog:",
49
58
  "tsup": "^8.0.0",
50
- "typescript": "catalog:"
59
+ "typescript": "catalog:",
60
+ "vitest": "4.0.17"
51
61
  }
52
62
  }