@amg555/frameforge-mcp 2.3.7 → 2.3.8

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/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ All notable changes to FrameForge are documented here.
4
4
 
5
5
  ---
6
6
 
7
+ ## [2.3.8] — 2026-11-09
8
+
9
+ ### Fixed & Enhanced
10
+ - **Multilingual Neural TTS Voice Routing (`src/pipeline/providers.js`)**:
11
+ - Automatically routes multilingual text to native neural voices: Malayalam (`ml-IN-MidhunNeural` / `ml-IN-SobhanaNeural`), Hindi (`hi-IN-MadhurNeural` / `hi-IN-SwaraNeural`), Tamil, Telugu, Chinese, Japanese, and Arabic.
12
+ - Eliminated Windows `shell: true` path space splitting and DEP0190 warnings in `_edgeTTS` by writing text to a UTF-8 temporary file and passing `--file` with `shell: false`.
13
+ - **Harness CLI Positional Arguments (`src/harness/bin/harness.mjs`)**:
14
+ - Added support for positional `.html` scene files (e.g. `node harness.mjs verify my-scene.html`).
15
+ - Added automatic fallback to `reference-scene.html` fixture so `npm run harness:verify` runs cleanly without requiring a pre-existing root `index.html`.
16
+
17
+ ---
18
+
7
19
  ## [2.3.7] — 2026-11-09
8
20
 
9
21
  ### Added
