@kongyo2/kongyoroid 1.0.0 → 2.1.0
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/README.md +239 -0
- package/dist/cache.d.ts +32 -7
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +139 -9
- package/dist/cache.js.map +1 -1
- package/dist/cli/flags.d.ts +270 -0
- package/dist/cli/flags.d.ts.map +1 -0
- package/dist/cli/flags.js +240 -0
- package/dist/cli/flags.js.map +1 -0
- package/dist/cli/help.d.ts +11 -0
- package/dist/cli/help.d.ts.map +1 -0
- package/dist/cli/help.js +346 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli.js +729 -243
- package/dist/cli.js.map +1 -1
- package/dist/errors.d.ts +43 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +56 -3
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +59 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -14
- package/dist/index.js.map +1 -1
- package/dist/io.d.ts +12 -0
- package/dist/io.d.ts.map +1 -1
- package/dist/io.js +161 -6
- package/dist/io.js.map +1 -1
- package/dist/kongyoroid.d.ts +237 -10
- package/dist/kongyoroid.d.ts.map +1 -1
- package/dist/kongyoroid.js +787 -87
- package/dist/kongyoroid.js.map +1 -1
- package/dist/limits.d.ts +5 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +6 -1
- package/dist/limits.js.map +1 -1
- package/dist/player.d.ts.map +1 -1
- package/dist/player.js +18 -22
- package/dist/player.js.map +1 -1
- package/dist/request.d.ts +1 -0
- package/dist/request.d.ts.map +1 -1
- package/dist/request.js +102 -15
- package/dist/request.js.map +1 -1
- package/dist/schema.d.ts +3 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +271 -68
- package/dist/schema.js.map +1 -1
- package/dist/song/mml.d.ts +15 -0
- package/dist/song/mml.d.ts.map +1 -0
- package/dist/song/mml.js +184 -0
- package/dist/song/mml.js.map +1 -0
- package/dist/song/pitch.d.ts.map +1 -0
- package/dist/{pitch.js → song/pitch.js} +1 -1
- package/dist/song/pitch.js.map +1 -0
- package/dist/song/score.d.ts +92 -0
- package/dist/song/score.d.ts.map +1 -0
- package/dist/song/score.js +454 -0
- package/dist/song/score.js.map +1 -0
- package/dist/synth/acoustics.d.ts +27 -0
- package/dist/synth/acoustics.d.ts.map +1 -0
- package/dist/synth/acoustics.js +608 -0
- package/dist/synth/acoustics.js.map +1 -0
- package/dist/synth/analysis.d.ts +22 -0
- package/dist/synth/analysis.d.ts.map +1 -0
- package/dist/synth/analysis.js +170 -0
- package/dist/synth/analysis.js.map +1 -0
- package/dist/synth/engine.d.ts +115 -0
- package/dist/synth/engine.d.ts.map +1 -0
- package/dist/synth/engine.js +223 -0
- package/dist/synth/engine.js.map +1 -0
- package/dist/synth/filters.d.ts +60 -0
- package/dist/synth/filters.d.ts.map +1 -0
- package/dist/synth/filters.js +166 -0
- package/dist/synth/filters.js.map +1 -0
- package/dist/synth/glottal.d.ts +20 -0
- package/dist/synth/glottal.d.ts.map +1 -0
- package/dist/synth/glottal.js +76 -0
- package/dist/synth/glottal.js.map +1 -0
- package/dist/synth/phonemes.d.ts +42 -0
- package/dist/synth/phonemes.d.ts.map +1 -0
- package/dist/synth/phonemes.js +311 -0
- package/dist/synth/phonemes.js.map +1 -0
- package/dist/synth/plan.d.ts +127 -0
- package/dist/synth/plan.d.ts.map +1 -0
- package/dist/synth/plan.js +81 -0
- package/dist/synth/plan.js.map +1 -0
- package/dist/synth/prosody.d.ts +38 -0
- package/dist/synth/prosody.d.ts.map +1 -0
- package/dist/synth/prosody.js +120 -0
- package/dist/synth/prosody.js.map +1 -0
- package/dist/synth/random.d.ts +15 -0
- package/dist/synth/random.d.ts.map +1 -0
- package/dist/synth/random.js +59 -0
- package/dist/synth/random.js.map +1 -0
- package/dist/synth/renderer.d.ts +77 -0
- package/dist/synth/renderer.d.ts.map +1 -0
- package/dist/synth/renderer.js +343 -0
- package/dist/synth/renderer.js.map +1 -0
- package/dist/synth/song.d.ts +30 -0
- package/dist/synth/song.d.ts.map +1 -0
- package/dist/synth/song.js +453 -0
- package/dist/synth/song.js.map +1 -0
- package/dist/synth/speech.d.ts +49 -0
- package/dist/synth/speech.d.ts.map +1 -0
- package/dist/synth/speech.js +358 -0
- package/dist/synth/speech.js.map +1 -0
- package/dist/synth/stream.d.ts +26 -0
- package/dist/synth/stream.d.ts.map +1 -0
- package/dist/synth/stream.js +67 -0
- package/dist/synth/stream.js.map +1 -0
- package/dist/synth/textstream.d.ts +35 -0
- package/dist/synth/textstream.d.ts.map +1 -0
- package/dist/synth/textstream.js +209 -0
- package/dist/synth/textstream.js.map +1 -0
- package/dist/synth/voice.d.ts +64 -0
- package/dist/synth/voice.d.ts.map +1 -0
- package/dist/synth/voice.js +208 -0
- package/dist/synth/voice.js.map +1 -0
- package/dist/text/dictionary.d.ts +50 -0
- package/dist/text/dictionary.d.ts.map +1 -0
- package/dist/text/dictionary.js +0 -0
- package/dist/text/dictionary.js.map +1 -0
- package/dist/text/frontend.d.ts +35 -0
- package/dist/text/frontend.d.ts.map +1 -0
- package/dist/text/frontend.js +559 -0
- package/dist/text/frontend.js.map +1 -0
- package/dist/text/lexicon.d.ts +10 -0
- package/dist/text/lexicon.d.ts.map +1 -0
- package/dist/text/lexicon.js +763 -0
- package/dist/text/lexicon.js.map +1 -0
- package/dist/{mora.d.ts → text/mora.d.ts} +15 -3
- package/dist/text/mora.d.ts.map +1 -0
- package/dist/{mora.js → text/mora.js} +87 -9
- package/dist/text/mora.js.map +1 -0
- package/dist/text/normalize.d.ts +34 -0
- package/dist/text/normalize.d.ts.map +1 -0
- package/dist/text/normalize.js +504 -0
- package/dist/text/normalize.js.map +1 -0
- package/dist/text/notation.d.ts +32 -0
- package/dist/text/notation.d.ts.map +1 -0
- package/dist/text/notation.js +182 -0
- package/dist/text/notation.js.map +1 -0
- package/dist/text/reading.d.ts +52 -0
- package/dist/text/reading.d.ts.map +1 -0
- package/dist/text/reading.js +32 -0
- package/dist/text/reading.js.map +1 -0
- package/dist/text/sentences.d.ts +11 -0
- package/dist/text/sentences.d.ts.map +1 -0
- package/dist/text/sentences.js +91 -0
- package/dist/text/sentences.js.map +1 -0
- package/dist/{text.d.ts → text/split.d.ts} +2 -2
- package/dist/text/split.d.ts.map +1 -0
- package/dist/{text.js → text/split.js} +2 -2
- package/dist/text/split.js.map +1 -0
- package/dist/types.d.ts +58 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/version.d.ts +4 -0
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +5 -1
- package/dist/version.js.map +1 -1
- package/dist/voicevox/client.d.ts +1 -1
- package/dist/voicevox/client.d.ts.map +1 -1
- package/dist/voicevox/dictionary.js +1 -1
- package/dist/voicevox/dictionary.js.map +1 -1
- package/dist/voicevox/song.js +1 -1
- package/dist/voicevox/song.js.map +1 -1
- package/dist/voicevox/speech.d.ts +1 -0
- package/dist/voicevox/speech.d.ts.map +1 -1
- package/dist/voicevox/speech.js +8 -2
- package/dist/voicevox/speech.js.map +1 -1
- package/dist/wav.d.ts +1 -0
- package/dist/wav.d.ts.map +1 -1
- package/dist/wav.js +8 -6
- package/dist/wav.js.map +1 -1
- package/docs/cli.md +261 -0
- package/examples/batch.jsonl +4 -3
- package/examples/builtin/dictionary.json +7 -0
- package/examples/builtin/song-compact.json +13 -0
- package/examples/builtin/song-mml.json +9 -0
- package/examples/builtin/song-notes.json +25 -0
- package/examples/builtin/speech-dictionary.json +9 -0
- package/examples/builtin/speech-kana.json +7 -0
- package/examples/builtin/speech.json +7 -0
- package/package.json +10 -4
- package/dist/formant.d.ts +0 -34
- package/dist/formant.d.ts.map +0 -1
- package/dist/formant.js +0 -622
- package/dist/formant.js.map +0 -1
- package/dist/mora.d.ts.map +0 -1
- package/dist/mora.js.map +0 -1
- package/dist/notation.d.ts +0 -15
- package/dist/notation.d.ts.map +0 -1
- package/dist/notation.js +0 -114
- package/dist/notation.js.map +0 -1
- package/dist/pitch.d.ts.map +0 -1
- package/dist/pitch.js.map +0 -1
- package/dist/score.d.ts +0 -37
- package/dist/score.d.ts.map +0 -1
- package/dist/score.js +0 -166
- package/dist/score.js.map +0 -1
- package/dist/text.d.ts.map +0 -1
- package/dist/text.js.map +0 -1
- package/examples/song-notes.json +0 -24
- /package/dist/{pitch.d.ts → song/pitch.d.ts} +0 -0
- /package/examples/{song.json → voicevox/song.json} +0 -0
- /package/examples/{speech-kana.json → voicevox/speech-kana.json} +0 -0
- /package/examples/{speech.json → voicevox/speech.json} +0 -0
package/dist/cli.js
CHANGED
|
@@ -1,276 +1,490 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
4
|
+
import { commandHelp, rootHelp } from "./cli/help.js";
|
|
5
|
+
import { ALLOWED, assign, checkAllowed, env, numberFlag, parseCommandLine, parseDictEntries, styleFlag, } from "./cli/flags.js";
|
|
6
|
+
import { DiskCache } from "./cache.js";
|
|
7
|
+
import { EXIT_CODES, KongyoroidError, asKongyoroidError, exitCodeOf, formatDiagnostic, invalid } from "./errors.js";
|
|
8
|
+
import { ensureReadableFile, existingFileSha256, readBytes, readChunks, readLines, readText, writeAudioIdempotent, writeStream, writeTextFile, writeWithBackpressure, } from "./io.js";
|
|
9
|
+
import { DEFAULT_SAMPLE_RATE, LIMITS } from "./limits.js";
|
|
8
10
|
import { Kongyoroid } from "./kongyoroid.js";
|
|
9
11
|
import { playWav } from "./player.js";
|
|
10
12
|
import { parseEngine, parseJson } from "./request.js";
|
|
11
|
-
import { BATCH_JOB_SCHEMA, CAPABILITIES, REQUEST_SCHEMA } from "./schema.js";
|
|
12
|
-
import {
|
|
13
|
+
import { BATCH_JOB_SCHEMA, CAPABILITIES, DICTIONARY_SCHEMA, REQUEST_SCHEMA } from "./schema.js";
|
|
14
|
+
import { pcm16Bytes, streamingWavHeader } from "./synth/stream.js";
|
|
15
|
+
import { LocalDictionary } from "./text/dictionary.js";
|
|
16
|
+
import { integer, isObject, keys, literal, object, string } from "./validate.js";
|
|
13
17
|
import { PACKAGE_NAME, VERSION } from "./version.js";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
Usage: kongyoroid <command> [flags]
|
|
17
|
-
|
|
18
|
-
speak --text "こんにちは" [--speaker ずんだもん] [-o out.wav] Read text aloud
|
|
19
|
-
sing --lyrics "ドレミ" --melody "C4 D4 E4" [--beats "1 1 2"] Sing a score
|
|
20
|
-
render --input request.json | - Render a full JSON request (see: schema)
|
|
21
|
-
batch --input jobs.jsonl --output-dir out Many renders, one JSONL line per job
|
|
22
|
-
reading --text "..." [--speaker ...] Show the engine's reading (kana notation)
|
|
23
|
-
voices [--kind speech|song|all] [--query 名前] List VOICEVOX styles (id, character, type)
|
|
24
|
-
doctor [--speaker ...] [--initialize] Check the engine, versions, style counts
|
|
25
|
-
dict list | add | update <uuid> | delete <uuid> Manage the VOICEVOX user dictionary
|
|
26
|
-
play <file.wav> Play a WAV with the system player
|
|
27
|
-
schema [--kind request|batch] JSON Schema of the request format
|
|
28
|
-
capabilities Machine-readable feature descriptor
|
|
29
|
-
|
|
30
|
-
Common flags
|
|
31
|
-
--engine formant|voicevox|auto (default formant; a speaker/singer/teacher selects voicevox)
|
|
32
|
-
--endpoint URL --timeout-ms N --retries N --cache-dir DIR
|
|
33
|
-
-o, --output FILE|- (default: kongyoroid-<kind>-<hash>.wav; "-" streams WAV to stdout, JSON to stderr)
|
|
34
|
-
--force replace an existing output file --play play the result after writing
|
|
35
|
-
|
|
36
|
-
speak flags: -t/--text, -i/--input FILE|- (text), --kana NOTATION, --speaker ID|NAME, --speed, --pitch,
|
|
37
|
-
--intonation, --volume, --pre-pause, --post-pause, --pause-length, --pause-scale, --no-upspeak,
|
|
38
|
-
--split sentence|paragraph|none, --sample-rate, --seed
|
|
39
|
-
sing flags: --lyrics, --melody, --beats, --tempo, --singer, --teacher, --transpose, --volume, --vibrato-depth,
|
|
40
|
-
--vibrato-rate, --lead-in, --lead-out, --sample-rate, --seed, -i/--input FILE|- (song JSON; flags override)
|
|
41
|
-
dict flags: --surface, --pronunciation, --accent N, --word-type PROPER_NOUN|COMMON_NOUN|VERB|ADJECTIVE|SUFFIX, --priority 0-10
|
|
42
|
-
|
|
43
|
-
Environment: KONGYOROID_ENDPOINT (or VOICEVOX_URL), KONGYOROID_ENGINE, KONGYOROID_SPEAKER, KONGYOROID_SINGER,
|
|
44
|
-
KONGYOROID_TEACHER, KONGYOROID_CACHE_DIR
|
|
45
|
-
Output: one JSON object per line on stdout ({ "ok": true, ... }); errors on stderr ({ "ok": false, "error": {...} }).
|
|
46
|
-
Exit codes: 0 ok, 1 internal, 2 input, 3 engine, 4 io, 130 cancelled.
|
|
47
|
-
`;
|
|
48
|
-
const OPTIONS = {
|
|
49
|
-
help: { type: "boolean", short: "h" },
|
|
50
|
-
version: { type: "boolean", short: "v" },
|
|
51
|
-
text: { type: "string", short: "t" },
|
|
52
|
-
input: { type: "string", short: "i" },
|
|
53
|
-
output: { type: "string", short: "o" },
|
|
54
|
-
"output-dir": { type: "string" },
|
|
55
|
-
force: { type: "boolean" },
|
|
56
|
-
play: { type: "boolean" },
|
|
57
|
-
engine: { type: "string" },
|
|
58
|
-
endpoint: { type: "string" },
|
|
59
|
-
"timeout-ms": { type: "string" },
|
|
60
|
-
retries: { type: "string" },
|
|
61
|
-
concurrency: { type: "string" },
|
|
62
|
-
"cache-dir": { type: "string" },
|
|
63
|
-
kana: { type: "string" },
|
|
64
|
-
speaker: { type: "string" },
|
|
65
|
-
speed: { type: "string" },
|
|
66
|
-
pitch: { type: "string" },
|
|
67
|
-
intonation: { type: "string" },
|
|
68
|
-
volume: { type: "string" },
|
|
69
|
-
"pre-pause": { type: "string" },
|
|
70
|
-
"post-pause": { type: "string" },
|
|
71
|
-
"pause-length": { type: "string" },
|
|
72
|
-
"pause-scale": { type: "string" },
|
|
73
|
-
upspeak: { type: "boolean" },
|
|
74
|
-
split: { type: "string" },
|
|
75
|
-
"sample-rate": { type: "string" },
|
|
76
|
-
seed: { type: "string" },
|
|
77
|
-
lyrics: { type: "string" },
|
|
78
|
-
melody: { type: "string" },
|
|
79
|
-
beats: { type: "string" },
|
|
80
|
-
tempo: { type: "string" },
|
|
81
|
-
singer: { type: "string" },
|
|
82
|
-
teacher: { type: "string" },
|
|
83
|
-
transpose: { type: "string" },
|
|
84
|
-
"vibrato-depth": { type: "string" },
|
|
85
|
-
"vibrato-rate": { type: "string" },
|
|
86
|
-
"lead-in": { type: "string" },
|
|
87
|
-
"lead-out": { type: "string" },
|
|
88
|
-
kind: { type: "string" },
|
|
89
|
-
query: { type: "string" },
|
|
90
|
-
initialize: { type: "boolean" },
|
|
91
|
-
surface: { type: "string" },
|
|
92
|
-
pronunciation: { type: "string" },
|
|
93
|
-
accent: { type: "string" },
|
|
94
|
-
"word-type": { type: "string" },
|
|
95
|
-
priority: { type: "string" },
|
|
96
|
-
};
|
|
97
|
-
const NETWORK = ["endpoint", "timeout-ms", "retries", "engine"];
|
|
98
|
-
const OUTPUT = ["output", "force", "play", "cache-dir", "concurrency"];
|
|
99
|
-
const SPEECH = [
|
|
100
|
-
"text",
|
|
101
|
-
"input",
|
|
102
|
-
"kana",
|
|
103
|
-
"speaker",
|
|
104
|
-
"speed",
|
|
105
|
-
"pitch",
|
|
106
|
-
"intonation",
|
|
107
|
-
"volume",
|
|
108
|
-
"pre-pause",
|
|
109
|
-
"post-pause",
|
|
110
|
-
"pause-length",
|
|
111
|
-
"pause-scale",
|
|
112
|
-
"upspeak",
|
|
113
|
-
"split",
|
|
114
|
-
"sample-rate",
|
|
115
|
-
"seed",
|
|
116
|
-
];
|
|
117
|
-
const SONG = [
|
|
118
|
-
"input",
|
|
119
|
-
"lyrics",
|
|
120
|
-
"melody",
|
|
121
|
-
"beats",
|
|
122
|
-
"tempo",
|
|
123
|
-
"singer",
|
|
124
|
-
"teacher",
|
|
125
|
-
"transpose",
|
|
126
|
-
"volume",
|
|
127
|
-
"vibrato-depth",
|
|
128
|
-
"vibrato-rate",
|
|
129
|
-
"lead-in",
|
|
130
|
-
"lead-out",
|
|
131
|
-
"sample-rate",
|
|
132
|
-
"seed",
|
|
133
|
-
];
|
|
134
|
-
const ALLOWED = {
|
|
135
|
-
speak: [...NETWORK, ...OUTPUT, ...SPEECH],
|
|
136
|
-
sing: [...NETWORK, ...OUTPUT, ...SONG],
|
|
137
|
-
render: [...NETWORK, ...OUTPUT, "input"],
|
|
138
|
-
batch: [...NETWORK, "input", "output-dir", "force", "concurrency", "cache-dir"],
|
|
139
|
-
reading: [...NETWORK, "text", "input", "kana", "speaker"],
|
|
140
|
-
voices: [...NETWORK, "kind", "query"],
|
|
141
|
-
doctor: [...NETWORK, "speaker", "initialize"],
|
|
142
|
-
dict: [...NETWORK, "surface", "pronunciation", "accent", "word-type", "priority"],
|
|
143
|
-
play: [],
|
|
144
|
-
schema: ["kind"],
|
|
145
|
-
capabilities: [],
|
|
146
|
-
};
|
|
18
|
+
import { wavHeader } from "./wav.js";
|
|
147
19
|
async function emit(value, error = false) {
|
|
148
20
|
await writeStream(error ? process.stderr : process.stdout, `${JSON.stringify(value)}\n`);
|
|
149
21
|
}
|
|
150
|
-
function
|
|
151
|
-
const raw = values[name];
|
|
152
|
-
if (raw === undefined || typeof raw !== "string")
|
|
153
|
-
return undefined;
|
|
154
|
-
const value = Number(raw.trim());
|
|
155
|
-
if (raw.trim().length === 0 || !Number.isFinite(value))
|
|
156
|
-
invalid(`$flags.${name}`, `Expected a number, got ${JSON.stringify(raw)}.`);
|
|
157
|
-
return value;
|
|
158
|
-
}
|
|
159
|
-
function styleFlag(values, name) {
|
|
160
|
-
const raw = values[name];
|
|
161
|
-
if (raw === undefined)
|
|
162
|
-
return undefined;
|
|
163
|
-
return /^\d+$/u.test(raw.trim()) ? Number(raw) : raw;
|
|
164
|
-
}
|
|
165
|
-
function assign(target, key, value) {
|
|
166
|
-
if (value !== undefined)
|
|
167
|
-
target[key] = value;
|
|
168
|
-
}
|
|
169
|
-
function env(name) {
|
|
170
|
-
const value = process.env[name];
|
|
171
|
-
return value === undefined || value.trim().length === 0 ? undefined : value.trim();
|
|
172
|
-
}
|
|
173
|
-
function metadata(result) {
|
|
22
|
+
function metadata(result, extraWarnings = []) {
|
|
174
23
|
return {
|
|
175
24
|
engine: result.engine,
|
|
25
|
+
engineVersion: result.engineVersion,
|
|
176
26
|
kind: result.kind,
|
|
27
|
+
...(result.voice === undefined ? {} : { voice: result.voice }),
|
|
177
28
|
styles: result.styles,
|
|
178
29
|
...(result.kana === undefined ? {} : { kana: result.kana }),
|
|
179
30
|
chunks: result.chunks,
|
|
180
31
|
cached: result.cached,
|
|
181
32
|
sha256: result.sha256,
|
|
33
|
+
requestHash: result.requestHash,
|
|
182
34
|
bytes: result.audio.length,
|
|
183
35
|
sampleRate: result.info.sampleRate,
|
|
184
36
|
channels: result.info.channels,
|
|
185
37
|
frames: result.info.frames,
|
|
186
38
|
durationSeconds: Math.round(result.info.durationSeconds * 1000) / 1000,
|
|
39
|
+
...(result.peak === undefined ? {} : { peak: result.peak, rms: result.rms, limitedSamples: result.limitedSamples }),
|
|
187
40
|
elapsedMs: result.elapsedMs,
|
|
41
|
+
timings: result.timings,
|
|
42
|
+
warnings: extraWarnings.length === 0 ? result.warnings : [...result.warnings, ...extraWarnings],
|
|
43
|
+
adjustments: result.adjustments,
|
|
188
44
|
};
|
|
189
45
|
}
|
|
190
|
-
|
|
46
|
+
const PLAN_UNAVAILABLE = {
|
|
47
|
+
severity: "warning",
|
|
48
|
+
code: "PLAN_UNAVAILABLE",
|
|
49
|
+
message: "--plan-out was ignored: only the formant engine produces a synthesis plan.",
|
|
50
|
+
help: "Render with --engine formant (and no VOICEVOX style) to write the plan.",
|
|
51
|
+
path: "$flags.plan-out",
|
|
52
|
+
};
|
|
53
|
+
async function reportDiagnostics(values, warnings, location) {
|
|
54
|
+
if (values.diagnostics !== "compact")
|
|
55
|
+
return;
|
|
56
|
+
for (const warning of warnings)
|
|
57
|
+
await writeStream(process.stderr, `${formatDiagnostic(warning, location)}\n`);
|
|
58
|
+
}
|
|
59
|
+
async function deliver(result, values, signal, plan) {
|
|
60
|
+
const planOut = values["plan-out"];
|
|
61
|
+
const extraWarnings = planOut !== undefined && plan === undefined ? [PLAN_UNAVAILABLE] : [];
|
|
62
|
+
if (planOut !== undefined && planOut !== "-" && plan !== undefined) {
|
|
63
|
+
await writeTextFile(planOut, `${JSON.stringify(plan, null, 2)}\n`, values.force ?? false);
|
|
64
|
+
}
|
|
65
|
+
await reportDiagnostics(values, [...result.warnings, ...extraWarnings], values.input ?? "<request>");
|
|
191
66
|
if (values.output === "-") {
|
|
192
67
|
if (values.play)
|
|
193
|
-
invalid("$flags.play", "--play cannot be combined with --output -."
|
|
68
|
+
invalid("$flags.play", "--play cannot be combined with --output -.", {
|
|
69
|
+
hint: "Write to a file and play it: -o out.wav --play",
|
|
70
|
+
});
|
|
194
71
|
await writeStream(process.stdout, result.audio);
|
|
195
|
-
return {
|
|
72
|
+
return {
|
|
73
|
+
ok: true,
|
|
74
|
+
output: "stdout",
|
|
75
|
+
...metadata(result, extraWarnings),
|
|
76
|
+
...(plan === undefined || planOut !== "-" ? {} : { plan }),
|
|
77
|
+
};
|
|
196
78
|
}
|
|
197
|
-
const path = values.output ?? `kongyoroid-${result.kind}-${result.sha256.slice(0,
|
|
198
|
-
const written = await
|
|
199
|
-
const played = values.play ? await playWav(written, signal) : undefined;
|
|
200
|
-
return {
|
|
79
|
+
const path = values.output ?? `kongyoroid-${result.kind}-${result.sha256.slice(0, 12)}.wav`;
|
|
80
|
+
const written = await writeAudioIdempotent(path, result.audio, values.force ?? false, signal);
|
|
81
|
+
const played = values.play ? await playWav(written.path, signal) : undefined;
|
|
82
|
+
return {
|
|
83
|
+
ok: true,
|
|
84
|
+
path: written.path,
|
|
85
|
+
written: written.written,
|
|
86
|
+
unchanged: written.unchanged,
|
|
87
|
+
...metadata(result, extraWarnings),
|
|
88
|
+
...(played === undefined ? {} : { player: played.player }),
|
|
89
|
+
...(plan === undefined || planOut !== "-" ? {} : { plan }),
|
|
90
|
+
};
|
|
201
91
|
}
|
|
202
92
|
function speechRequestFromFlags(values, text) {
|
|
203
|
-
|
|
93
|
+
return withSpeechFlags(values, { kind: "speech", text });
|
|
94
|
+
}
|
|
95
|
+
function assignExclusive(request, values, first, second) {
|
|
96
|
+
const a = numberFlag(values, first.flag);
|
|
97
|
+
const b = numberFlag(values, second.flag);
|
|
98
|
+
if (a !== undefined && b !== undefined)
|
|
99
|
+
invalid(`$flags.${second.flag}`, `Use either --${first.flag} or --${second.flag}, not both.`);
|
|
100
|
+
if (a !== undefined) {
|
|
101
|
+
request[first.key] = a;
|
|
102
|
+
delete request[second.key];
|
|
103
|
+
}
|
|
104
|
+
if (b !== undefined) {
|
|
105
|
+
request[second.key] = b;
|
|
106
|
+
delete request[first.key];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function withSpeechFlags(values, base) {
|
|
110
|
+
const request = { ...base, kind: "speech" };
|
|
204
111
|
assign(request, "engine", values.engine);
|
|
112
|
+
assign(request, "voice", values.voice);
|
|
205
113
|
assign(request, "kana", values.kana);
|
|
114
|
+
const entries = parseDictEntries(values);
|
|
115
|
+
if (entries.length > 0) {
|
|
116
|
+
const existing = Array.isArray(base["dictionary"]) ? base["dictionary"] : [];
|
|
117
|
+
request["dictionary"] = [...existing, ...entries];
|
|
118
|
+
}
|
|
119
|
+
if (values["strict-reading"] === false)
|
|
120
|
+
request["strictReading"] = false;
|
|
206
121
|
assign(request, "speaker", styleFlag(values, "speaker"));
|
|
207
122
|
assign(request, "speed", numberFlag(values, "speed"));
|
|
208
|
-
|
|
123
|
+
assignExclusive(request, values, { flag: "pitch", key: "pitch" }, { flag: "pitch-semitones", key: "pitchSemitones" });
|
|
209
124
|
assign(request, "intonation", numberFlag(values, "intonation"));
|
|
210
|
-
|
|
125
|
+
assignExclusive(request, values, { flag: "volume", key: "volume" }, { flag: "gain-db", key: "gainDb" });
|
|
126
|
+
assign(request, "breathiness", numberFlag(values, "breathiness"));
|
|
211
127
|
assign(request, "prePause", numberFlag(values, "pre-pause"));
|
|
212
128
|
assign(request, "postPause", numberFlag(values, "post-pause"));
|
|
213
129
|
assign(request, "pauseLength", numberFlag(values, "pause-length"));
|
|
214
130
|
assign(request, "pauseScale", numberFlag(values, "pause-scale"));
|
|
215
|
-
|
|
131
|
+
if (values.upspeak !== undefined)
|
|
132
|
+
request["upspeak"] = values.upspeak;
|
|
216
133
|
assign(request, "split", values.split);
|
|
217
134
|
assign(request, "sampleRate", numberFlag(values, "sample-rate"));
|
|
218
135
|
assign(request, "seed", numberFlag(values, "seed"));
|
|
219
136
|
return request;
|
|
220
137
|
}
|
|
138
|
+
function applyVibratoFlags(request, values, base) {
|
|
139
|
+
const depth = numberFlag(values, "vibrato-depth");
|
|
140
|
+
const rate = numberFlag(values, "vibrato-rate");
|
|
141
|
+
const delay = numberFlag(values, "vibrato-delay-ms");
|
|
142
|
+
const fade = numberFlag(values, "vibrato-fade-ms");
|
|
143
|
+
const existing = isObject(base["vibrato"]) ? base["vibrato"] : undefined;
|
|
144
|
+
if (existing === undefined && delay === undefined && fade === undefined) {
|
|
145
|
+
assign(request, "vibratoDepth", depth);
|
|
146
|
+
assign(request, "vibratoRate", rate);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const vibrato = { ...existing };
|
|
150
|
+
if (existing === undefined) {
|
|
151
|
+
assign(vibrato, "depthCents", base["vibratoDepth"]);
|
|
152
|
+
assign(vibrato, "rateHz", base["vibratoRate"]);
|
|
153
|
+
delete request["vibratoDepth"];
|
|
154
|
+
delete request["vibratoRate"];
|
|
155
|
+
}
|
|
156
|
+
assign(vibrato, "depthCents", depth);
|
|
157
|
+
assign(vibrato, "rateHz", rate);
|
|
158
|
+
assign(vibrato, "delayMs", delay);
|
|
159
|
+
assign(vibrato, "fadeMs", fade);
|
|
160
|
+
request["vibrato"] = vibrato;
|
|
161
|
+
}
|
|
221
162
|
function songRequestFromFlags(values, base) {
|
|
222
163
|
const request = { ...base, kind: "song" };
|
|
223
|
-
if (values.
|
|
224
|
-
|
|
164
|
+
if (values.mml !== undefined) {
|
|
165
|
+
if (values.melody !== undefined || values.beats !== undefined)
|
|
166
|
+
invalid("$flags.mml", "--mml cannot be combined with --melody or --beats.");
|
|
167
|
+
request["notes"] = { mml: values.mml, ...(values.lyrics === undefined ? {} : { lyrics: values.lyrics }) };
|
|
168
|
+
}
|
|
169
|
+
else if (values.lyrics !== undefined || values.melody !== undefined) {
|
|
170
|
+
if (values.melody === undefined)
|
|
171
|
+
invalid("$flags.melody", "--lyrics needs --melody (or use --mml).", {
|
|
172
|
+
hint: 'Example: --lyrics "ドレミ" --melody "C4 D4 E4"',
|
|
173
|
+
});
|
|
174
|
+
const notes = { lyrics: values.lyrics ?? "", melody: values.melody };
|
|
225
175
|
assign(notes, "beats", values.beats);
|
|
226
176
|
request["notes"] = notes;
|
|
227
177
|
}
|
|
228
178
|
else if (values.beats !== undefined) {
|
|
229
|
-
invalid("$flags.beats", "--beats needs --melody."
|
|
179
|
+
invalid("$flags.beats", "--beats needs --melody.", {
|
|
180
|
+
hint: 'Example: --lyrics "ドレミ" --melody "C4 D4 E4" --beats "1 1 2"',
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if (request["notes"] === undefined) {
|
|
184
|
+
invalid(values.input === undefined ? "$flags.melody" : "$.notes", "A song needs notes: give --lyrics with --melody, --mml, or --input FILE containing a notes list.", {
|
|
185
|
+
hint: 'Examples: kongyoroid sing --lyrics "ドレミ" --melody "C4 D4 E4" | kongyoroid sing --mml "t120 o4 c d e" | kongyoroid sing --input song.json',
|
|
186
|
+
});
|
|
230
187
|
}
|
|
231
188
|
assign(request, "engine", values.engine);
|
|
189
|
+
assign(request, "voice", values.voice);
|
|
232
190
|
assign(request, "tempo", numberFlag(values, "tempo"));
|
|
233
191
|
assign(request, "singer", styleFlag(values, "singer"));
|
|
234
192
|
assign(request, "teacher", styleFlag(values, "teacher"));
|
|
235
193
|
assign(request, "transpose", numberFlag(values, "transpose"));
|
|
236
|
-
|
|
237
|
-
assign(request, "
|
|
238
|
-
|
|
194
|
+
assignExclusive(request, values, { flag: "volume", key: "volume" }, { flag: "gain-db", key: "gainDb" });
|
|
195
|
+
assign(request, "breathiness", numberFlag(values, "breathiness"));
|
|
196
|
+
applyVibratoFlags(request, values, base);
|
|
197
|
+
assign(request, "portamentoMs", numberFlag(values, "portamento-ms"));
|
|
198
|
+
assign(request, "scoopCents", numberFlag(values, "scoop-cents"));
|
|
199
|
+
assign(request, "scoopMs", numberFlag(values, "scoop-ms"));
|
|
200
|
+
if (values["consonant-compression"] !== undefined)
|
|
201
|
+
request["consonantCompression"] = values["consonant-compression"];
|
|
239
202
|
assign(request, "leadIn", numberFlag(values, "lead-in"));
|
|
240
203
|
assign(request, "leadOut", numberFlag(values, "lead-out"));
|
|
241
204
|
assign(request, "sampleRate", numberFlag(values, "sample-rate"));
|
|
242
205
|
assign(request, "seed", numberFlag(values, "seed"));
|
|
243
206
|
return request;
|
|
244
207
|
}
|
|
245
|
-
|
|
208
|
+
function rejectTextAndInput(values) {
|
|
246
209
|
if (values.text !== undefined && values.input !== undefined)
|
|
247
210
|
invalid("$flags", "Use either --text or --input, not both.");
|
|
211
|
+
}
|
|
212
|
+
async function textInput(values, signal) {
|
|
213
|
+
rejectTextAndInput(values);
|
|
248
214
|
if (values.text !== undefined)
|
|
249
215
|
return values.text;
|
|
250
216
|
if (values.input !== undefined)
|
|
251
217
|
return (await readText(values.input, signal)).replace(/\r?\n$/u, "");
|
|
252
|
-
|
|
218
|
+
if (values.kana !== undefined)
|
|
219
|
+
return values.kana;
|
|
220
|
+
return invalid("$flags.text", "Provide --text or --input FILE (use - for standard input).", {
|
|
221
|
+
hint: 'Example: kongyoroid speak --text "こんにちは" -o hello.wav',
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
function overlayRequest(values, json) {
|
|
225
|
+
if (isObject(json) && json["kind"] === "song")
|
|
226
|
+
return songRequestFromFlags(values, json);
|
|
227
|
+
if (isObject(json) && json["kind"] === "speech" && typeof json["text"] === "string")
|
|
228
|
+
return withSpeechFlags(values, json);
|
|
229
|
+
if (isObject(json)) {
|
|
230
|
+
const overlay = { ...json };
|
|
231
|
+
assign(overlay, "engine", values.engine);
|
|
232
|
+
assign(overlay, "voice", values.voice);
|
|
233
|
+
return overlay;
|
|
234
|
+
}
|
|
235
|
+
return json;
|
|
253
236
|
}
|
|
254
|
-
function
|
|
237
|
+
async function requestFromInputs(values, signal, command) {
|
|
238
|
+
rejectTextAndInput(values);
|
|
239
|
+
const hasSpeech = values.text !== undefined || values.kana !== undefined;
|
|
240
|
+
const hasSong = values.lyrics !== undefined || values.melody !== undefined || values.mml !== undefined;
|
|
241
|
+
if (values.input !== undefined) {
|
|
242
|
+
const raw = await readText(values.input, signal);
|
|
243
|
+
const looksLikeJson = raw.trim().startsWith("{");
|
|
244
|
+
if (!looksLikeJson && !hasSong)
|
|
245
|
+
return speechRequestFromFlags(values, raw.replace(/\r?\n$/u, ""));
|
|
246
|
+
return overlayRequest(values, parseJson(raw));
|
|
247
|
+
}
|
|
248
|
+
if (hasSong)
|
|
249
|
+
return songRequestFromFlags(values, {});
|
|
250
|
+
if (hasSpeech)
|
|
251
|
+
return speechRequestFromFlags(values, await textInput(values, signal));
|
|
252
|
+
const scoreHint = command === "speak" ? "" : ", or --lyrics/--melody/--mml";
|
|
253
|
+
return invalid("$flags.input", `Provide --input FILE|- (text or a request JSON) or --text${scoreHint} for ${command}.`, {
|
|
254
|
+
hint: `Examples: kongyoroid ${command} --text "こんにちは" | kongyoroid ${command} --input request.json`,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
async function loadDictionaryFile(path, tolerateMissing) {
|
|
258
|
+
if (path === undefined)
|
|
259
|
+
return undefined;
|
|
260
|
+
try {
|
|
261
|
+
return await LocalDictionary.load(path);
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
if (tolerateMissing &&
|
|
265
|
+
error instanceof KongyoroidError &&
|
|
266
|
+
error.code === "IO_ERROR" &&
|
|
267
|
+
/ENOENT/u.test(error.message))
|
|
268
|
+
return undefined;
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
async function createAgent(values, tolerateMissingDictionary = false) {
|
|
255
273
|
const engineFlag = values.engine ?? env("KONGYOROID_ENGINE");
|
|
256
274
|
const concurrency = numberFlag(values, "concurrency");
|
|
257
275
|
const timeoutMs = numberFlag(values, "timeout-ms");
|
|
258
276
|
const retries = numberFlag(values, "retries");
|
|
259
277
|
const cacheDir = values["cache-dir"] ?? env("KONGYOROID_CACHE_DIR");
|
|
278
|
+
const voice = values.voice ?? env("KONGYOROID_VOICE");
|
|
260
279
|
const speaker = env("KONGYOROID_SPEAKER");
|
|
261
280
|
const singer = env("KONGYOROID_SINGER");
|
|
262
281
|
const teacher = env("KONGYOROID_TEACHER");
|
|
263
|
-
|
|
282
|
+
const dictionaryPath = values.dictionary ?? env("KONGYOROID_DICTIONARY");
|
|
283
|
+
const dictionary = await loadDictionaryFile(dictionaryPath, tolerateMissingDictionary);
|
|
284
|
+
const options = {
|
|
264
285
|
endpoint: values.endpoint ?? env("KONGYOROID_ENDPOINT") ?? env("VOICEVOX_URL") ?? "http://127.0.0.1:50021",
|
|
265
|
-
...(engineFlag === undefined ? {} : { engine: parseEngine(engineFlag, "$flags.engine") }),
|
|
286
|
+
...(engineFlag === undefined || engineFlag === "all" ? {} : { engine: parseEngine(engineFlag, "$flags.engine") }),
|
|
287
|
+
...(voice === undefined ? {} : { voice }),
|
|
266
288
|
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
267
289
|
...(retries === undefined ? {} : { retries }),
|
|
268
290
|
...(concurrency === undefined ? {} : { concurrency }),
|
|
269
|
-
...(
|
|
291
|
+
...(dictionary === undefined ? {} : { dictionary }),
|
|
270
292
|
...(speaker === undefined ? {} : { speaker }),
|
|
271
293
|
...(singer === undefined ? {} : { singer }),
|
|
272
294
|
...(teacher === undefined ? {} : { teacher }),
|
|
273
|
-
|
|
295
|
+
cache: {
|
|
296
|
+
...(cacheDir === undefined ? {} : { directory: cacheDir }),
|
|
297
|
+
...(values.cache === false ? { enabled: false } : {}),
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
return new Kongyoroid(options);
|
|
301
|
+
}
|
|
302
|
+
function planDetail(values) {
|
|
303
|
+
return literal(values.detail ?? "summary", "$flags.detail", ["summary", "phonemes", "acoustics"]);
|
|
304
|
+
}
|
|
305
|
+
async function runValidate(agent, values, signal) {
|
|
306
|
+
const request = await requestFromInputs(values, signal, "validate");
|
|
307
|
+
const result = await agent.validate(request, { signal });
|
|
308
|
+
if (values.diagnostics === "compact") {
|
|
309
|
+
for (const warning of result.warnings)
|
|
310
|
+
await writeStream(process.stdout, `${formatDiagnostic(warning, values.input ?? "<request>")}\n`);
|
|
311
|
+
if (result.warnings.length === 0)
|
|
312
|
+
await emit({ ok: true, operation: "validate", renderable: result.renderable, requestHash: result.requestHash });
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const { request: resolved, ...rest } = result;
|
|
316
|
+
await emit({ ...rest, request: resolved });
|
|
317
|
+
}
|
|
318
|
+
async function runPlan(agent, values, signal) {
|
|
319
|
+
const request = await requestFromInputs(values, signal, "plan");
|
|
320
|
+
const result = await agent.plan(request, { signal, detail: planDetail(values) });
|
|
321
|
+
await emit(result);
|
|
322
|
+
}
|
|
323
|
+
async function* single(text) {
|
|
324
|
+
yield text;
|
|
325
|
+
}
|
|
326
|
+
async function* prepend(head, iterator) {
|
|
327
|
+
for (const chunk of head)
|
|
328
|
+
yield chunk;
|
|
329
|
+
try {
|
|
330
|
+
while (true) {
|
|
331
|
+
const next = await iterator.next();
|
|
332
|
+
if (next.done === true)
|
|
333
|
+
return;
|
|
334
|
+
yield next.value;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
finally {
|
|
338
|
+
await iterator.return?.();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
async function streamInput(values, signal) {
|
|
342
|
+
const fromFlags = () => {
|
|
343
|
+
const request = speechRequestFromFlags(values, "placeholder");
|
|
344
|
+
delete request["text"];
|
|
345
|
+
return request;
|
|
346
|
+
};
|
|
347
|
+
if (values.text !== undefined)
|
|
348
|
+
return { request: fromFlags(), source: single(values.text) };
|
|
349
|
+
const iterator = readChunks(values.input ?? "-", signal)[Symbol.asyncIterator]();
|
|
350
|
+
const head = [];
|
|
351
|
+
let leading = "";
|
|
352
|
+
while (leading.trim().length === 0) {
|
|
353
|
+
const next = await iterator.next();
|
|
354
|
+
if (next.done === true)
|
|
355
|
+
break;
|
|
356
|
+
head.push(next.value);
|
|
357
|
+
leading += next.value;
|
|
358
|
+
}
|
|
359
|
+
if (!leading.trimStart().startsWith("{"))
|
|
360
|
+
return { request: fromFlags(), source: prepend(head, iterator) };
|
|
361
|
+
for (let next = await iterator.next(); next.done !== true; next = await iterator.next())
|
|
362
|
+
head.push(next.value);
|
|
363
|
+
const json = object(parseJson(head.join("")), "$");
|
|
364
|
+
if (json["kind"] !== undefined && json["kind"] !== "speech")
|
|
365
|
+
invalid("$.kind", "speak accepts only speech requests; use sing or render for songs.");
|
|
366
|
+
const text = json["text"];
|
|
367
|
+
if (typeof text !== "string")
|
|
368
|
+
invalid("$.text", "A request JSON given to --stream needs a text string to stream.", {
|
|
369
|
+
hint: 'Example: {"kind":"speech","text":"一文目。二文目。","voice":"soft"}',
|
|
370
|
+
});
|
|
371
|
+
const request = withSpeechFlags(values, json);
|
|
372
|
+
delete request["text"];
|
|
373
|
+
return { request, source: single(text) };
|
|
374
|
+
}
|
|
375
|
+
async function runSpeakStream(agent, values, signal) {
|
|
376
|
+
if (values.play)
|
|
377
|
+
invalid("$flags.play", "--play cannot be combined with --stream.");
|
|
378
|
+
if (values.kana !== undefined)
|
|
379
|
+
invalid("$flags.kana", "--kana cannot be combined with --stream; streaming reads each sentence from the text.", {
|
|
380
|
+
hint: "Give readings with --dict-entry SURFACE=READING[:ACCENT], or drop --stream to speak the kana at once.",
|
|
381
|
+
});
|
|
382
|
+
rejectTextAndInput(values);
|
|
383
|
+
const format = literal(values.format ?? "wav", "$flags.format", ["wav", "pcm", "ndjson"]);
|
|
384
|
+
const output = values.output ?? "-";
|
|
385
|
+
if (output !== "-" && format !== "wav")
|
|
386
|
+
invalid("$flags.format", "Streaming to a file writes WAV; use --output - for pcm or ndjson.");
|
|
387
|
+
const flushRaw = numberFlag(values, "flush-ms");
|
|
388
|
+
const flushMs = flushRaw === undefined ? undefined : integer(flushRaw, "$flags.flush-ms", 1, 600_000);
|
|
389
|
+
const { request: input, source } = await streamInput(values, signal);
|
|
390
|
+
const events = agent.speakStream(source, input, { signal, ...(flushMs === undefined ? {} : { flushMs }) });
|
|
391
|
+
const stdout = process.stdout;
|
|
392
|
+
let sampleRate = 0;
|
|
393
|
+
let file;
|
|
394
|
+
let fileFrames = 0;
|
|
395
|
+
const filePath = output === "-" ? "" : resolve(output);
|
|
396
|
+
const sentences = [];
|
|
397
|
+
const openFile = async () => {
|
|
398
|
+
if (output === "-" || file !== undefined)
|
|
399
|
+
return;
|
|
400
|
+
const { open } = await import("node:fs/promises");
|
|
401
|
+
if (!values.force) {
|
|
402
|
+
const existing = await existingFileSha256(filePath);
|
|
403
|
+
if (existing !== undefined)
|
|
404
|
+
invalid("$flags.output", `Refusing to overwrite ${filePath}.`, {
|
|
405
|
+
hint: "Pass --force or choose another path.",
|
|
406
|
+
repairOptions: [{ action: "use-force", description: "Overwrite with --force." }],
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
if (sampleRate === 0)
|
|
410
|
+
sampleRate = numberFlag(values, "sample-rate") ?? DEFAULT_SAMPLE_RATE;
|
|
411
|
+
file = await open(filePath, "w", 0o644);
|
|
412
|
+
await file.write(wavHeader(0, sampleRate));
|
|
413
|
+
};
|
|
414
|
+
let headerWritten = false;
|
|
415
|
+
let completed = false;
|
|
416
|
+
try {
|
|
417
|
+
for await (const event of events) {
|
|
418
|
+
if (event.type === "sentence") {
|
|
419
|
+
const line = {
|
|
420
|
+
type: "sentence",
|
|
421
|
+
index: event.index,
|
|
422
|
+
text: event.text,
|
|
423
|
+
kana: event.kana,
|
|
424
|
+
durationSeconds: Math.round(event.durationSeconds * 1000) / 1000,
|
|
425
|
+
startSeconds: sampleRate === 0 ? 0 : Math.round((event.startFrame / sampleRate) * 1000) / 1000,
|
|
426
|
+
warnings: event.warnings,
|
|
427
|
+
};
|
|
428
|
+
sentences.push(line);
|
|
429
|
+
if (format === "ndjson" && output === "-")
|
|
430
|
+
await writeWithBackpressure(stdout, `${JSON.stringify(line)}\n`, signal);
|
|
431
|
+
else if (values.progress)
|
|
432
|
+
await emit(line, true);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (event.type === "audio") {
|
|
436
|
+
const block = event.block;
|
|
437
|
+
if (sampleRate === 0) {
|
|
438
|
+
sampleRate = block.sampleRate;
|
|
439
|
+
await openFile();
|
|
440
|
+
}
|
|
441
|
+
const bytes = pcm16Bytes(block.samples);
|
|
442
|
+
if (file !== undefined) {
|
|
443
|
+
await file.write(bytes);
|
|
444
|
+
fileFrames += block.samples.length;
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (format === "ndjson") {
|
|
448
|
+
await writeWithBackpressure(stdout, `${JSON.stringify({ type: "audio", sequence: event.sequence, sentence: event.index, sampleRate, encoding: "s16le", channels: 1, frames: block.samples.length, startFrame: block.start, base64: Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64") })}\n`, signal);
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (format === "wav" && !headerWritten) {
|
|
452
|
+
await writeWithBackpressure(stdout, streamingWavHeader(sampleRate), signal);
|
|
453
|
+
headerWritten = true;
|
|
454
|
+
}
|
|
455
|
+
await writeWithBackpressure(stdout, bytes, signal);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
completed = true;
|
|
459
|
+
if (output !== "-")
|
|
460
|
+
await openFile();
|
|
461
|
+
const summary = {
|
|
462
|
+
ok: true,
|
|
463
|
+
operation: "speak",
|
|
464
|
+
stream: true,
|
|
465
|
+
format,
|
|
466
|
+
output: output === "-" ? "stdout" : filePath,
|
|
467
|
+
...(output === "-" ? {} : { written: true }),
|
|
468
|
+
sentences: event.sentences,
|
|
469
|
+
frames: event.frames,
|
|
470
|
+
durationSeconds: Math.round(event.durationSeconds * 1000) / 1000,
|
|
471
|
+
sampleRate: sampleRate === 0 ? null : sampleRate,
|
|
472
|
+
...(values.progress || format === "ndjson" ? {} : { segments: sentences }),
|
|
473
|
+
};
|
|
474
|
+
if (format === "ndjson" && output === "-")
|
|
475
|
+
await writeWithBackpressure(stdout, `${JSON.stringify({ type: "end", ...summary })}\n`, signal);
|
|
476
|
+
else
|
|
477
|
+
await emit(summary, output === "-");
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
finally {
|
|
481
|
+
if (file !== undefined) {
|
|
482
|
+
const header = wavHeader(fileFrames, sampleRate);
|
|
483
|
+
await file.write(header, 0, 44, 0);
|
|
484
|
+
await file.close();
|
|
485
|
+
}
|
|
486
|
+
void completed;
|
|
487
|
+
}
|
|
274
488
|
}
|
|
275
489
|
async function runBatch(agent, values, signal, onFailure) {
|
|
276
490
|
const input = string(values.input, "$flags.input", 1, 4096);
|
|
@@ -279,6 +493,7 @@ async function runBatch(agent, values, signal, onFailure) {
|
|
|
279
493
|
const active = new Set();
|
|
280
494
|
const ids = new Set();
|
|
281
495
|
let line = 0;
|
|
496
|
+
let stop = false;
|
|
282
497
|
const run = async (text, index) => {
|
|
283
498
|
let id = null;
|
|
284
499
|
try {
|
|
@@ -291,12 +506,22 @@ async function runBatch(agent, values, signal, onFailure) {
|
|
|
291
506
|
invalid(`$line[${index}].id`, "Duplicate job id (case-insensitive).");
|
|
292
507
|
ids.add(id.toLowerCase());
|
|
293
508
|
const result = await agent.render(job["request"], { signal });
|
|
294
|
-
const
|
|
295
|
-
await emit({
|
|
509
|
+
const written = await writeAudioIdempotent(resolve(directory, `${id}.wav`), result.audio, values.force ?? false, signal);
|
|
510
|
+
await emit({
|
|
511
|
+
ok: true,
|
|
512
|
+
id,
|
|
513
|
+
line: index,
|
|
514
|
+
path: written.path,
|
|
515
|
+
written: written.written,
|
|
516
|
+
unchanged: written.unchanged,
|
|
517
|
+
...metadata(result),
|
|
518
|
+
});
|
|
296
519
|
}
|
|
297
520
|
catch (error) {
|
|
298
521
|
const failure = asKongyoroidError(error);
|
|
299
522
|
onFailure();
|
|
523
|
+
if (values["stop-on-error"])
|
|
524
|
+
stop = true;
|
|
300
525
|
process.exitCode = Math.max(Number(process.exitCode ?? 0), exitCodeOf(failure));
|
|
301
526
|
await emit({ ok: false, id, line: index, error: failure.toJSON() });
|
|
302
527
|
if (failure.code === "ABORTED")
|
|
@@ -305,6 +530,8 @@ async function runBatch(agent, values, signal, onFailure) {
|
|
|
305
530
|
};
|
|
306
531
|
try {
|
|
307
532
|
for await (const text of readLines(input, signal)) {
|
|
533
|
+
if (stop)
|
|
534
|
+
break;
|
|
308
535
|
line += 1;
|
|
309
536
|
if (line > LIMITS.batchLines)
|
|
310
537
|
invalid("$flags.input", `A batch is limited to ${LIMITS.batchLines} lines.`);
|
|
@@ -321,69 +548,231 @@ async function runBatch(agent, values, signal, onFailure) {
|
|
|
321
548
|
}
|
|
322
549
|
}
|
|
323
550
|
async function runDict(agent, values, positionals, signal) {
|
|
324
|
-
const action = literal(positionals[1], "$.dict", ["list", "add", "update", "delete"]);
|
|
551
|
+
const action = literal(positionals[1], "$.dict", ["list", "add", "update", "delete", "check"]);
|
|
552
|
+
const scope = literal(values.scope ?? "local", "$flags.scope", ["local", "voicevox"]);
|
|
553
|
+
const reading = values.reading ?? values.pronunciation;
|
|
554
|
+
if (scope === "voicevox") {
|
|
555
|
+
if (action === "check")
|
|
556
|
+
invalid("$flags.scope", "dict check is only available for the local dictionary.");
|
|
557
|
+
if (action === "list") {
|
|
558
|
+
await emit({ ok: true, operation: "dict.list", scope, words: await agent.voicevoxDictionary.list({ signal }) });
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (action === "delete") {
|
|
562
|
+
const uuid = string(values.id ?? positionals[2], "$flags.id", 1, 200);
|
|
563
|
+
if (values["dry-run"]) {
|
|
564
|
+
await emit({ ok: true, operation: "dict.delete", scope, dryRun: true, uuid });
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
await agent.voicevoxDictionary.remove(uuid, { signal });
|
|
568
|
+
await emit({ ok: true, operation: "dict.delete", scope, deleted: uuid });
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const priority = numberFlag(values, "priority");
|
|
572
|
+
const draft = {
|
|
573
|
+
surface: string(values.surface, "$flags.surface", 1, 200),
|
|
574
|
+
pronunciation: string(reading, "$flags.reading", 1, 200),
|
|
575
|
+
accentType: numberFlag(values, "accent") ?? invalid("$flags.accent", "Provide --accent N (0 = flat)."),
|
|
576
|
+
...(values["word-type"] === undefined ? {} : { wordType: values["word-type"] }),
|
|
577
|
+
...(priority === undefined ? {} : { priority }),
|
|
578
|
+
};
|
|
579
|
+
if (action === "add") {
|
|
580
|
+
if (values["dry-run"]) {
|
|
581
|
+
await emit({ ok: true, operation: "dict.add", scope, dryRun: true, word: draft });
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const uuid = await agent.voicevoxDictionary.add(draft, { signal });
|
|
585
|
+
await emit({ ok: true, operation: "dict.add", scope, uuid, word: draft });
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
const uuid = string(values.id ?? positionals[2], "$flags.id", 1, 200);
|
|
589
|
+
await agent.voicevoxDictionary.update(uuid, draft, { signal });
|
|
590
|
+
await emit({ ok: true, operation: "dict.update", scope, updated: uuid, word: draft });
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const path = values.dictionary ?? env("KONGYOROID_DICTIONARY") ?? "kongyoroid-dictionary.json";
|
|
594
|
+
const dictionary = (await loadDictionaryFile(path, true)) ?? new LocalDictionary();
|
|
325
595
|
if (action === "list") {
|
|
326
|
-
await emit({
|
|
596
|
+
await emit({
|
|
597
|
+
ok: true,
|
|
598
|
+
operation: "dict.list",
|
|
599
|
+
scope,
|
|
600
|
+
path: resolve(path),
|
|
601
|
+
entries: dictionary.list(),
|
|
602
|
+
digest: dictionary.digest(),
|
|
603
|
+
});
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
if (action === "check") {
|
|
607
|
+
const text = await textInput(values, signal);
|
|
608
|
+
const merged = agent.dictionary.merge(dictionary);
|
|
609
|
+
const analysed = await agent.reading(text, { signal, dictionary: merged, engine: "formant", strict: false });
|
|
610
|
+
await emit({
|
|
611
|
+
ok: true,
|
|
612
|
+
operation: "dict.check",
|
|
613
|
+
scope,
|
|
614
|
+
path: resolve(path),
|
|
615
|
+
text,
|
|
616
|
+
kana: analysed.kana,
|
|
617
|
+
hits: analysed.dictionaryHits,
|
|
618
|
+
phrases: analysed.phrases.map((p) => ({ text: p.text, accent: p.accent, accentSource: p.accentSource })),
|
|
619
|
+
warnings: analysed.warnings,
|
|
620
|
+
});
|
|
327
621
|
return;
|
|
328
622
|
}
|
|
329
623
|
if (action === "delete") {
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
624
|
+
const id = string(values.id ?? positionals[2], "$flags.id", 1, 100);
|
|
625
|
+
const entry = dictionary.get(id);
|
|
626
|
+
if (entry === undefined) {
|
|
627
|
+
await emit({
|
|
628
|
+
ok: true,
|
|
629
|
+
operation: "dict.delete",
|
|
630
|
+
scope,
|
|
631
|
+
path: resolve(path),
|
|
632
|
+
id,
|
|
633
|
+
deleted: false,
|
|
634
|
+
message: "No such entry: nothing to do.",
|
|
635
|
+
});
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (values["dry-run"]) {
|
|
639
|
+
await emit({ ok: true, operation: "dict.delete", scope, path: resolve(path), dryRun: true, entry });
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
dictionary.remove(id);
|
|
643
|
+
await writeTextFile(path, `${JSON.stringify(dictionary.toJSON(), null, 2)}\n`, true);
|
|
644
|
+
await emit({
|
|
645
|
+
ok: true,
|
|
646
|
+
operation: "dict.delete",
|
|
647
|
+
scope,
|
|
648
|
+
path: resolve(path),
|
|
649
|
+
id,
|
|
650
|
+
deleted: true,
|
|
651
|
+
digest: dictionary.digest(),
|
|
652
|
+
});
|
|
333
653
|
return;
|
|
334
654
|
}
|
|
655
|
+
const surface = string(values.surface, "$flags.surface", 1, 200);
|
|
656
|
+
const accent = numberFlag(values, "accent");
|
|
335
657
|
const priority = numberFlag(values, "priority");
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
...(
|
|
658
|
+
const input = {
|
|
659
|
+
...(values.id === undefined ? {} : { id: values.id }),
|
|
660
|
+
surface,
|
|
661
|
+
reading: string(reading, "$flags.reading", 1, 200),
|
|
662
|
+
...(accent === undefined ? {} : { accent }),
|
|
663
|
+
...(values.match === undefined ? {} : { match: literal(values.match, "$flags.match", ["word", "anywhere"]) }),
|
|
341
664
|
...(priority === undefined ? {} : { priority }),
|
|
342
665
|
};
|
|
343
|
-
if (action === "
|
|
344
|
-
|
|
345
|
-
|
|
666
|
+
if (action === "update" && values.id === undefined)
|
|
667
|
+
invalid("$flags.id", "dict update needs --id ENTRY_ID.", { hint: "List ids with: kongyoroid dict list" });
|
|
668
|
+
if (action === "add" && values.id === undefined) {
|
|
669
|
+
const existing = dictionary
|
|
670
|
+
.find(surface)
|
|
671
|
+
.find((entry) => entry.reading === input.reading && (accent === undefined || entry.accent === accent));
|
|
672
|
+
if (existing !== undefined) {
|
|
673
|
+
await emit({
|
|
674
|
+
ok: true,
|
|
675
|
+
operation: "dict.add",
|
|
676
|
+
scope,
|
|
677
|
+
path: resolve(path),
|
|
678
|
+
entry: existing,
|
|
679
|
+
created: false,
|
|
680
|
+
unchanged: true,
|
|
681
|
+
});
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (values["dry-run"]) {
|
|
686
|
+
await emit({ ok: true, operation: `dict.${action}`, scope, path: resolve(path), dryRun: true, entry: input });
|
|
346
687
|
return;
|
|
347
688
|
}
|
|
348
|
-
const
|
|
349
|
-
await
|
|
350
|
-
await emit({
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
strict: true,
|
|
689
|
+
const result = dictionary.upsert(input);
|
|
690
|
+
await writeTextFile(path, `${JSON.stringify(dictionary.toJSON(), null, 2)}\n`, true);
|
|
691
|
+
await emit({
|
|
692
|
+
ok: true,
|
|
693
|
+
operation: `dict.${action}`,
|
|
694
|
+
scope,
|
|
695
|
+
path: resolve(path),
|
|
696
|
+
entry: result.entry,
|
|
697
|
+
created: result.created,
|
|
698
|
+
digest: dictionary.digest(),
|
|
359
699
|
});
|
|
360
|
-
|
|
361
|
-
|
|
700
|
+
}
|
|
701
|
+
async function runCache(values, positionals) {
|
|
702
|
+
const action = literal(positionals[1], "$.cache", ["stats", "prune", "clear"]);
|
|
703
|
+
const directory = values["cache-dir"] ?? env("KONGYOROID_CACHE_DIR");
|
|
704
|
+
if (directory === undefined)
|
|
705
|
+
invalid("$flags.cache-dir", "Provide --cache-dir DIR (or set KONGYOROID_CACHE_DIR).", {
|
|
706
|
+
hint: "Example: kongyoroid cache stats --cache-dir ./cache",
|
|
707
|
+
});
|
|
708
|
+
const cache = new DiskCache(directory);
|
|
709
|
+
if (action === "stats") {
|
|
710
|
+
await emit({ ok: true, operation: "cache.stats", ...(await cache.stats()) });
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
const maxBytes = numberFlag(values, "max-bytes");
|
|
714
|
+
const maxEntries = numberFlag(values, "max-entries");
|
|
715
|
+
if (action === "prune" && maxBytes === undefined && maxEntries === undefined) {
|
|
716
|
+
invalid("$flags.max-bytes", "cache prune needs --max-bytes N and/or --max-entries N.", {
|
|
717
|
+
hint: "Example: kongyoroid cache prune --cache-dir ./cache --max-bytes 200000000",
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
const before = await cache.stats();
|
|
721
|
+
if (values["dry-run"]) {
|
|
722
|
+
await emit({
|
|
723
|
+
ok: true,
|
|
724
|
+
operation: `cache.${action}`,
|
|
725
|
+
dryRun: true,
|
|
726
|
+
directory: cache.directory,
|
|
727
|
+
wouldRemove: action === "clear" ? before.entries : await cache.prune(maxBytes, maxEntries, true),
|
|
728
|
+
...before,
|
|
729
|
+
});
|
|
362
730
|
return;
|
|
363
731
|
}
|
|
732
|
+
const removed = action === "clear" ? await cache.clear() : await cache.prune(maxBytes, maxEntries);
|
|
733
|
+
await emit({ ok: true, operation: `cache.${action}`, directory: cache.directory, removed, ...(await cache.stats()) });
|
|
734
|
+
}
|
|
735
|
+
async function main(argv) {
|
|
736
|
+
const { values, positionals } = parseCommandLine(argv);
|
|
737
|
+
const command = positionals[0];
|
|
364
738
|
if (values.version) {
|
|
365
739
|
await emit({ ok: true, name: PACKAGE_NAME, version: VERSION });
|
|
366
740
|
return;
|
|
367
741
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
742
|
+
if (command === "help") {
|
|
743
|
+
await writeStream(process.stdout, positionals[1] === undefined ? rootHelp() : commandHelp(positionals[1]));
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (values.help || command === undefined) {
|
|
747
|
+
if (command === undefined && !values.help) {
|
|
748
|
+
await writeStream(process.stderr, rootHelp());
|
|
749
|
+
process.exitCode = EXIT_CODES.input;
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
await writeStream(process.stdout, command === undefined ? rootHelp() : commandHelp(command));
|
|
753
|
+
return;
|
|
371
754
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
755
|
+
if (!Object.hasOwn(ALLOWED, command)) {
|
|
756
|
+
invalid("$.command", `Unknown command ${JSON.stringify(command)}.`, {
|
|
757
|
+
hint: `Commands: ${Object.keys(ALLOWED)
|
|
758
|
+
.filter((name) => name !== "help")
|
|
759
|
+
.join(", ")}. Run kongyoroid --help.`,
|
|
760
|
+
});
|
|
376
761
|
}
|
|
762
|
+
checkAllowed(command, values);
|
|
377
763
|
if (command === "schema") {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
: REQUEST_SCHEMA);
|
|
764
|
+
const kind = literal(values.kind ?? "request", "$flags.kind", ["request", "batch", "dictionary"]);
|
|
765
|
+
await emit(kind === "batch" ? BATCH_JOB_SCHEMA : kind === "dictionary" ? DICTIONARY_SCHEMA : REQUEST_SCHEMA);
|
|
381
766
|
return;
|
|
382
767
|
}
|
|
383
768
|
if (command === "capabilities") {
|
|
384
769
|
await emit(CAPABILITIES);
|
|
385
770
|
return;
|
|
386
771
|
}
|
|
772
|
+
if (command === "cache") {
|
|
773
|
+
await runCache(values, positionals);
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
387
776
|
const controller = new AbortController();
|
|
388
777
|
const cancel = () => {
|
|
389
778
|
controller.abort();
|
|
@@ -393,33 +782,92 @@ async function main(argv) {
|
|
|
393
782
|
const signal = controller.signal;
|
|
394
783
|
try {
|
|
395
784
|
if (command === "play") {
|
|
396
|
-
const path = string(positionals[1], "$.play.file", 1, 4096);
|
|
397
|
-
await
|
|
785
|
+
const path = resolve(string(positionals[1], "$.play.file", 1, 4096));
|
|
786
|
+
await ensureReadableFile(path);
|
|
787
|
+
await emit({ ok: true, operation: "play", path, ...(await playWav(path, signal)) });
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
if (command === "inspect") {
|
|
791
|
+
const path = string(positionals[1] ?? values.input, "$.inspect.file", 1, 4096);
|
|
792
|
+
const agent = new Kongyoroid({ cache: { enabled: false } });
|
|
793
|
+
const bytes = await readBytes(resolve(path));
|
|
794
|
+
const windowMs = numberFlag(values, "window-ms");
|
|
795
|
+
const result = agent.inspect(bytes, {
|
|
796
|
+
pitchTrack: values["pitch-track"] ?? false,
|
|
797
|
+
...(windowMs === undefined ? {} : { windowMs }),
|
|
798
|
+
});
|
|
799
|
+
await emit({ ok: true, operation: "inspect", path: resolve(path), ...result });
|
|
398
800
|
return;
|
|
399
801
|
}
|
|
400
|
-
|
|
802
|
+
if (positionals.length > 1 && command !== "dict") {
|
|
803
|
+
invalid("$.command", `Unexpected argument ${JSON.stringify(positionals[1])} for ${command}.`, {
|
|
804
|
+
hint: `Run: kongyoroid ${command} --help`,
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
const agent = await createAgent(values, command === "dict");
|
|
401
808
|
switch (command) {
|
|
402
809
|
case "speak": {
|
|
403
|
-
|
|
404
|
-
|
|
810
|
+
if (values.stream && !values["dry-run"]) {
|
|
811
|
+
await runSpeakStream(agent, values, signal);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
const request = await requestFromInputs(values, signal, "speak");
|
|
815
|
+
if (isObject(request) && request["kind"] !== "speech")
|
|
816
|
+
invalid("$.kind", "speak accepts only speech requests; use sing or render for songs.");
|
|
817
|
+
if (values["dry-run"]) {
|
|
818
|
+
await runValidateRequest(agent, request, values, signal);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
const result = await agent.render(request, { signal });
|
|
822
|
+
const plan = values["plan-out"] === undefined || result.engine !== "formant"
|
|
823
|
+
? undefined
|
|
824
|
+
: (await agent.plan(request, { signal, detail: "phonemes" })).plan;
|
|
825
|
+
await emit(await deliver(result, values, signal, plan), values.output === "-");
|
|
405
826
|
return;
|
|
406
827
|
}
|
|
407
828
|
case "sing": {
|
|
408
829
|
const base = values.input === undefined ? {} : object(parseJson(await readText(values.input, signal)), "$");
|
|
409
830
|
if (base["kind"] !== undefined && base["kind"] !== "song")
|
|
410
831
|
invalid("$.kind", "sing accepts only song requests.");
|
|
411
|
-
const
|
|
412
|
-
|
|
832
|
+
const request = songRequestFromFlags(values, base);
|
|
833
|
+
if (values["dry-run"]) {
|
|
834
|
+
await runValidateRequest(agent, request, values, signal);
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
const result = await agent.render(request, { signal });
|
|
838
|
+
const plan = values["plan-out"] === undefined || result.engine !== "formant"
|
|
839
|
+
? undefined
|
|
840
|
+
: (await agent.plan(request, { signal, detail: "phonemes" })).plan;
|
|
841
|
+
await emit(await deliver(result, values, signal, plan), values.output === "-");
|
|
413
842
|
return;
|
|
414
843
|
}
|
|
415
844
|
case "render": {
|
|
416
845
|
const input = string(values.input, "$flags.input", 1, 4096);
|
|
417
846
|
const request = parseJson(await readText(input, signal));
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
847
|
+
const withFlags = isObject(request)
|
|
848
|
+
? {
|
|
849
|
+
...request,
|
|
850
|
+
...(values.engine === undefined ? {} : { engine: values.engine }),
|
|
851
|
+
...(values.voice === undefined ? {} : { voice: values.voice }),
|
|
852
|
+
}
|
|
853
|
+
: request;
|
|
854
|
+
if (values["dry-run"]) {
|
|
855
|
+
await runValidateRequest(agent, withFlags, values, signal);
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
const result = await agent.render(withFlags, { signal });
|
|
859
|
+
const plan = values["plan-out"] === undefined || result.engine !== "formant"
|
|
860
|
+
? undefined
|
|
861
|
+
: (await agent.plan(withFlags, { signal, detail: "phonemes" })).plan;
|
|
862
|
+
await emit(await deliver(result, values, signal, plan), values.output === "-");
|
|
421
863
|
return;
|
|
422
864
|
}
|
|
865
|
+
case "validate":
|
|
866
|
+
await runValidate(agent, values, signal);
|
|
867
|
+
return;
|
|
868
|
+
case "plan":
|
|
869
|
+
await runPlan(agent, values, signal);
|
|
870
|
+
return;
|
|
423
871
|
case "batch": {
|
|
424
872
|
let failures = 0;
|
|
425
873
|
await runBatch(agent, values, signal, () => {
|
|
@@ -430,28 +878,60 @@ async function main(argv) {
|
|
|
430
878
|
return;
|
|
431
879
|
}
|
|
432
880
|
case "reading": {
|
|
433
|
-
const text =
|
|
881
|
+
const text = values.kana !== undefined && values.text === undefined && values.input === undefined
|
|
882
|
+
? values.kana
|
|
883
|
+
: await textInput(values, signal);
|
|
434
884
|
const speakerRef = styleFlag(values, "speaker");
|
|
885
|
+
const entries = parseDictEntries(values);
|
|
435
886
|
const reading = await agent.reading(text, {
|
|
436
887
|
signal,
|
|
437
888
|
...(values.kana === undefined ? {} : { kana: values.kana }),
|
|
438
889
|
...(speakerRef === undefined ? {} : { speaker: speakerRef }),
|
|
890
|
+
...(values.engine === undefined ? {} : { engine: parseEngine(values.engine, "$flags.engine") }),
|
|
891
|
+
...(entries.length === 0
|
|
892
|
+
? {}
|
|
893
|
+
: {
|
|
894
|
+
dictionary: new LocalDictionary(entries.map((entry, index) => ({
|
|
895
|
+
id: `flag-${index + 1}`,
|
|
896
|
+
surface: entry.surface,
|
|
897
|
+
reading: entry.reading,
|
|
898
|
+
accent: entry.accent ?? null,
|
|
899
|
+
match: "word",
|
|
900
|
+
priority: 10,
|
|
901
|
+
caseSensitive: false,
|
|
902
|
+
}))),
|
|
903
|
+
}),
|
|
904
|
+
...(values["strict-reading"] === false ? { strict: false } : {}),
|
|
439
905
|
});
|
|
440
|
-
await emit({ ok: true, text, ...reading });
|
|
906
|
+
await emit({ ok: true, operation: "reading", text, ...reading });
|
|
441
907
|
return;
|
|
442
908
|
}
|
|
443
909
|
case "voices": {
|
|
444
910
|
const kind = literal(values.kind ?? "all", "$flags.kind", ["speech", "song", "all"]);
|
|
911
|
+
const engine = literal(values.engine ?? "all", "$flags.engine", ["formant", "voicevox", "auto", "all"]);
|
|
445
912
|
const query = values.query?.normalize("NFKC").toLowerCase();
|
|
446
|
-
const
|
|
447
|
-
|
|
913
|
+
const result = await agent.voices({ signal, engine, kind });
|
|
914
|
+
const voices = result.voices.filter((voice) => query === undefined ||
|
|
915
|
+
`${voice.id} ${voice.name} ${voice.character ?? ""}`.normalize("NFKC").toLowerCase().includes(query));
|
|
916
|
+
await emit({
|
|
917
|
+
ok: true,
|
|
918
|
+
operation: "voices",
|
|
919
|
+
kind,
|
|
920
|
+
engine,
|
|
921
|
+
count: voices.length,
|
|
922
|
+
voices,
|
|
923
|
+
voicevox: result.voicevox,
|
|
924
|
+
});
|
|
448
925
|
return;
|
|
449
926
|
}
|
|
450
927
|
case "doctor": {
|
|
451
|
-
const
|
|
928
|
+
const engine = values.engine === undefined
|
|
929
|
+
? undefined
|
|
930
|
+
: literal(values.engine, "$flags.engine", ["formant", "voicevox", "auto", "all"]);
|
|
931
|
+
const diagnosis = await agent.doctor({ signal, ...(engine === undefined ? {} : { engine }) });
|
|
452
932
|
const speakerRef = styleFlag(values, "speaker");
|
|
453
933
|
let style;
|
|
454
|
-
if (diagnosis.ok && speakerRef !== undefined) {
|
|
934
|
+
if (diagnosis.voicevox?.ok === true && speakerRef !== undefined) {
|
|
455
935
|
const all = await agent.styles.all({ signal });
|
|
456
936
|
const selected = all.find((s) => s.id === speakerRef || `${s.character}/${s.name}` === speakerRef || s.character === speakerRef);
|
|
457
937
|
if (selected === undefined)
|
|
@@ -460,7 +940,7 @@ async function main(argv) {
|
|
|
460
940
|
await agent.client.initializeSpeaker(selected.id, true, { signal });
|
|
461
941
|
style = { ...selected, initialized: await agent.client.isInitializedSpeaker(selected.id, { signal }) };
|
|
462
942
|
}
|
|
463
|
-
await emit({ ...diagnosis,
|
|
943
|
+
await emit({ ...diagnosis, operation: "doctor", ...(style === undefined ? {} : { style }) });
|
|
464
944
|
if (!diagnosis.ok)
|
|
465
945
|
process.exitCode = EXIT_CODES.engine;
|
|
466
946
|
return;
|
|
@@ -477,6 +957,12 @@ async function main(argv) {
|
|
|
477
957
|
process.removeListener("SIGTERM", cancel);
|
|
478
958
|
}
|
|
479
959
|
}
|
|
960
|
+
async function runValidateRequest(agent, request, values, signal) {
|
|
961
|
+
const result = await agent.validate(request, { signal });
|
|
962
|
+
await reportDiagnostics(values, result.warnings, values.input ?? "<request>");
|
|
963
|
+
const { request: resolved, ...rest } = result;
|
|
964
|
+
await emit({ ...rest, dryRun: true, request: resolved });
|
|
965
|
+
}
|
|
480
966
|
function isArgumentError(error) {
|
|
481
967
|
return (error instanceof Error &&
|
|
482
968
|
"code" in error &&
|
|
@@ -493,7 +979,7 @@ catch (error) {
|
|
|
493
979
|
message: error.message,
|
|
494
980
|
path: "$flags",
|
|
495
981
|
retryable: false,
|
|
496
|
-
hint: "Run kongyoroid --help.",
|
|
982
|
+
hint: "Run kongyoroid --help, or kongyoroid <command> --help for the accepted flags.",
|
|
497
983
|
}
|
|
498
984
|
: asKongyoroidError(error).toJSON();
|
|
499
985
|
process.exitCode = exitCodeOf(new KongyoroidError(data));
|