@studiomeyer/mcp-video 1.0.0 → 1.0.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.
- package/.github/FUNDING.yml +2 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +9 -0
- package/.github/dependabot.yml +46 -0
- package/.github/workflows/ci.yml +2 -2
- package/CHANGELOG.md +157 -0
- package/CODE_OF_CONDUCT.md +7 -0
- package/ECOSYSTEM.md +35 -0
- package/README.md +26 -2
- package/SECURITY.md +11 -0
- package/dist/handlers/smart-screenshot.js +10 -3
- package/dist/handlers/smart-screenshot.js.map +1 -1
- package/dist/handlers/tts.js +6 -2
- package/dist/handlers/tts.js.map +1 -1
- package/dist/handlers/video.js +17 -7
- package/dist/handlers/video.js.map +1 -1
- package/dist/lib/error-sanitizer.d.ts +26 -0
- package/dist/lib/error-sanitizer.js +58 -0
- package/dist/lib/error-sanitizer.js.map +1 -0
- package/dist/lib/error-sanitizer.test.d.ts +1 -0
- package/dist/lib/error-sanitizer.test.js +73 -0
- package/dist/lib/error-sanitizer.test.js.map +1 -0
- package/dist/lib/ffmpeg-bin.d.ts +64 -0
- package/dist/lib/ffmpeg-bin.js +96 -0
- package/dist/lib/ffmpeg-bin.js.map +1 -0
- package/dist/lib/ffmpeg-bin.test.d.ts +18 -0
- package/dist/lib/ffmpeg-bin.test.js +169 -0
- package/dist/lib/ffmpeg-bin.test.js.map +1 -0
- package/dist/lib/ffmpeg-run.d.ts +43 -0
- package/dist/lib/ffmpeg-run.js +67 -0
- package/dist/lib/ffmpeg-run.js.map +1 -0
- package/dist/lib/ffmpeg-run.test.d.ts +1 -0
- package/dist/lib/ffmpeg-run.test.js +66 -0
- package/dist/lib/ffmpeg-run.test.js.map +1 -0
- package/dist/lib/ffmpeg-safety.d.ts +37 -0
- package/dist/lib/ffmpeg-safety.js +67 -0
- package/dist/lib/ffmpeg-safety.js.map +1 -0
- package/dist/lib/ffmpeg-safety.test.d.ts +1 -0
- package/dist/lib/ffmpeg-safety.test.js +72 -0
- package/dist/lib/ffmpeg-safety.test.js.map +1 -0
- package/dist/lib/temp-dir.d.ts +24 -0
- package/dist/lib/temp-dir.js +53 -0
- package/dist/lib/temp-dir.js.map +1 -0
- package/dist/lib/temp-dir.test.d.ts +1 -0
- package/dist/lib/temp-dir.test.js +68 -0
- package/dist/lib/temp-dir.test.js.map +1 -0
- package/dist/lib/url-guard.d.ts +41 -0
- package/dist/lib/url-guard.js +134 -0
- package/dist/lib/url-guard.js.map +1 -0
- package/dist/lib/url-guard.test.d.ts +10 -0
- package/dist/lib/url-guard.test.js +231 -0
- package/dist/lib/url-guard.test.js.map +1 -0
- package/dist/server.js +9 -4
- package/dist/server.js.map +1 -1
- package/dist/tools/engine/audio-mixer.js +5 -20
- package/dist/tools/engine/audio-mixer.js.map +1 -1
- package/dist/tools/engine/audio.js +3 -19
- package/dist/tools/engine/audio.js.map +1 -1
- package/dist/tools/engine/beat-sync.js +7 -30
- package/dist/tools/engine/beat-sync.js.map +1 -1
- package/dist/tools/engine/capture.js +7 -0
- package/dist/tools/engine/capture.js.map +1 -1
- package/dist/tools/engine/chroma-key.js +2 -11
- package/dist/tools/engine/chroma-key.js.map +1 -1
- package/dist/tools/engine/concat.js +2 -11
- package/dist/tools/engine/concat.js.map +1 -1
- package/dist/tools/engine/editing.js +12 -35
- package/dist/tools/engine/editing.js.map +1 -1
- package/dist/tools/engine/encoder.js +2 -12
- package/dist/tools/engine/encoder.js.map +1 -1
- package/dist/tools/engine/lut-presets.js +2 -11
- package/dist/tools/engine/lut-presets.js.map +1 -1
- package/dist/tools/engine/narrated-video.js +30 -39
- package/dist/tools/engine/narrated-video.js.map +1 -1
- package/dist/tools/engine/smart-screenshot.js +7 -0
- package/dist/tools/engine/smart-screenshot.js.map +1 -1
- package/dist/tools/engine/social-format.js +2 -11
- package/dist/tools/engine/social-format.js.map +1 -1
- package/dist/tools/engine/template-renderer.js +2 -11
- package/dist/tools/engine/template-renderer.js.map +1 -1
- package/dist/tools/engine/text-animations.js +2 -11
- package/dist/tools/engine/text-animations.js.map +1 -1
- package/dist/tools/engine/text-overlay.js +2 -11
- package/dist/tools/engine/text-overlay.js.map +1 -1
- package/dist/tools/engine/tts.js +11 -6
- package/dist/tools/engine/tts.js.map +1 -1
- package/dist/tools/engine/voice-effects.js +3 -20
- package/dist/tools/engine/voice-effects.js.map +1 -1
- package/package.json +6 -6
- package/src/handlers/smart-screenshot.ts +8 -3
- package/src/handlers/tts.ts +6 -2
- package/src/handlers/video.ts +14 -7
- package/src/lib/error-sanitizer.test.ts +88 -0
- package/src/lib/error-sanitizer.ts +66 -0
- package/src/lib/ffmpeg-bin.test.ts +192 -0
- package/src/lib/ffmpeg-bin.ts +111 -0
- package/src/lib/ffmpeg-run.test.ts +76 -0
- package/src/lib/ffmpeg-run.ts +110 -0
- package/src/lib/ffmpeg-safety.test.ts +88 -0
- package/src/lib/ffmpeg-safety.ts +79 -0
- package/src/lib/temp-dir.test.ts +75 -0
- package/src/lib/temp-dir.ts +58 -0
- package/src/lib/url-guard.test.ts +261 -0
- package/src/lib/url-guard.ts +143 -0
- package/src/server.ts +10 -5
- package/src/tools/engine/audio-mixer.ts +8 -21
- package/src/tools/engine/audio.ts +6 -21
- package/src/tools/engine/beat-sync.ts +10 -31
- package/src/tools/engine/capture.ts +8 -0
- package/src/tools/engine/chroma-key.ts +2 -11
- package/src/tools/engine/concat.ts +2 -11
- package/src/tools/engine/editing.ts +17 -34
- package/src/tools/engine/encoder.ts +2 -12
- package/src/tools/engine/lut-presets.ts +2 -11
- package/src/tools/engine/narrated-video.ts +26 -38
- package/src/tools/engine/smart-screenshot.ts +8 -0
- package/src/tools/engine/social-format.ts +2 -11
- package/src/tools/engine/template-renderer.ts +2 -11
- package/src/tools/engine/text-animations.ts +2 -11
- package/src/tools/engine/text-overlay.ts +2 -11
- package/src/tools/engine/tts.ts +15 -6
- package/src/tools/engine/voice-effects.ts +3 -17
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
* 3. Merge video + audio into final output
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { execFile } from 'child_process';
|
|
13
12
|
import * as fs from 'fs';
|
|
14
13
|
import * as path from 'path';
|
|
15
14
|
import { logger } from '../../lib/logger.js';
|
|
15
|
+
import { withTempDir } from '../../lib/temp-dir.js';
|
|
16
|
+
import { validateFfmpegPath, type FfmpegProtocolSet } from '../../lib/ffmpeg-safety.js';
|
|
17
|
+
import { runFfmpeg as runFfmpegSafe } from '../../lib/ffmpeg-run.js';
|
|
16
18
|
import { generateSpeech } from './tts.js';
|
|
17
19
|
import type { TTSProvider, ElevenLabsVoice, ElevenLabsModel, OpenAIVoice, OpenAIModel } from './tts.js';
|
|
18
20
|
import { recordWebsite } from './capture.js';
|
|
@@ -91,10 +93,7 @@ export async function createNarratedVideo(
|
|
|
91
93
|
viewport = 'desktop',
|
|
92
94
|
} = config;
|
|
93
95
|
|
|
94
|
-
|
|
95
|
-
fs.mkdirSync(tempDir, { recursive: true });
|
|
96
|
-
|
|
97
|
-
try {
|
|
96
|
+
return withTempDir('narrated-video', async (tempDir) => {
|
|
98
97
|
// ─── Step 1: Generate all speech segments ───────────────────
|
|
99
98
|
logger.info(`Generating ${segments.length} speech segment(s)...`);
|
|
100
99
|
const audioPaths: string[] = [];
|
|
@@ -197,14 +196,11 @@ export async function createNarratedVideo(
|
|
|
197
196
|
finalPath,
|
|
198
197
|
);
|
|
199
198
|
|
|
200
|
-
await runFfmpeg(mergeArgs);
|
|
199
|
+
await runFfmpeg(mergeArgs, config.backgroundMusicPath ? 'https-input' : 'local-only');
|
|
201
200
|
|
|
202
201
|
const finalStats = fs.statSync(finalPath);
|
|
203
202
|
const finalDuration = await getMediaDuration(finalPath);
|
|
204
203
|
|
|
205
|
-
// ─── Cleanup ────────────────────────────────────────────────
|
|
206
|
-
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
207
|
-
|
|
208
204
|
logger.info(`Narrated video ready: ${finalPath} (${finalDuration.toFixed(1)}s, ${(finalStats.size / 1024 / 1024).toFixed(2)} MB)`);
|
|
209
205
|
|
|
210
206
|
return {
|
|
@@ -221,11 +217,7 @@ export async function createNarratedVideo(
|
|
|
221
217
|
},
|
|
222
218
|
url,
|
|
223
219
|
};
|
|
224
|
-
}
|
|
225
|
-
// Cleanup on error
|
|
226
|
-
try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
227
|
-
throw error;
|
|
228
|
-
}
|
|
220
|
+
});
|
|
229
221
|
}
|
|
230
222
|
|
|
231
223
|
// ─── Helpers ────────────────────────────────────────────────────────
|
|
@@ -234,34 +226,30 @@ export async function createNarratedVideo(
|
|
|
234
226
|
* Concatenate multiple audio files using ffmpeg concat demuxer
|
|
235
227
|
*/
|
|
236
228
|
async function concatenateAudio(files: string[], outputPath: string): Promise<void> {
|
|
229
|
+
for (const f of files) validateFfmpegPath(f, 'concat-input');
|
|
230
|
+
validateFfmpegPath(outputPath, 'concat-output');
|
|
231
|
+
|
|
237
232
|
// Create concat list file
|
|
238
233
|
const listPath = outputPath + '.txt';
|
|
239
|
-
const listContent = files.map((f) => `file '${f}'`).join('\n');
|
|
234
|
+
const listContent = files.map((f) => `file '${f.replace(/'/g, "'\\''")}'`).join('\n');
|
|
240
235
|
fs.writeFileSync(listPath, listContent);
|
|
241
236
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
237
|
+
try {
|
|
238
|
+
await runFfmpeg([
|
|
239
|
+
'-y',
|
|
240
|
+
'-f', 'concat',
|
|
241
|
+
'-safe', '0',
|
|
242
|
+
'-i', listPath,
|
|
243
|
+
'-c:a', 'libmp3lame',
|
|
244
|
+
'-b:a', '192k',
|
|
245
|
+
outputPath,
|
|
246
|
+
]);
|
|
247
|
+
} finally {
|
|
248
|
+
// Cleanup list file
|
|
249
|
+
try { fs.unlinkSync(listPath); } catch { /* already gone */ }
|
|
250
|
+
}
|
|
254
251
|
}
|
|
255
252
|
|
|
256
|
-
function runFfmpeg(args: string[]): Promise<string> {
|
|
257
|
-
return
|
|
258
|
-
execFile('ffmpeg', args, { maxBuffer: 50 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
259
|
-
if (error) {
|
|
260
|
-
logger.error(`ffmpeg failed: ${stderr}`);
|
|
261
|
-
reject(new Error(`ffmpeg failed: ${stderr || error.message}`));
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
resolve(stdout);
|
|
265
|
-
});
|
|
266
|
-
});
|
|
253
|
+
function runFfmpeg(args: string[], protocols: FfmpegProtocolSet = 'local-only'): Promise<string> {
|
|
254
|
+
return runFfmpegSafe(args, { maxBuffer: 50 * 1024 * 1024, protocols, label: 'narrated-video' });
|
|
267
255
|
}
|
|
@@ -16,6 +16,7 @@ import type { Page, Browser, BrowserContext, ElementHandle } from 'playwright';
|
|
|
16
16
|
import * as fs from 'fs';
|
|
17
17
|
import * as path from 'path';
|
|
18
18
|
import { logger } from '../../lib/logger.js';
|
|
19
|
+
import { guardFinalUrl } from '../../lib/url-guard.js';
|
|
19
20
|
|
|
20
21
|
const OUTPUT_DIR = process.env.VIDEO_OUTPUT_DIR || './output';
|
|
21
22
|
|
|
@@ -425,6 +426,13 @@ export async function smartScreenshot(config: SmartScreenshotConfig): Promise<Sm
|
|
|
425
426
|
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
|
|
426
427
|
});
|
|
427
428
|
|
|
429
|
+
// Post-redirect guard: follow-the-redirect must not land on a private IP.
|
|
430
|
+
const finalUrl = page.url();
|
|
431
|
+
const finalGuard = guardFinalUrl(finalUrl);
|
|
432
|
+
if (!finalGuard.ok) {
|
|
433
|
+
throw new Error(`post-redirect check failed — final URL rejected: ${finalGuard.reason}`);
|
|
434
|
+
}
|
|
435
|
+
|
|
428
436
|
await page.waitForTimeout(waitAfterLoad);
|
|
429
437
|
|
|
430
438
|
// ─── 3. Dismiss overlays ────────────────────────────────────
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Social media format converter — crop, scale, pad for every platform
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { execFile } from 'child_process';
|
|
6
5
|
import * as fs from 'fs';
|
|
7
6
|
import * as path from 'path';
|
|
8
7
|
import { logger } from '../../lib/logger.js';
|
|
8
|
+
import { runFfmpeg as runFfmpegSafe } from '../../lib/ffmpeg-run.js';
|
|
9
9
|
|
|
10
10
|
// ─── Format Definitions ─────────────────────────────────────────────
|
|
11
11
|
|
|
@@ -133,14 +133,5 @@ export async function convertToAllFormats(
|
|
|
133
133
|
// ─── Helper ─────────────────────────────────────────────────────────
|
|
134
134
|
|
|
135
135
|
function runFfmpeg(args: string[]): Promise<string> {
|
|
136
|
-
return
|
|
137
|
-
execFile('ffmpeg', args, { maxBuffer: 50 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
138
|
-
if (error) {
|
|
139
|
-
logger.error(`ffmpeg failed: ${stderr}`);
|
|
140
|
-
reject(new Error(`ffmpeg failed: ${stderr || error.message}`));
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
resolve(stdout);
|
|
144
|
-
});
|
|
145
|
-
});
|
|
136
|
+
return runFfmpegSafe(args, { maxBuffer: 50 * 1024 * 1024, label: 'social-format' });
|
|
146
137
|
}
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
* concat, audio, social-format.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { execFile } from 'child_process';
|
|
12
11
|
import * as fs from 'fs';
|
|
13
12
|
import * as path from 'path';
|
|
14
13
|
import { logger } from '../../lib/logger.js';
|
|
14
|
+
import { runFfmpeg as runFfmpegSafe } from '../../lib/ffmpeg-run.js';
|
|
15
15
|
import { getTemplate } from './templates.js';
|
|
16
16
|
import type { VideoTemplate, TemplateSlot } from './templates.js';
|
|
17
17
|
|
|
@@ -55,16 +55,7 @@ export interface RenderResult {
|
|
|
55
55
|
// ─── Helpers ────────────────────────────────────────────────────────
|
|
56
56
|
|
|
57
57
|
function runFfmpeg(args: string[], timeoutMs = 600_000): Promise<string> {
|
|
58
|
-
return
|
|
59
|
-
execFile('ffmpeg', args, { maxBuffer: 100 * 1024 * 1024, timeout: timeoutMs }, (error, stdout, stderr) => {
|
|
60
|
-
if (error) {
|
|
61
|
-
logger.error(`ffmpeg failed: ${stderr}`);
|
|
62
|
-
reject(new Error(`ffmpeg failed: ${stderr || error.message}`));
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
resolve(stdout);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
58
|
+
return runFfmpegSafe(args, { maxBuffer: 100 * 1024 * 1024, timeoutMs, label: 'template-renderer' });
|
|
68
59
|
}
|
|
69
60
|
|
|
70
61
|
function ensureDir(filePath: string): void {
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* manipulation to create effects like typewriter, pop, slide, bounce, etc.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { execFile } from 'child_process';
|
|
9
8
|
import * as fs from 'fs';
|
|
10
9
|
import * as path from 'path';
|
|
11
10
|
import { logger } from '../../lib/logger.js';
|
|
11
|
+
import { runFfmpeg as runFfmpegSafe } from '../../lib/ffmpeg-run.js';
|
|
12
12
|
|
|
13
13
|
// ─── Types ──────────────────────────────────────────────────────────
|
|
14
14
|
|
|
@@ -81,16 +81,7 @@ export const TEXT_ANIMATION_DESCRIPTIONS: Record<TextAnimation, string> = {
|
|
|
81
81
|
// ─── Helpers ────────────────────────────────────────────────────────
|
|
82
82
|
|
|
83
83
|
function runFfmpeg(args: string[], timeoutMs = 300_000): Promise<string> {
|
|
84
|
-
return
|
|
85
|
-
execFile('ffmpeg', args, { maxBuffer: 100 * 1024 * 1024, timeout: timeoutMs }, (error, stdout, stderr) => {
|
|
86
|
-
if (error) {
|
|
87
|
-
logger.error(`ffmpeg failed: ${stderr}`);
|
|
88
|
-
reject(new Error(`ffmpeg failed: ${stderr || error.message}`));
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
resolve(stdout);
|
|
92
|
-
});
|
|
93
|
-
});
|
|
84
|
+
return runFfmpegSafe(args, { maxBuffer: 100 * 1024 * 1024, timeoutMs, label: 'text-animations' });
|
|
94
85
|
}
|
|
95
86
|
|
|
96
87
|
function ensureDir(filePath: string): void {
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Text overlay engine — animated titles, subtitles, watermarks
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { execFile } from 'child_process';
|
|
6
5
|
import * as fs from 'fs';
|
|
7
6
|
import { logger } from '../../lib/logger.js';
|
|
7
|
+
import { runFfmpeg as runFfmpegSafe } from '../../lib/ffmpeg-run.js';
|
|
8
8
|
|
|
9
9
|
// ─── Types ──────────────────────────────────────────────────────────
|
|
10
10
|
|
|
@@ -130,14 +130,5 @@ function findFont(): string {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
function runFfmpeg(args: string[]): Promise<string> {
|
|
133
|
-
return
|
|
134
|
-
execFile('ffmpeg', args, { maxBuffer: 50 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
135
|
-
if (error) {
|
|
136
|
-
logger.error(`ffmpeg failed: ${stderr}`);
|
|
137
|
-
reject(new Error(`ffmpeg failed: ${stderr || error.message}`));
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
resolve(stdout);
|
|
141
|
-
});
|
|
142
|
-
});
|
|
133
|
+
return runFfmpegSafe(args, { maxBuffer: 50 * 1024 * 1024, label: 'text-overlay' });
|
|
143
134
|
}
|
package/src/tools/engine/tts.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import * as fs from 'fs';
|
|
11
11
|
import * as path from 'path';
|
|
12
12
|
import { logger } from '../../lib/logger.js';
|
|
13
|
+
import { sanitizeErrorMessage } from '../../lib/error-sanitizer.js';
|
|
13
14
|
import { getMediaDuration } from './audio.js';
|
|
14
15
|
|
|
15
16
|
// ─── Types ──────────────────────────────────────────────────────────
|
|
@@ -121,7 +122,7 @@ export async function generateSpeech(config: TTSConfig): Promise<TTSResult> {
|
|
|
121
122
|
} catch (error) {
|
|
122
123
|
// Fallback: try the other provider
|
|
123
124
|
const fallback = provider === 'elevenlabs' ? 'openai' : 'elevenlabs';
|
|
124
|
-
const msg =
|
|
125
|
+
const msg = sanitizeErrorMessage(error);
|
|
125
126
|
logger.warn(`${provider} TTS failed (${msg}), falling back to ${fallback}`);
|
|
126
127
|
|
|
127
128
|
try {
|
|
@@ -131,7 +132,7 @@ export async function generateSpeech(config: TTSConfig): Promise<TTSResult> {
|
|
|
131
132
|
audioPath = await openaiTTS(config, finalPath);
|
|
132
133
|
}
|
|
133
134
|
} catch (fallbackError) {
|
|
134
|
-
const fbMsg =
|
|
135
|
+
const fbMsg = sanitizeErrorMessage(fallbackError);
|
|
135
136
|
throw new Error(`Both TTS providers failed. ${provider}: ${msg}, ${fallback}: ${fbMsg}`);
|
|
136
137
|
}
|
|
137
138
|
}
|
|
@@ -197,8 +198,12 @@ async function elevenLabsTTS(config: TTSConfig, outputPath: string): Promise<str
|
|
|
197
198
|
});
|
|
198
199
|
|
|
199
200
|
if (!response.ok) {
|
|
200
|
-
const errorText = await response.text();
|
|
201
|
-
throw new Error(
|
|
201
|
+
const errorText = await response.text().catch(() => '');
|
|
202
|
+
throw new Error(
|
|
203
|
+
sanitizeErrorMessage(errorText, {
|
|
204
|
+
prefix: `ElevenLabs API ${response.status}: `,
|
|
205
|
+
})
|
|
206
|
+
);
|
|
202
207
|
}
|
|
203
208
|
|
|
204
209
|
const arrayBuffer = await response.arrayBuffer();
|
|
@@ -236,8 +241,12 @@ async function openaiTTS(config: TTSConfig, outputPath: string): Promise<string>
|
|
|
236
241
|
});
|
|
237
242
|
|
|
238
243
|
if (!response.ok) {
|
|
239
|
-
const errorText = await response.text();
|
|
240
|
-
throw new Error(
|
|
244
|
+
const errorText = await response.text().catch(() => '');
|
|
245
|
+
throw new Error(
|
|
246
|
+
sanitizeErrorMessage(errorText, {
|
|
247
|
+
prefix: `OpenAI TTS API ${response.status}: `,
|
|
248
|
+
})
|
|
249
|
+
);
|
|
241
250
|
}
|
|
242
251
|
|
|
243
252
|
const arrayBuffer = await response.arrayBuffer();
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* Works on both audio files and video files (preserves video stream).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { execFile } from 'child_process';
|
|
9
8
|
import * as fs from 'fs';
|
|
10
9
|
import * as path from 'path';
|
|
11
10
|
import { logger } from '../../lib/logger.js';
|
|
11
|
+
import { runFfmpeg as runFfmpegSafe, runFfprobe as runFfprobeSafe } from '../../lib/ffmpeg-run.js';
|
|
12
12
|
|
|
13
13
|
// ─── Types ──────────────────────────────────────────────────────────
|
|
14
14
|
|
|
@@ -141,25 +141,11 @@ function getEffectFilter(effect: VoiceEffect, intensity: number): { filter: stri
|
|
|
141
141
|
// ─── Helpers ────────────────────────────────────────────────────────
|
|
142
142
|
|
|
143
143
|
function runFfmpeg(args: string[], timeoutMs = 300_000): Promise<string> {
|
|
144
|
-
return
|
|
145
|
-
execFile('ffmpeg', args, { maxBuffer: 100 * 1024 * 1024, timeout: timeoutMs }, (error, stdout, stderr) => {
|
|
146
|
-
if (error) {
|
|
147
|
-
logger.error(`ffmpeg failed: ${stderr}`);
|
|
148
|
-
reject(new Error(`ffmpeg failed: ${stderr || error.message}`));
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
resolve(stdout);
|
|
152
|
-
});
|
|
153
|
-
});
|
|
144
|
+
return runFfmpegSafe(args, { maxBuffer: 100 * 1024 * 1024, timeoutMs, label: 'voice-effects' });
|
|
154
145
|
}
|
|
155
146
|
|
|
156
147
|
function runFfprobe(args: string[]): Promise<string> {
|
|
157
|
-
return
|
|
158
|
-
execFile('ffprobe', args, { maxBuffer: 10 * 1024 * 1024 }, (error, stdout) => {
|
|
159
|
-
if (error) { reject(new Error(`ffprobe failed: ${error.message}`)); return; }
|
|
160
|
-
resolve(stdout.trim());
|
|
161
|
-
});
|
|
162
|
-
});
|
|
148
|
+
return runFfprobeSafe(args, { maxBuffer: 10 * 1024 * 1024, label: 'voice-effects-probe' }).then((s) => s.trim());
|
|
163
149
|
}
|
|
164
150
|
|
|
165
151
|
function ensureDir(filePath: string): void {
|