package/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  [![npm](https://img.shields.io/npm/v/@amg555/frameforge-mcp.svg?label=npm)](https://www.npmjs.com/package/@amg555/frameforge-mcp)
10
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
11
11
  [![Node.js](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg)](https://nodejs.org/)
12
- [![Version](https://img.shields.io/badge/version-2.3.7-blue.svg)](CHANGELOG.md)
12
+ [![Version](https://img.shields.io/badge/version-2.3.8-blue.svg)](CHANGELOG.md)
13
13
  [![MCP Spec](https://img.shields.io/badge/MCP-2024--11--05-blue.svg)](https://modelcontextprotocol.io/)
14
14
  [![Tests](https://img.shields.io/badge/tests-539%20passing-brightgreen.svg)](#testing)
15
15
 
@@ -0,0 +1,79 @@
1
+ {
2
+ "passed": true,
3
+ "timesTested": 9,
4
+ "results": [
5
+ {
6
+ "time": 0.1,
7
+ "a": "8d21a5d5a07ed0d7",
8
+ "b": "8d21a5d5a07ed0d7",
9
+ "c": "8d21a5d5a07ed0d7",
10
+ "ok": true,
11
+ "failure": null
12
+ },
13
+ {
14
+ "time": 0.3,
15
+ "a": "8d21a5d5a07ed0d7",
16
+ "b": "8d21a5d5a07ed0d7",
17
+ "c": "8d21a5d5a07ed0d7",
18
+ "ok": true,
19
+ "failure": null
20
+ },
21
+ {
22
+ "time": 0.56,
23
+ "a": "8d21a5d5a07ed0d7",
24
+ "b": "8d21a5d5a07ed0d7",
25
+ "c": "8d21a5d5a07ed0d7",
26
+ "ok": true,
27
+ "failure": null
28
+ },
29
+ {
30
+ "time": 0.8,
31
+ "a": "8d21a5d5a07ed0d7",
32
+ "b": "8d21a5d5a07ed0d7",
33
+ "c": "8d21a5d5a07ed0d7",
34
+ "ok": true,
35
+ "failure": null
36
+ },
37
+ {
38
+ "time": 1,
39
+ "a": "8d21a5d5a07ed0d7",
40
+ "b": "8d21a5d5a07ed0d7",
41
+ "c": "8d21a5d5a07ed0d7",
42
+ "ok": true,
43
+ "failure": null
44
+ },
45
+ {
46
+ "time": 1.24,
47
+ "a": "8d21a5d5a07ed0d7",
48
+ "b": "8d21a5d5a07ed0d7",
49
+ "c": "8d21a5d5a07ed0d7",
50
+ "ok": true,
51
+ "failure": null
52
+ },
53
+ {
54
+ "time": 1.5,
55
+ "a": "8d21a5d5a07ed0d7",
56
+ "b": "8d21a5d5a07ed0d7",
57
+ "c": "8d21a5d5a07ed0d7",
58
+ "ok": true,
59
+ "failure": null
60
+ },
61
+ {
62
+ "time": 1.76,
63
+ "a": "8d21a5d5a07ed0d7",
64
+ "b": "8d21a5d5a07ed0d7",
65
+ "c": "8d21a5d5a07ed0d7",
66
+ "ok": true,
67
+ "failure": null
68
+ },
69
+ {
70
+ "time": 1.9,
71
+ "a": "8d21a5d5a07ed0d7",
72
+ "b": "8d21a5d5a07ed0d7",
73
+ "c": "8d21a5d5a07ed0d7",
74
+ "ok": true,
75
+ "failure": null
76
+ }
77
+ ],
78
+ "framesSha256": "82dff4b5de7e0d9d"
79
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amg555/frameforge-mcp",
3
- "version": "2.3.7",
3
+ "version": "2.3.8",
4
4
  "description": "FrameForge — AI video production engine for agentic IDEs. npx @amg555/frameforge-mcp. FFmpeg + TTS run on your machine. 19 MCP tools.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -20,7 +20,7 @@ check('batch handling', code.includes('Array.isArray(body)'));
20
20
  check('ping handler', code.includes("method === 'ping'"));
21
21
  check('notifications/init', code.includes("notifications/initialized"));
22
22
  check('body size limit', code.includes('MAX_BODY_BYTES'));
23
- check('SERVER_INFO version 2.3.7', code.includes("version: '2.3.7'"));
23
+ check('SERVER_INFO version 2.3.8', code.includes("version: '2.3.8'"));
24
24
  check('18 tools defined (incl get_job_status)', code.includes('get_job_status'));
25
25
  check('export_to_editor tool defined', code.includes('export_to_editor'));
26
26
  check('edit_video tool defined', code.includes('edit_video'));
@@ -295,7 +295,7 @@ check('cf-worker has checkRateLimit', cfWorker.includes('checkRateLimit'));
295
295
  check('cf-worker hashes token (SHA-256)', cfWorker.includes('SHA-256'));
296
296
  check('cf-worker uses RATE_LIMITS KV', cfWorker.includes('env.RATE_LIMITS'));
297
297
  check('cf-worker returns 429 on limit', cfWorker.includes('429'));
298
- check('cf-worker version 2.3.7', cfWorker.includes("'2.3.7'"));
298
+ check('cf-worker version 2.3.8', cfWorker.includes("'2.3.8'"));
299
299
  check('wrangler.toml has kv_namespaces',fs.readFileSync('./cloudflare-mcp/wrangler.toml','utf8').includes('kv_namespaces'));
300
300
 
301
301
  // ── plugins/skills present ────────────────────────────────────────────────
@@ -64,7 +64,9 @@ function resolveProjectFile(filename, fallback) {
64
64
  return inCwd;
65
65
  }
66
66
 
67
- const HTML_FILE = flags.html ? path.resolve(CWD, flags.html) : resolveProjectFile('index.html', resolveProjectFile('studio.html'));
67
+ const positionalHtml = extras.find(e => typeof e === 'string' && e.endsWith('.html'));
68
+ const fixtureFallback = path.resolve(__dirname, '../test/fixtures/reference-scene.html');
69
+ const HTML_FILE = flags.html ? path.resolve(CWD, flags.html) : (positionalHtml ? path.resolve(CWD, positionalHtml) : resolveProjectFile('index.html', resolveProjectFile('studio.html', fixtureFallback)));
68
70
  const OUT_DIR = flags.outDir ? path.resolve(CWD, flags.outDir) : path.join(CWD, 'out');
69
71
  const CHECK_DIR = path.join(OUT_DIR, 'check');
70
72
  const FRAMES_DIR = flags.frames ? path.resolve(CWD, flags.frames) : flags.out ? path.resolve(CWD, flags.out) : path.join(OUT_DIR, 'frames');
@@ -1524,7 +1524,7 @@ Features:
1524
1524
 
1525
1525
  const SERVER_INFO = {
1526
1526
  name: 'video-pipeline',
1527
- version: '2.3.7',
1527
+ version: '2.3.8',
1528
1528
  };
1529
1529
 
1530
1530
  const CAPABILITIES = {
@@ -368,10 +368,38 @@ class TTSProvider {
368
368
  * picks by gender preference (TTS_VOICE_GENDER=male|female|default).
369
369
  *
370
370
  * @param {string} [explicitVoice] voice passed by caller (takes priority)
371
+ * @param {string} [text] narration text for language detection
371
372
  * @returns {string}
372
373
  */
373
- selectNarratorVoice(explicitVoice) {
374
+ selectNarratorVoice(explicitVoice, text = '') {
374
375
  if (explicitVoice) return explicitVoice;
376
+
377
+ // Language detection for edge-tts
378
+ if (this.name === 'edge-tts' && text) {
379
+ const gender = (process.env.TTS_VOICE_GENDER || 'default').toLowerCase();
380
+ if (/[\u0D00-\u0D7F]/.test(text)) {
381
+ return gender === 'female' ? 'ml-IN-SobhanaNeural' : 'ml-IN-MidhunNeural';
382
+ }
383
+ if (/[\u0900-\u097F]/.test(text)) {
384
+ return gender === 'female' ? 'hi-IN-SwaraNeural' : 'hi-IN-MadhurNeural';
385
+ }
386
+ if (/[\u0B80-\u0BFF]/.test(text)) {
387
+ return gender === 'female' ? 'ta-IN-PallaviNeural' : 'ta-IN-ValluvarNeural';
388
+ }
389
+ if (/[\u0C00-\u0C7F]/.test(text)) {
390
+ return gender === 'female' ? 'te-IN-ShrutiNeural' : 'te-IN-MohanNeural';
391
+ }
392
+ if (/[\u4E00-\u9FFF]/.test(text)) {
393
+ return gender === 'female' ? 'zh-CN-XiaoxiaoNeural' : 'zh-CN-YunjianNeural';
394
+ }
395
+ if (/[\u3040-\u30FF]/.test(text)) {
396
+ return gender === 'female' ? 'ja-JP-NanamiNeural' : 'ja-JP-KeitaNeural';
397
+ }
398
+ if (/[\u0600-\u06FF]/.test(text)) {
399
+ return gender === 'female' ? 'ar-SA-ZariyahNeural' : 'ar-SA-HamedNeural';
400
+ }
401
+ }
402
+
375
403
  const voices = NARRATOR_VOICES[this.name] || NARRATOR_VOICES['edge-tts'];
376
404
  const gender = (process.env.TTS_VOICE_GENDER || 'default').toLowerCase();
377
405
  return voices[gender] || voices.default;
@@ -384,7 +412,7 @@ class TTSProvider {
384
412
  * @returns {Promise<string>} resolves with outputPath
385
413
  */
386
414
  async synthesize({ text, voice, outputPath }) {
387
- const resolvedVoice = this.selectNarratorVoice(voice);
415
+ const resolvedVoice = this.selectNarratorVoice(voice, text);
388
416
  switch (this.name) {
389
417
  case 'edge-tts': return this._edgeTTS({ text, voice: resolvedVoice, outputPath });
390
418
  case 'elevenlabs': return this._elevenLabs({ text, voice: resolvedVoice, outputPath });
@@ -464,27 +492,36 @@ class TTSProvider {
464
492
 
465
493
  async _edgeTTS({ text, voice, outputPath }) {
466
494
  const rawPath = outputPath.replace(/\.[^.]+$/, '_raw.mp3');
467
- const safeVoice = voice || NARRATOR_VOICES['edge-tts'].default;
495
+ const txtPath = rawPath + '.txt';
496
+ const safeVoice = voice || this.selectNarratorVoice(null, text);
468
497
  // No artificial rate manipulation — use the voice's natural cadence.
469
498
  // Only a mild rate adjustment from env (e.g. TTS_RATE='+0%') is supported.
470
499
  const rate = this._overrides.rate || process.env.TTS_RATE || '+0%';
471
500
  return withRetry(async () => {
472
- const _shell = process.platform === 'win32';
473
- await new Promise((resolve, reject) => {
474
- const proc = spawn('edge-tts', ['--voice', safeVoice, '--rate', rate, '--text', text, '--write-media', rawPath], { timeout: 40000, shell: _shell });
475
- proc.on('close', code => code === 0 ? resolve() : reject(new Error(`edge-tts exited ${code}`)));
476
- proc.on('error', reject);
477
- });
478
- // Normalise audio via FFmpeg — trim trailing silence, normalise levels
479
- await new Promise((resolve, reject) => {
480
- const proc = spawn('ffmpeg', ['-nostdin', '-y', '-threads', '2',
481
- '-i', rawPath,
482
- '-af', 'silenceremove=stop_periods=-1:stop_duration=0.3:stop_threshold=-40dB,highpass=f=80,volume=1.0,dynaudnorm=f=150:g=5:p=0.95',
483
- '-ar', '48000', '-ac', '2', '-c:a', 'aac', '-b:a', '192k', outputPath], { timeout: 30000, shell: _shell });
484
- proc.on('close', code => code === 0 ? resolve() : reject(new Error(`ffmpeg exited ${code}`)));
485
- proc.on('error', reject);
486
- });
487
- await fs.unlink(rawPath).catch(() => {});
501
+ await fs.writeFile(txtPath, text, 'utf8');
502
+ try {
503
+ await new Promise((resolve, reject) => {
504
+ const proc = spawn('edge-tts', ['--voice', safeVoice, '--rate', rate, '--file', txtPath, '--write-media', rawPath], { timeout: 40000, shell: false });
505
+ let stderr = '';
506
+ proc.stderr?.on('data', d => { stderr += d; });
507
+ proc.on('close', code => code === 0 ? resolve() : reject(new Error(`edge-tts exited ${code}: ${stderr}`)));
508
+ proc.on('error', reject);
509
+ });
510
+ // Normalise audio via FFmpeg — trim trailing silence, normalise levels
511
+ await new Promise((resolve, reject) => {
512
+ const proc = spawn('ffmpeg', ['-nostdin', '-y', '-threads', '2',
513
+ '-i', rawPath,
514
+ '-af', 'silenceremove=stop_periods=-1:stop_duration=0.3:stop_threshold=-40dB,highpass=f=80,volume=1.0,dynaudnorm=f=150:g=5:p=0.95',
515
+ '-ar', '48000', '-ac', '2', '-c:a', 'aac', '-b:a', '192k', outputPath], { timeout: 30000 });
516
+ let stderr = '';
517
+ proc.stderr?.on('data', d => { stderr += d; });
518
+ proc.on('close', code => code === 0 ? resolve() : reject(new Error(`ffmpeg exited ${code}: ${stderr}`)));
519
+ proc.on('error', reject);
520
+ });
521
+ } finally {
522
+ await fs.unlink(txtPath).catch(() => {});
523
+ await fs.unlink(rawPath).catch(() => {});
524
+ }
488
525
  return outputPath;
489
526
  }, { maxAttempts: 3, baseDelayMs: 3000, label: 'TTS(edge-tts)' });
490
527
  }