@thijshakkenberg/valkyrie-mom-mcp 2.4.1 → 2.5.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.
Files changed (36) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +17 -3
  3. package/agents/scenario-designer.md +3 -0
  4. package/dist/io/localization-io.d.ts.map +1 -1
  5. package/dist/io/localization-io.js +3 -1
  6. package/dist/io/localization-io.js.map +1 -1
  7. package/dist/narration/kokoro.d.ts +81 -0
  8. package/dist/narration/kokoro.d.ts.map +1 -0
  9. package/dist/narration/kokoro.js +241 -0
  10. package/dist/narration/kokoro.js.map +1 -0
  11. package/dist/narration/narration-text.d.ts +32 -0
  12. package/dist/narration/narration-text.d.ts.map +1 -0
  13. package/dist/narration/narration-text.js +82 -0
  14. package/dist/narration/narration-text.js.map +1 -0
  15. package/dist/resources/format-docs.d.ts +1 -1
  16. package/dist/resources/format-docs.d.ts.map +1 -1
  17. package/dist/resources/format-docs.js +2 -1
  18. package/dist/resources/format-docs.js.map +1 -1
  19. package/dist/server.d.ts.map +1 -1
  20. package/dist/server.js +54 -0
  21. package/dist/server.js.map +1 -1
  22. package/dist/tools/narration.d.ts +44 -0
  23. package/dist/tools/narration.d.ts.map +1 -0
  24. package/dist/tools/narration.js +88 -0
  25. package/dist/tools/narration.js.map +1 -0
  26. package/package.json +1 -1
  27. package/skills/artwork/SKILL.md +22 -10
  28. package/skills/narration/SKILL.md +78 -0
  29. package/skills/scenario/SKILL.md +1 -1
  30. package/skills/ui-and-puzzles/SKILL.md +2 -2
  31. package/src/io/localization-io.ts +3 -1
  32. package/src/narration/kokoro.ts +298 -0
  33. package/src/narration/narration-text.ts +119 -0
  34. package/src/resources/format-docs.ts +2 -1
  35. package/src/server.ts +76 -0
  36. package/src/tools/narration.ts +121 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "valkyrie-mom",
3
- "version": "2.4.1",
3
+ "version": "2.5.1",
4
4
  "description": "AI-assisted Mansions of Madness scenario editor for Valkyrie",
