@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +17 -3
- package/agents/scenario-designer.md +3 -0
- package/dist/io/localization-io.d.ts.map +1 -1
- package/dist/io/localization-io.js +3 -1
- package/dist/io/localization-io.js.map +1 -1
- package/dist/narration/kokoro.d.ts +81 -0
- package/dist/narration/kokoro.d.ts.map +1 -0
- package/dist/narration/kokoro.js +241 -0
- package/dist/narration/kokoro.js.map +1 -0
- package/dist/narration/narration-text.d.ts +32 -0
- package/dist/narration/narration-text.d.ts.map +1 -0
- package/dist/narration/narration-text.js +82 -0
- package/dist/narration/narration-text.js.map +1 -0
- package/dist/resources/format-docs.d.ts +1 -1
- package/dist/resources/format-docs.d.ts.map +1 -1
- package/dist/resources/format-docs.js +2 -1
- package/dist/resources/format-docs.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +54 -0
- package/dist/server.js.map +1 -1
- package/dist/tools/narration.d.ts +44 -0
- package/dist/tools/narration.d.ts.map +1 -0
- package/dist/tools/narration.js +88 -0
- package/dist/tools/narration.js.map +1 -0
- package/package.json +1 -1
- package/skills/artwork/SKILL.md +22 -10
- package/skills/narration/SKILL.md +78 -0
- package/skills/scenario/SKILL.md +1 -1
- package/skills/ui-and-puzzles/SKILL.md +2 -2
- package/src/io/localization-io.ts +3 -1
- package/src/narration/kokoro.ts +298 -0
- package/src/narration/narration-text.ts +119 -0
- package/src/resources/format-docs.ts +2 -1
- package/src/server.ts +76 -0
- package/src/tools/narration.ts +121 -0
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
|
|
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
|
-
- **
|
|
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/
|
|
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,
|
|
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
|
-
|
|
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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"}
|