5
5
  "author": {
6
6
  "name": "Thijs Hakkenberg"
package/README.md CHANGED
@@ -8,7 +8,7 @@ MCP (Model Context Protocol) server and Claude Code plugin for AI-assisted Mansi
8
8
 
9
9
  This server exposes Valkyrie scenario editing as MCP tools, enabling AI assistants (Claude, etc.) to create, modify, validate, and build complete MoM scenarios. It auto-detects the Valkyrie editor directory so scenarios appear directly in the app.
10
10
 
11
- The plugin bundles 8 skills covering advanced patterns (event loops, mythos scaling, tile placement chains, custom puzzles, generated artwork, etc.) and an autonomous scenario designer agent.
11
+ The plugin bundles 9 skills covering advanced patterns (event loops, mythos scaling, tile placement chains, custom puzzles, generated artwork, spoken narration, etc.) and an autonomous scenario designer agent.
12
12
 
13
13
  ## Install
14
14
 
@@ -21,7 +21,7 @@ claude plugin install valkyrie-mom
21
21
 
22
22
  This gives you:
23
23
  - **MCP server** with 25 tools for scenario editing (auto-started)
24
- - **8 skills**: `/scenario`, `/event-patterns`, `/tile-placement`, `/variables-and-mythos`, `/custom-monsters`, `/ui-and-puzzles`, `/items-and-distribution`, `/artwork`
24
+ - **9 skills**: `/scenario`, `/event-patterns`, `/tile-placement`, `/variables-and-mythos`, `/custom-monsters`, `/ui-and-puzzles`, `/items-and-distribution`, `/artwork`, `/narration`
25
25
  - **Scenario designer agent** for autonomous scenario creation
26
26
  - **5 MCP resources** for format documentation
27
27
 
@@ -84,6 +84,7 @@ npx tsx src/index.ts # Run MCP server via stdio
84
84
  | `/ui-and-puzzles` | Prologues, interactive journals, combination locks, built-in puzzle types |
85
85
  | `/items-and-distribution` | Random items, unique items, starting items, inspection events |
86
86
  | `/artwork` | Generate pictures with a local ComfyUI (FLUX.2 [klein] 4B) and show them: cover, intro cutscene, handouts, scenes, monsters, tokens, image puzzles |
87
+ | `/narration` | Read story text aloud with a local Kokoro voice: what to narrate, voices, pronunciations, how Valkyrie plays event audio |
87
88
 
88
89
  ## MCP Tools
89
90
 
@@ -146,6 +147,14 @@ comfy launch --background
146
147
 
147
148
  On NVIDIA GPUs with less VRAM, the fp8 model (`black-forest-labs/FLUX.2-klein-4b-fp8`) is smaller. Set `VALKYRIE_COMFYUI_URL` if ComfyUI runs somewhere other than `http://127.0.0.1:8188`. The `/artwork` skill covers prompts and where Valkyrie shows images.
148
149
 
150
+ ### Narration
151
+ | Tool | Description |
152
+ |------|-------------|
153
+ | `narration_status` | Check the local text-to-speech engine and model, list the voices; `install=true` installs the engine |
154
+ | `generate_narration` | Speak components' dialog text with Kokoro-82M into OGG clips in the scenario folder (`audio/narration/<Event>.ogg`) and set their `audio=`. Speaks only the italic story text by default, expands `{qst:}`, turns icons into words, takes respellings for names |
155
+
156
+ Narration is optional and runs entirely on your machine, on the CPU: [Kokoro-82M](https://huggingface.co/hexgrad/Kokoro-82M) (Apache 2.0) through [kokoro-js](https://github.com/hexgrad/kokoro/tree/main/kokoro.js), with OGG Vorbis encoding in WebAssembly, so no Python or ffmpeg is needed. `narration_status install=true` installs the engine once (about 450 MB, in `~/.cache/valkyrie-mom-mcp/kokoro`, or `VALKYRIE_TTS_DIR`); the first narration downloads the model (about 330 MB). English voices only. The default voice, `af_nicole` at speed 0.9, was picked by ear from 15 voices for horror (`bf_isabella`, `bm_lewis` and `bm_daniel` also fit); the model's quality grades didn't predict the fit. The `/narration` skill covers what to narrate and how Valkyrie plays the clips.
157
+
149
158
  ### Reference
150
159
  | Tool | Description |
151
160
  |------|-------------|
@@ -239,7 +248,7 @@ npm run hermes:tools # Re-export the Hermes plugin tool list after changing too
239
248
  .claude-plugin/ Claude Code plugin manifest
240
249
  plugin.yaml Hermes Agent plugin manifest (+ __init__.py entry point)
241
250
  hermes_bridge/ Hermes plugin: MCP stdio client, tool registration, exported tools.json
242
- skills/ 8 skill SKILL.md files (Claude Code and Hermes)
251
+ skills/ 9 skill SKILL.md files (Claude Code and Hermes)
243
252
  agents/ Scenario designer agent
244
253
  src/
245
254
  io/ INI parser/writer, localization CSV, ZIP packager
@@ -249,6 +258,7 @@ src/
249
258
  tools/ MCP tool implementations
250
259
  story/ Storyline graph: outline, Mermaid, HTML
251
260
  artwork/ ComfyUI (FLUX.2 [klein]) artwork generation
261
+ narration/ Kokoro text-to-speech, dialog text to speech text
252
262
  resources/ Format documentation resources
253
263
  catalogs/ 846-entry game content catalog
254
264
  diagnostics/ Bug report builder, session trace, ring buffer
@@ -265,6 +275,10 @@ tests/
265
275
  integration.test.ts Full pipeline tests
266
276
  ```
267
277
 
278
+ ## Changes and Roadmap
279
+
280
+ See [CHANGELOG.md](CHANGELOG.md) for what changed in each release and [TODO.md](TODO.md) for what is planned next (hybrid semantic search for game content, generated sound effects).
281
+
268
282
  ## Related Projects
269
283
 
270
284
  - [Valkyrie](https://github.com/NPBruce/valkyrie) - Scenario builder and player
@@ -28,6 +28,7 @@ skills:
28
28
  - ui-and-puzzles
29
29
  - items-and-distribution
30
30
  - artwork
31
+ - narration
31
32
  ---
32
33
 
33
34
  # Scenario Designer Agent
@@ -42,6 +43,7 @@ You are an autonomous Mansions of Madness 2nd Edition scenario designer. You cre
42
43
  - Configure monster spawns with appropriate difficulty scaling
43
44
  - Build custom puzzles and UI overlays
44
45
  - Generate scenario artwork (cover, intro, handouts) when a local ComfyUI is available (`artwork_status`)
46
+ - Narrate the introduction, key story beats and endings with a local Kokoro voice when the engine is installed (`narration_status`)
45
47
  - Set up mythos scaling for tension progression
46
48
  - Write atmospheric narrative text in the Lovecraftian horror style
47
49
  - Validate scenarios for correctness and completeness
@@ -58,6 +60,7 @@ Use these skills for detailed pattern guidance:
58
60
  - `/ui-and-puzzles` — Prologues, interactive journals, combination locks, built-in puzzles
59
61
  - `/items-and-distribution` — Random items, unique items, starting items, inspection events
60
62
  - `/artwork` — Generate pictures with ComfyUI (FLUX.2 [klein]) and show them: cover, intro cutscene, handouts, scenes, monsters, tokens, image puzzles
63
+ - `/narration` — Read story text aloud with a local Kokoro voice: OGG clips on event `audio=`, what to narrate, voices, pronunciations
61
64
 
62
65
  ## Design Principles
63
66
 
@@ -1 +1 @@
1
- {"version":3,"file":"localization-io.d.ts","sourceRoot":"","sources":["../../src/io/localization-io.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B;AAKD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,kBAAkB,CA4CrE;AAcD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAexF"}
1
+ {"version":3,"file":"localization-io.d.ts","sourceRoot":"","sources":["../../src/io/localization-io.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC9B;AAKD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,kBAAkB,CA4CrE;AAcD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAiBxF"}
@@ -77,7 +77,9 @@ export function writeLocalization(language, entries) {
77
77
  const lines = [`.,${language}`];
78
78
  for (const [key, rawValue] of entries) {
79
79
  const value = rawValue.replace(/\r\n|\r|\n/g, '\\n');
80
- if (value.includes('"') && !value.includes(TRIPLE)) {
80
+ // ParseEntry strips a pair of outer quotes even inside |||, so |||"dialogue"||| would lose them
81
+ const quotedAtBothEnds = value.length > 1 && value.startsWith('"') && value.endsWith('"');
82
+ if (value.includes('"') && !value.includes(TRIPLE) && !quotedAtBothEnds) {
81
83
  lines.push(`${key},${TRIPLE}${value}${TRIPLE}`);
82
84
  }
83
85
  else if (value.includes('"') || value.includes(TRIPLE) || value.includes('\\n') || value.includes(',')) {
@@ -1 +1 @@
1
- {"version":3,"file":"localization-io.js","sourceRoot":"","sources":["../../src/io/localization-io.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"localization-io.js","sourceRoot":"","sources":["../../src/io/localization-io.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Narration with Kokoro-82M (kokoro-js, ONNX on the CPU) saved as OGG Vorbis, the format
3
+ * Valkyrie's editor lists for event audio. kokoro-js brings onnxruntime (~400 MB), so it is
4
+ * not a dependency of this package: installNarrationEngine() puts it in its own folder, and
5
+ * the model (~330 MB fp32) is downloaded there on first use.
6
+ */
7
+ export declare const KOKORO_MODEL_ID = "onnx-community/Kokoro-82M-v1.0-ONNX";
8
+ /** Pinned so every install behaves the same; wasm-media-encoders encodes OGG Vorbis without ffmpeg */
9
+ export declare const ENGINE_PACKAGES: {
10
+ readonly 'kokoro-js': "1.2.1";
11
+ readonly 'wasm-media-encoders': "0.7.0";
12
+ };
13
+ /** fp32 is both faster on the CPU (about 2 s per sentence on an M1 Pro) and cleaner than q8 */
14
+ export type KokoroDtype = 'fp32' | 'fp16' | 'q8' | 'q4' | 'q4f16';
15
+ export declare const DEFAULT_DTYPE: KokoroDtype;
16
+ export declare const SAMPLE_RATE = 24000;
17
+ /**
18
+ * Voices picked by ear for horror narration. 15 voices read the same passage at speed 0.9
19
+ * ("The lamp gutters and goes out. / Somewhere beneath the floorboards of the Blackwood
20
+ * house, something has begun to sing. / It knows your name.") on 2026-10-04. The quality
21
+ * grades did not predict the fit: the A and A- voices (af_heart, af_bella) were not picked,
22
+ * and two D voices were. Best first.
23
+ */
24
+ export declare const HORROR_PICKS: ReadonlyArray<{
25
+ voice: string;
26
+ verdict: string;
27
+ }>;
28
+ /** Also in that trial, not picked */
29
+ export declare const HORROR_TRIAL_OTHERS: readonly ["am_fenrir", "am_michael", "am_onyx", "am_puck", "am_echo", "af_heart", "af_bella", "af_kore", "bm_george", "bm_fable", "bf_emma"];
30
+ /** The trial's winner, at the speed it was judged at */
31
+ export declare const DEFAULT_VOICE: string;
32
+ export declare const DEFAULT_SPEED = 0.9;
33
+ /** kokoro-js speaks English only: a = American, b = British. Grades are hexgrad's quality ratings */
34
+ export declare const VOICES: Record<string, string>;
35
+ /** Where the engine lives: VALKYRIE_TTS_DIR, or a per-user cache folder */
36
+ export declare function defaultEngineDir(): string;
37
+ export interface NarrationStatus {
38
+ engineDir: string;
39
+ /** Installed package versions; missing packages are absent */
40
+ packages: Partial<Record<keyof typeof ENGINE_PACKAGES, string>>;
41
+ installed: boolean;
42
+ dtype: KokoroDtype;
43
+ /** The model for dtype is in the engine's cache; otherwise the first narration downloads it */
44
+ modelDownloaded: boolean;
45
+ }
46
+ export declare function getNarrationStatus(engineDir?: string, dtype?: KokoroDtype): NarrationStatus;
47
+ export declare function narrationSetupInstructions(status: NarrationStatus): string;
48
+ export type RunCommand = (cmd: string, args: string[], cwd: string) => Promise<{
49
+ code: number;
50
+ output: string;
51
+ }>;
52
+ /** Installs kokoro-js and the OGG encoder into the engine folder with npm */
53
+ export declare function installNarrationEngine(engineDir?: string, run?: RunCommand): Promise<NarrationStatus>;
54
+ /** Text to samples and samples to OGG; an interface so tests can stand in for the model */
55
+ export interface SpeechEngine {
56
+ /** Mono float samples at SAMPLE_RATE, one entry per sentence */
57
+ synthesize(text: string, voice: string, speed: number): Promise<Float32Array[]>;
58
+ encodeOgg(samples: Float32Array, sampleRate: number): Promise<Uint8Array>;
59
+ }
60
+ /** Loads kokoro-js from the engine folder once per process (the first load may download the model) */
61
+ export declare function loadKokoroEngine(engineDir?: string, dtype?: KokoroDtype): Promise<SpeechEngine>;
62
+ export interface NarrateOptions {
63
+ /** Spoken in order with a pause between them */
64
+ paragraphs: string[];
65
+ outputFile: string;
66
+ voice?: string;
67
+ speed?: number;
68
+ engine: SpeechEngine;
69
+ }
70
+ export interface NarrateResult {
71
+ file: string;
72
+ /** Length of the clip */
73
+ duration: number;
74
+ bytes: number;
75
+ /** Time taken to synthesize and encode */
76
+ seconds: number;
77
+ }
78
+ export declare function assertVoice(voice: string): void;
79
+ /** Speaks the paragraphs and writes one OGG Vorbis clip */
80
+ export declare function narrate(opts: NarrateOptions): Promise<NarrateResult>;
81
+ //# sourceMappingURL=kokoro.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kokoro.d.ts","sourceRoot":"","sources":["../../src/narration/kokoro.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,241 @@
1
+ import { spawn } from 'node:child_process';
2
+ import * as fs from 'node:fs';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+ /**
7
+ * Narration with Kokoro-82M (kokoro-js, ONNX on the CPU) saved as OGG Vorbis, the format
8
+ * Valkyrie's editor lists for event audio. kokoro-js brings onnxruntime (~400 MB), so it is
9
+ * not a dependency of this package: installNarrationEngine() puts it in its own folder, and
10
+ * the model (~330 MB fp32) is downloaded there on first use.
11
+ */
12
+ export const KOKORO_MODEL_ID = 'onnx-community/Kokoro-82M-v1.0-ONNX';
13
+ /** Pinned so every install behaves the same; wasm-media-encoders encodes OGG Vorbis without ffmpeg */
14
+ export const ENGINE_PACKAGES = { 'kokoro-js': '1.2.1', 'wasm-media-encoders': '0.7.0' };
15
+ export const DEFAULT_DTYPE = process.env.VALKYRIE_TTS_DTYPE ?? 'fp32';
16
+ export const SAMPLE_RATE = 24000;
17
+ /**
18
+ * Voices picked by ear for horror narration. 15 voices read the same passage at speed 0.9
19
+ * ("The lamp gutters and goes out. / Somewhere beneath the floorboards of the Blackwood
20
+ * house, something has begun to sing. / It knows your name.") on 2026-10-04. The quality
21
+ * grades did not predict the fit: the A and A- voices (af_heart, af_bella) were not picked,
22
+ * and two D voices were. Best first.
23
+ */
24
+ export const HORROR_PICKS = [
25
+ { voice: 'af_nicole', verdict: 'best fit' },
26
+ { voice: 'bf_isabella', verdict: 'good' },
27
+ { voice: 'bm_lewis', verdict: 'good' },
28
+ { voice: 'bm_daniel', verdict: 'good' },
29
+ ];
30
+ /** Also in that trial, not picked */
31
+ export const HORROR_TRIAL_OTHERS = ['am_fenrir', 'am_michael', 'am_onyx', 'am_puck', 'am_echo', 'af_heart', 'af_bella', 'af_kore', 'bm_george', 'bm_fable', 'bf_emma'];
32
+ /** The trial's winner, at the speed it was judged at */
33
+ export const DEFAULT_VOICE = HORROR_PICKS[0].voice;
34
+ export const DEFAULT_SPEED = 0.9;
35
+ /** kokoro-js speaks English only: a = American, b = British. Grades are hexgrad's quality ratings */
36
+ export const VOICES = {
37
+ af_heart: 'American female, A', af_bella: 'American female, A-', af_nicole: 'American female, B-',
38
+ af_aoede: 'American female, C+', af_kore: 'American female, C+', af_sarah: 'American female, C+',
39
+ af_alloy: 'American female, C', af_nova: 'American female, C', af_sky: 'American female, C-',
40
+ af_jessica: 'American female, D', af_river: 'American female, D',
41
+ am_fenrir: 'American male, C+', am_michael: 'American male, C+', am_puck: 'American male, C+',
42
+ am_echo: 'American male, D', am_eric: 'American male, D', am_liam: 'American male, D', am_onyx: 'American male, D',
43
+ am_santa: 'American male, D-', am_adam: 'American male, F+',
44
+ bf_emma: 'British female, B-', bf_isabella: 'British female, C', bf_alice: 'British female, D', bf_lily: 'British female, D',
45
+ bm_george: 'British male, C', bm_fable: 'British male, C', bm_lewis: 'British male, D+', bm_daniel: 'British male, D',
46
+ };
47
+ /** Silence between paragraphs and between sentences, in seconds */
48
+ const PARAGRAPH_PAUSE = 0.6;
49
+ const SENTENCE_PAUSE = 0.12;
50
+ /** Where the engine lives: VALKYRIE_TTS_DIR, or a per-user cache folder */
51
+ export function defaultEngineDir() {
52
+ if (process.env.VALKYRIE_TTS_DIR)
53
+ return process.env.VALKYRIE_TTS_DIR;
54
+ const base = process.platform === 'win32'
55
+ ? (process.env.LOCALAPPDATA ?? path.join(os.homedir(), 'AppData', 'Local'))
56
+ : (process.env.XDG_CACHE_HOME ?? path.join(os.homedir(), '.cache'));
57
+ return path.join(base, 'valkyrie-mom-mcp', 'kokoro');
58
+ }
59
+ function modelFile(dtype) {
60
+ return dtype === 'fp32' ? 'model.onnx' : dtype === 'q8' ? 'model_quantized.onnx' : `model_${dtype}.onnx`;
61
+ }
62
+ function installedVersion(dir, pkg) {
63
+ try {
64
+ return JSON.parse(fs.readFileSync(path.join(dir, 'node_modules', pkg, 'package.json'), 'utf8')).version;
65
+ }
66
+ catch {
67
+ return undefined;
68
+ }
69
+ }
70
+ export function getNarrationStatus(engineDir = defaultEngineDir(), dtype = DEFAULT_DTYPE) {
71
+ const packages = {};
72
+ for (const pkg of Object.keys(ENGINE_PACKAGES)) {
73
+ const v = installedVersion(engineDir, pkg);
74
+ if (v)
75
+ packages[pkg] = v;
76
+ }
77
+ return {
78
+ engineDir,
79
+ packages,
80
+ installed: Object.keys(packages).length === Object.keys(ENGINE_PACKAGES).length,
81
+ dtype,
82
+ modelDownloaded: fs.existsSync(path.join(engineDir, 'models', ...KOKORO_MODEL_ID.split('/'), 'onnx', modelFile(dtype))),
83
+ };
84
+ }
85
+ export function narrationSetupInstructions(status) {
86
+ const deps = Object.entries(ENGINE_PACKAGES).map(([p, v]) => `${p}@${v}`).join(' ');
87
+ const lines = [];
88
+ if (!status.installed) {
89
+ lines.push(`The narration engine is not installed in ${status.engineDir}.`, 'Call narration_status with install=true, or run:', ` npm install --prefix "${status.engineDir}" ${deps}`, 'It needs about 450 MB (kokoro-js with onnxruntime). Set VALKYRIE_TTS_DIR to keep it elsewhere.');
90
+ }
91
+ if (!status.modelDownloaded) {
92
+ lines.push(`The Kokoro-82M model (${status.dtype}, ${status.dtype === 'fp32' ? '~330 MB' : '~90-170 MB'}) is downloaded from Hugging Face on the first narration.`);
93
+ }
94
+ return lines.join('\n');
95
+ }
96
+ const runCommand = (cmd, args, cwd) => new Promise((resolve, reject) => {
97
+ const child = spawn(cmd, args, { cwd, shell: process.platform === 'win32', stdio: ['ignore', 'pipe', 'pipe'] });
98
+ let output = '';
99
+ child.stdout.on('data', d => { output += d; });
100
+ child.stderr.on('data', d => { output += d; });
101
+ child.on('error', reject);
102
+ child.on('close', code => resolve({ code: code ?? 1, output }));
103
+ });
104
+ /** Installs kokoro-js and the OGG encoder into the engine folder with npm */
105
+ export async function installNarrationEngine(engineDir = defaultEngineDir(), run = runCommand) {
106
+ fs.mkdirSync(engineDir, { recursive: true });
107
+ fs.writeFileSync(path.join(engineDir, 'package.json'), JSON.stringify({
108
+ name: 'valkyrie-mom-narration-engine',
109
+ private: true,
110
+ description: 'Text to speech for valkyrie-mom-mcp (generate_narration)',
111
+ dependencies: ENGINE_PACKAGES,
112
+ }, null, 2) + '\n');
113
+ const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
114
+ const { code, output } = await run(npm, ['install', '--no-audit', '--no-fund', '--omit=dev', '--loglevel=error'], engineDir);
115
+ if (code !== 0)
116
+ throw new Error(`npm install failed in ${engineDir} (exit ${code}):\n${output.slice(-2000)}`);
117
+ const status = getNarrationStatus(engineDir);
118
+ if (!status.installed)
119
+ throw new Error(`npm install finished, but ${engineDir} is still missing packages:\n${output.slice(-2000)}`);
120
+ return status;
121
+ }
122
+ /** The ES module entry of a package installed in the engine folder */
123
+ function esmEntry(engineDir, pkg) {
124
+ const pkgDir = path.join(engineDir, 'node_modules', pkg);
125
+ const pj = JSON.parse(fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf8'));
126
+ const root = pj.exports?.['.'] ?? pj.exports;
127
+ const entry = root?.node?.import ?? root?.import ?? pj.module ?? pj.main ?? 'index.js';
128
+ return pathToFileURL(path.join(pkgDir, typeof entry === 'string' ? entry : entry.default)).href;
129
+ }
130
+ /**
131
+ * MCP talks JSON over stdout: anything the libraries print there would break the connection,
132
+ * so console output goes to stderr while they run.
133
+ */
134
+ async function quietStdout(fn) {
135
+ const saved = { log: console.log, info: console.info, table: console.table, debug: console.debug };
136
+ console.log = console.info = console.debug = (...a) => console.error(...a);
137
+ console.table = (d) => console.error(d);
138
+ try {
139
+ return await fn();
140
+ }
141
+ finally {
142
+ Object.assign(console, saved);
143
+ }
144
+ }
145
+ const engines = new Map();
146
+ /** Loads kokoro-js from the engine folder once per process (the first load may download the model) */
147
+ export function loadKokoroEngine(engineDir = defaultEngineDir(), dtype = DEFAULT_DTYPE) {
148
+ const key = `${engineDir}|${dtype}`;
149
+ let engine = engines.get(key);
150
+ if (!engine) {
151
+ engine = createKokoroEngine(engineDir, dtype);
152
+ engines.set(key, engine);
153
+ engine.catch(() => engines.delete(key));
154
+ }
155
+ return engine;
156
+ }
157
+ async function createKokoroEngine(engineDir, dtype) {
158
+ const status = getNarrationStatus(engineDir, dtype);
159
+ if (!status.installed)
160
+ throw new Error(narrationSetupInstructions(status));
161
+ // kokoro-js's own env.cacheDir does not reach the transformers instance that loads the
162
+ // model, so set it there; otherwise the model lands inside node_modules
163
+ const transformers = await import(esmEntry(engineDir, '@huggingface/transformers'));
164
+ transformers.env.cacheDir = path.join(engineDir, 'models');
165
+ const kokoro = await import(esmEntry(engineDir, 'kokoro-js'));
166
+ const encoders = await import(esmEntry(engineDir, 'wasm-media-encoders'));
167
+ // kokoro-js is loaded at runtime from the engine folder, so it has no types here
168
+ const tts = await quietStdout(() => kokoro.KokoroTTS.from_pretrained(KOKORO_MODEL_ID, { dtype, device: 'cpu' }));
169
+ return {
170
+ async synthesize(text, voice, speed) {
171
+ const chunks = [];
172
+ await quietStdout(async () => {
173
+ // Sentence by sentence keeps each one under the model's 510-token limit. stream() does
174
+ // not close a splitter it makes from a string, so close our own to get the last sentence
175
+ const splitter = new kokoro.TextSplitterStream();
176
+ splitter.push(text);
177
+ splitter.close();
178
+ for await (const { audio } of tts.stream(splitter, { voice, speed }))
179
+ chunks.push(audio.audio);
180
+ });
181
+ return chunks;
182
+ },
183
+ async encodeOgg(samples, sampleRate) {
184
+ const encoder = await encoders.createOggEncoder();
185
+ encoder.configure({ channels: 1, sampleRate, vbrQuality: 4 });
186
+ // encode() and finalize() return views into the encoder's memory, so copy each one
187
+ const body = encoder.encode([samples]).slice();
188
+ const tail = encoder.finalize().slice();
189
+ const out = new Uint8Array(body.length + tail.length);
190
+ out.set(body);
191
+ out.set(tail, body.length);
192
+ return out;
193
+ },
194
+ };
195
+ }
196
+ export function assertVoice(voice) {
197
+ if (!(voice in VOICES)) {
198
+ throw new Error(`Unknown voice "${voice}". Voices: ${Object.keys(VOICES).join(', ')}`);
199
+ }
200
+ }
201
+ /** Speaks the paragraphs and writes one OGG Vorbis clip */
202
+ export async function narrate(opts) {
203
+ const voice = opts.voice ?? DEFAULT_VOICE;
204
+ assertVoice(voice);
205
+ const speed = opts.speed ?? DEFAULT_SPEED;
206
+ if (!(speed >= 0.5 && speed <= 2))
207
+ throw new Error('speed must be between 0.5 and 2');
208
+ if (opts.paragraphs.length === 0)
209
+ throw new Error('Nothing to narrate');
210
+ const started = Date.now();
211
+ const silence = (s) => new Float32Array(Math.round(s * SAMPLE_RATE));
212
+ const parts = [];
213
+ for (const [i, paragraph] of opts.paragraphs.entries()) {
214
+ if (i > 0)
215
+ parts.push(silence(PARAGRAPH_PAUSE));
216
+ const sentences = await opts.engine.synthesize(paragraph, voice, speed);
217
+ sentences.forEach((s, j) => {
218
+ if (j > 0)
219
+ parts.push(silence(SENTENCE_PAUSE));
220
+ parts.push(s);
221
+ });
222
+ }
223
+ // A short tail so the last word is not clipped when playback stops
224
+ parts.push(silence(0.25));
225
+ const samples = new Float32Array(parts.reduce((n, p) => n + p.length, 0));
226
+ let offset = 0;
227
+ for (const p of parts) {
228
+ samples.set(p, offset);
229
+ offset += p.length;
230
+ }
231
+ const ogg = await opts.engine.encodeOgg(samples, SAMPLE_RATE);
232
+ fs.mkdirSync(path.dirname(opts.outputFile), { recursive: true });
233
+ fs.writeFileSync(opts.outputFile, ogg);
234
+ return {
235
+ file: opts.outputFile,
236
+ duration: Math.round((samples.length / SAMPLE_RATE) * 10) / 10,
237
+ bytes: ogg.length,
238
+ seconds: Math.round((Date.now() - started) / 100) / 10,
239
+ };
240
+ }
241
+ //# sourceMappingURL=kokoro.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kokoro.js","sourceRoot":"","sources":["../../src/narration/kokoro.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import type { ScenarioModel } from '../model/scenario-model.js';
2
+ /**
3
+ * Turns Valkyrie dialog text into something a TTS voice can read aloud: {qst:} entries are
4
+ * expanded, icons become words, markup is dropped, and text that only exists while playing
5
+ * ({rnd:hero}, {var:}) is replaced or left out with a note.
6
+ */
7
+ /**
8
+ * Which part of the text to speak. Community scenarios set the story in italics
9
+ * ("<i>You come across an old tome.</i>\n\nTest {lore}."), so "auto" speaks only the
10
+ * italic parts when there are any, and the whole text otherwise.
11
+ */
12
+ export type NarrationPart = 'auto' | 'flavor' | 'all';
13
+ export interface SpeechTextOptions {
14
+ part?: NarrationPart;
15
+ /** Resolves {qst:KEY}; usually model.localization.get */
16
+ lookup?: (key: string) => string | undefined;
17
+ /** Word -> respelling for names the voice gets wrong, e.g. { Cthulhu: "Kuh-thoo-loo" } */
18
+ pronunciations?: Record<string, string>;
19
+ }
20
+ export interface SpeechText {
21
+ /** Paragraphs joined by blank lines; empty when nothing is left to say */
22
+ text: string;
23
+ paragraphs: string[];
24
+ /** What was replaced or dropped because it is only known during play */
25
+ notes: string[];
26
+ }
27
+ /** {ffg:TILE_TOWN_SQUARE_MAD20} -> "town square" */
28
+ export declare function humanizeFfgKey(key: string): string;
29
+ export declare function speechText(raw: string, opts?: SpeechTextOptions): SpeechText;
30
+ /** The localized text a component shows (<name>.text), or undefined when it has none */
31
+ export declare function componentText(model: ScenarioModel, name: string): string | undefined;
32
+ //# sourceMappingURL=narration-text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"narration-text.d.ts","sourceRoot":"","sources":["../../src/narration/narration-text.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAEhE;;;;GAIG;AAEH;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;AAEtD,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,yDAAyD;IACzD,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IAC7C,0FAA0F;IAC1F,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,UAAU;IACzB,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,wEAAwE;IACxE,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAQD,oDAAoD;AACpD,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAMlD;AA0BD,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,iBAAsB,GAAG,UAAU,CA2ChF;AAED,wFAAwF;AACxF,wBAAgB,aAAa,CAAC,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEpF"}
@@ -0,0 +1,82 @@
1
+ /** {action}, {strength}, ... as words */
2
+ const ICON_WORDS = {
3
+ action: 'action', strength: 'strength', agility: 'agility', observation: 'observation', lore: 'lore',
4
+ influence: 'influence', will: 'will', success: 'success', clue: 'clue', health: 'health', sanity: 'sanity',
5
+ };
6
+ /** {ffg:TILE_TOWN_SQUARE_MAD20} -> "town square" */
7
+ export function humanizeFfgKey(key) {
8
+ return key
9
+ .replace(/^(MONSTER|TILE|ITEM|TOKEN|INVESTIGATOR|PUZZLE|AUDIO|UI)_/i, '')
10
+ .replace(/_(MAD|MOM|BTT|SOT|HJ|POTS|SOA|SOTP)\d*$/i, '')
11
+ .replace(/_/g, ' ')
12
+ .toLowerCase();
13
+ }
14
+ /** TileTownSquare -> "Town Square", QItemOldTome -> "Old Tome" */
15
+ function humanizeComponentName(name) {
16
+ return name
17
+ .replace(/^(Tile|Token|QItem|CustomMonster|Spawn|MPlace|UI|Puzzle|Door)/, '')
18
+ .replace(/([a-z])([A-Z0-9])/g, '$1 $2')
19
+ .trim();
20
+ }
21
+ function escapeRegExp(s) {
22
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
23
+ }
24
+ /** Expands {qst:KEY} from the localization, a few levels deep */
25
+ function expandQst(text, lookup, notes, depth = 0) {
26
+ return text.replace(/\{qst:([^}]+)\}/g, (_, key) => {
27
+ const value = lookup?.(key);
28
+ if (value === undefined || depth >= 5) {
29
+ notes.push(`{qst:${key}} has no text and was left out`);
30
+ return '';
31
+ }
32
+ return expandQst(value, lookup, notes, depth + 1);
33
+ });
34
+ }
35
+ export function speechText(raw, opts = {}) {
36
+ const notes = [];
37
+ // Localization files keep newlines as a literal \n
38
+ let text = expandQst(raw, opts.lookup, notes).replace(/\\n/g, '\n');
39
+ const part = opts.part ?? 'auto';
40
+ if (part !== 'all') {
41
+ const italic = [...text.matchAll(/<i>([\s\S]*?)<\/i>/gi)].map(m => m[1]);
42
+ if (italic.length > 0)
43
+ text = italic.join('\n\n');
44
+ else if (part === 'flavor')
45
+ notes.push('No italic story text found; the whole text is spoken');
46
+ }
47
+ text = text
48
+ .replace(/\{rnd:hero\}/gi, () => {
49
+ notes.push('{rnd:hero} is spoken as "an investigator"');
50
+ return 'an investigator';
51
+ })
52
+ .replace(/\{c:([^}]+)\}/g, (_, name) => {
53
+ // On an event, {c:} repeats the investigator that event's {rnd:hero} picked
54
+ if (/^Event/.test(name)) {
55
+ notes.push(`{c:${name}} is spoken as "the investigator"`);
56
+ return 'the investigator';
57
+ }
58
+ return humanizeComponentName(name);
59
+ })
60
+ .replace(/\{var:([^}]+)\}/g, (_, name) => {
61
+ notes.push(`{var:${name}} is only known during play and was left out`);
62
+ return '';
63
+ })
64
+ .replace(/\{ffg:([^}]+)\}/g, (_, key) => humanizeFfgKey(key))
65
+ .replace(/\{(\w+)\}/g, (_, word) => ICON_WORDS[word.toLowerCase()] ?? word)
66
+ .replace(/<[^>]+>/g, '');
67
+ for (const [word, spoken] of Object.entries(opts.pronunciations ?? {})) {
68
+ if (!word.trim())
69
+ continue;
70
+ text = text.replace(new RegExp(`(?<![\\p{L}\\p{N}])${escapeRegExp(word)}(?![\\p{L}\\p{N}])`, 'giu'), spoken);
71
+ }
72
+ const paragraphs = text
73
+ .split(/\n+/)
74
+ .map(p => p.replace(/[ \t]+/g, ' ').replace(/\s+([,.;:!?])/g, '$1').trim())
75
+ .filter(p => /[\p{L}\p{N}]/u.test(p));
76
+ return { text: paragraphs.join('\n\n'), paragraphs, notes: [...new Set(notes)] };
77
+ }
78
+ /** The localized text a component shows (<name>.text), or undefined when it has none */
79
+ export function componentText(model, name) {
80
+ return model.localization.get(`${name}.text`);
81
+ }
82
+ //# sourceMappingURL=narration-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"narration-text.js","sourceRoot":"","sources":["../../src/narration/narration-text.ts"],"names":[],"mappings":"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"}