@kongyo2/kongyoroid 1.0.0 → 2.0.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 +233 -0
- package/dist/cache.d.ts +32 -7
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +138 -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 +336 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli.js +598 -232
- 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 +11 -0
- package/dist/io.d.ts.map +1 -1
- package/dist/io.js +149 -5
- 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 +777 -86
- 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/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 +30 -0
- package/dist/synth/prosody.d.ts.map +1 -0
- package/dist/synth/prosody.js +46 -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 +354 -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 +127 -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 +34 -0
- package/dist/text/frontend.d.ts.map +1 -0
- package/dist/text/frontend.js +550 -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 +496 -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 +10 -0
- package/dist/text/sentences.d.ts.map +1 -0
- package/dist/text/sentences.js +84 -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/docs/cli.md +251 -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 +11 -3
- 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,218 +1,109 @@
|
|
|
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 {
|
|
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 { existingFileSha256, readBytes, readChunks, readLines, readText, writeAudioIdempotent, writeStream, writeTextFile, writeWithBackpressure, } from "./io.js";
|
|
7
9
|
import { 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";
|
|
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";
|
|
12
16
|
import { 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 numberFlag(values, name) {
|
|
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
22
|
function metadata(result) {
|
|
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: result.warnings,
|
|
43
|
+
adjustments: result.adjustments,
|
|
188
44
|
};
|
|
189
45
|
}
|
|
190
|
-
async function
|
|
46
|
+
async function reportDiagnostics(values, warnings, location) {
|
|
47
|
+
if (values.diagnostics !== "compact")
|
|
48
|
+
return;
|
|
49
|
+
for (const warning of warnings)
|
|
50
|
+
await writeStream(process.stderr, `${formatDiagnostic(warning, location)}\n`);
|
|
51
|
+
}
|
|
52
|
+
async function deliver(agent, result, values, signal, plan) {
|
|
53
|
+
if (values["plan-out"] !== undefined && values["plan-out"] !== "-") {
|
|
54
|
+
await writeTextFile(values["plan-out"], `${JSON.stringify(plan ?? (await agent.plan(result.requestHash)), null, 2)}\n`, values.force ?? false);
|
|
55
|
+
}
|
|
56
|
+
await reportDiagnostics(values, result.warnings, values.input ?? "<request>");
|
|
191
57
|
if (values.output === "-") {
|
|
192
58
|
if (values.play)
|
|
193
|
-
invalid("$flags.play", "--play cannot be combined with --output -."
|
|
59
|
+
invalid("$flags.play", "--play cannot be combined with --output -.", {
|
|
60
|
+
hint: "Write to a file and play it: -o out.wav --play",
|
|
61
|
+
});
|
|
194
62
|
await writeStream(process.stdout, result.audio);
|
|
195
|
-
return {
|
|
63
|
+
return {
|
|
64
|
+
ok: true,
|
|
65
|
+
output: "stdout",
|
|
66
|
+
...metadata(result),
|
|
67
|
+
...(plan === undefined || values["plan-out"] !== "-" ? {} : { plan }),
|
|
68
|
+
};
|
|
196
69
|
}
|
|
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 {
|
|
70
|
+
const path = values.output ?? `kongyoroid-${result.kind}-${result.sha256.slice(0, 12)}.wav`;
|
|
71
|
+
const written = await writeAudioIdempotent(path, result.audio, values.force ?? false, signal);
|
|
72
|
+
const played = values.play ? await playWav(written.path, signal) : undefined;
|
|
73
|
+
return {
|
|
74
|
+
ok: true,
|
|
75
|
+
path: written.path,
|
|
76
|
+
written: written.written,
|
|
77
|
+
unchanged: written.unchanged,
|
|
78
|
+
...metadata(result),
|
|
79
|
+
...(played === undefined ? {} : { player: played.player }),
|
|
80
|
+
...(plan === undefined || values["plan-out"] !== "-" ? {} : { plan }),
|
|
81
|
+
};
|
|
201
82
|
}
|
|
202
83
|
function speechRequestFromFlags(values, text) {
|
|
203
84
|
const request = { kind: "speech", text };
|
|
204
85
|
assign(request, "engine", values.engine);
|
|
86
|
+
assign(request, "voice", values.voice);
|
|
205
87
|
assign(request, "kana", values.kana);
|
|
88
|
+
const entries = parseDictEntries(values);
|
|
89
|
+
if (entries.length > 0)
|
|
90
|
+
request["dictionary"] = entries;
|
|
91
|
+
if (values["strict-reading"] === false)
|
|
92
|
+
request["strictReading"] = false;
|
|
206
93
|
assign(request, "speaker", styleFlag(values, "speaker"));
|
|
207
94
|
assign(request, "speed", numberFlag(values, "speed"));
|
|
208
95
|
assign(request, "pitch", numberFlag(values, "pitch"));
|
|
96
|
+
assign(request, "pitchSemitones", numberFlag(values, "pitch-semitones"));
|
|
209
97
|
assign(request, "intonation", numberFlag(values, "intonation"));
|
|
210
98
|
assign(request, "volume", numberFlag(values, "volume"));
|
|
99
|
+
assign(request, "gainDb", numberFlag(values, "gain-db"));
|
|
100
|
+
assign(request, "breathiness", numberFlag(values, "breathiness"));
|
|
211
101
|
assign(request, "prePause", numberFlag(values, "pre-pause"));
|
|
212
102
|
assign(request, "postPause", numberFlag(values, "post-pause"));
|
|
213
103
|
assign(request, "pauseLength", numberFlag(values, "pause-length"));
|
|
214
104
|
assign(request, "pauseScale", numberFlag(values, "pause-scale"));
|
|
215
|
-
|
|
105
|
+
if (values.upspeak !== undefined)
|
|
106
|
+
request["upspeak"] = values.upspeak;
|
|
216
107
|
assign(request, "split", values.split);
|
|
217
108
|
assign(request, "sampleRate", numberFlag(values, "sample-rate"));
|
|
218
109
|
assign(request, "seed", numberFlag(values, "seed"));
|
|
@@ -220,22 +111,53 @@ function speechRequestFromFlags(values, text) {
|
|
|
220
111
|
}
|
|
221
112
|
function songRequestFromFlags(values, base) {
|
|
222
113
|
const request = { ...base, kind: "song" };
|
|
223
|
-
if (values.
|
|
224
|
-
|
|
114
|
+
if (values.mml !== undefined) {
|
|
115
|
+
if (values.melody !== undefined || values.beats !== undefined)
|
|
116
|
+
invalid("$flags.mml", "--mml cannot be combined with --melody or --beats.");
|
|
117
|
+
request["notes"] = { mml: values.mml, ...(values.lyrics === undefined ? {} : { lyrics: values.lyrics }) };
|
|
118
|
+
}
|
|
119
|
+
else if (values.lyrics !== undefined || values.melody !== undefined) {
|
|
120
|
+
if (values.melody === undefined)
|
|
121
|
+
invalid("$flags.melody", "--lyrics needs --melody (or use --mml).", {
|
|
122
|
+
hint: 'Example: --lyrics "ドレミ" --melody "C4 D4 E4"',
|
|
123
|
+
});
|
|
124
|
+
const notes = { lyrics: values.lyrics ?? "", melody: values.melody };
|
|
225
125
|
assign(notes, "beats", values.beats);
|
|
226
126
|
request["notes"] = notes;
|
|
227
127
|
}
|
|
228
128
|
else if (values.beats !== undefined) {
|
|
229
|
-
invalid("$flags.beats", "--beats needs --melody."
|
|
129
|
+
invalid("$flags.beats", "--beats needs --melody.", {
|
|
130
|
+
hint: 'Example: --lyrics "ドレミ" --melody "C4 D4 E4" --beats "1 1 2"',
|
|
131
|
+
});
|
|
230
132
|
}
|
|
231
133
|
assign(request, "engine", values.engine);
|
|
134
|
+
assign(request, "voice", values.voice);
|
|
232
135
|
assign(request, "tempo", numberFlag(values, "tempo"));
|
|
233
136
|
assign(request, "singer", styleFlag(values, "singer"));
|
|
234
137
|
assign(request, "teacher", styleFlag(values, "teacher"));
|
|
235
138
|
assign(request, "transpose", numberFlag(values, "transpose"));
|
|
236
139
|
assign(request, "volume", numberFlag(values, "volume"));
|
|
140
|
+
assign(request, "gainDb", numberFlag(values, "gain-db"));
|
|
141
|
+
assign(request, "breathiness", numberFlag(values, "breathiness"));
|
|
237
142
|
assign(request, "vibratoDepth", numberFlag(values, "vibrato-depth"));
|
|
238
143
|
assign(request, "vibratoRate", numberFlag(values, "vibrato-rate"));
|
|
144
|
+
const delay = numberFlag(values, "vibrato-delay-ms");
|
|
145
|
+
const fade = numberFlag(values, "vibrato-fade-ms");
|
|
146
|
+
if (delay !== undefined || fade !== undefined) {
|
|
147
|
+
const vibrato = {};
|
|
148
|
+
assign(vibrato, "delayMs", delay);
|
|
149
|
+
assign(vibrato, "fadeMs", fade);
|
|
150
|
+
assign(vibrato, "depthCents", numberFlag(values, "vibrato-depth"));
|
|
151
|
+
assign(vibrato, "rateHz", numberFlag(values, "vibrato-rate"));
|
|
152
|
+
delete request["vibratoDepth"];
|
|
153
|
+
delete request["vibratoRate"];
|
|
154
|
+
request["vibrato"] = vibrato;
|
|
155
|
+
}
|
|
156
|
+
assign(request, "portamentoMs", numberFlag(values, "portamento-ms"));
|
|
157
|
+
assign(request, "scoopCents", numberFlag(values, "scoop-cents"));
|
|
158
|
+
assign(request, "scoopMs", numberFlag(values, "scoop-ms"));
|
|
159
|
+
if (values["consonant-compression"] !== undefined)
|
|
160
|
+
request["consonantCompression"] = values["consonant-compression"];
|
|
239
161
|
assign(request, "leadIn", numberFlag(values, "lead-in"));
|
|
240
162
|
assign(request, "leadOut", numberFlag(values, "lead-out"));
|
|
241
163
|
assign(request, "sampleRate", numberFlag(values, "sample-rate"));
|
|
@@ -249,28 +171,208 @@ async function textInput(values, signal) {
|
|
|
249
171
|
return values.text;
|
|
250
172
|
if (values.input !== undefined)
|
|
251
173
|
return (await readText(values.input, signal)).replace(/\r?\n$/u, "");
|
|
252
|
-
return invalid("$flags.text", "Provide --text or --input FILE (use - for standard input)."
|
|
174
|
+
return invalid("$flags.text", "Provide --text or --input FILE (use - for standard input).", {
|
|
175
|
+
hint: 'Example: kongyoroid speak --text "こんにちは" -o hello.wav',
|
|
176
|
+
});
|
|
253
177
|
}
|
|
254
|
-
function
|
|
178
|
+
async function requestFromInputs(values, signal, command) {
|
|
179
|
+
const hasSpeech = values.text !== undefined || values.kana !== undefined;
|
|
180
|
+
const hasSong = values.lyrics !== undefined || values.melody !== undefined || values.mml !== undefined;
|
|
181
|
+
if (values.input !== undefined) {
|
|
182
|
+
const raw = await readText(values.input, signal);
|
|
183
|
+
const trimmed = raw.trim();
|
|
184
|
+
if (hasSpeech && !hasSong && !trimmed.startsWith("{"))
|
|
185
|
+
return speechRequestFromFlags(values, raw.replace(/\r?\n$/u, ""));
|
|
186
|
+
const json = parseJson(raw);
|
|
187
|
+
if (isObject(json) && json["kind"] === "song")
|
|
188
|
+
return songRequestFromFlags(values, json);
|
|
189
|
+
if (isObject(json) && values.engine !== undefined)
|
|
190
|
+
return { ...json, engine: values.engine };
|
|
191
|
+
return json;
|
|
192
|
+
}
|
|
193
|
+
if (hasSong)
|
|
194
|
+
return songRequestFromFlags(values, {});
|
|
195
|
+
if (hasSpeech || values.text !== undefined)
|
|
196
|
+
return speechRequestFromFlags(values, await textInput(values, signal));
|
|
197
|
+
return invalid("$flags.input", `Provide --input FILE|- (a request JSON), --text, or --lyrics/--melody/--mml for ${command}.`, {
|
|
198
|
+
hint: `Examples: kongyoroid ${command} --input request.json | kongyoroid ${command} --text "こんにちは"`,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
async function loadDictionaryFile(path, tolerateMissing) {
|
|
202
|
+
if (path === undefined)
|
|
203
|
+
return undefined;
|
|
204
|
+
try {
|
|
205
|
+
return await LocalDictionary.load(path);
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
if (tolerateMissing &&
|
|
209
|
+
error instanceof KongyoroidError &&
|
|
210
|
+
error.code === "IO_ERROR" &&
|
|
211
|
+
/ENOENT/u.test(error.message))
|
|
212
|
+
return undefined;
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function createAgent(values, tolerateMissingDictionary = false) {
|
|
255
217
|
const engineFlag = values.engine ?? env("KONGYOROID_ENGINE");
|
|
256
218
|
const concurrency = numberFlag(values, "concurrency");
|
|
257
219
|
const timeoutMs = numberFlag(values, "timeout-ms");
|
|
258
220
|
const retries = numberFlag(values, "retries");
|
|
259
221
|
const cacheDir = values["cache-dir"] ?? env("KONGYOROID_CACHE_DIR");
|
|
222
|
+
const voice = values.voice ?? env("KONGYOROID_VOICE");
|
|
260
223
|
const speaker = env("KONGYOROID_SPEAKER");
|
|
261
224
|
const singer = env("KONGYOROID_SINGER");
|
|
262
225
|
const teacher = env("KONGYOROID_TEACHER");
|
|
263
|
-
|
|
226
|
+
const dictionaryPath = values.dictionary ?? env("KONGYOROID_DICTIONARY");
|
|
227
|
+
const dictionary = await loadDictionaryFile(dictionaryPath, tolerateMissingDictionary);
|
|
228
|
+
const options = {
|
|
264
229
|
endpoint: values.endpoint ?? env("KONGYOROID_ENDPOINT") ?? env("VOICEVOX_URL") ?? "http://127.0.0.1:50021",
|
|
265
|
-
...(engineFlag === undefined ? {} : { engine: parseEngine(engineFlag, "$flags.engine") }),
|
|
230
|
+
...(engineFlag === undefined || engineFlag === "all" ? {} : { engine: parseEngine(engineFlag, "$flags.engine") }),
|
|
231
|
+
...(voice === undefined ? {} : { voice }),
|
|
266
232
|
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
267
233
|
...(retries === undefined ? {} : { retries }),
|
|
268
234
|
...(concurrency === undefined ? {} : { concurrency }),
|
|
269
|
-
...(
|
|
235
|
+
...(dictionary === undefined ? {} : { dictionary }),
|
|
270
236
|
...(speaker === undefined ? {} : { speaker }),
|
|
271
237
|
...(singer === undefined ? {} : { singer }),
|
|
272
238
|
...(teacher === undefined ? {} : { teacher }),
|
|
273
|
-
|
|
239
|
+
cache: {
|
|
240
|
+
...(cacheDir === undefined ? {} : { directory: cacheDir }),
|
|
241
|
+
...(values.cache === false ? { enabled: false } : {}),
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
return new Kongyoroid(options);
|
|
245
|
+
}
|
|
246
|
+
function planDetail(values) {
|
|
247
|
+
return literal(values.detail ?? "summary", "$flags.detail", ["summary", "phonemes", "acoustics"]);
|
|
248
|
+
}
|
|
249
|
+
async function runValidate(agent, values, signal) {
|
|
250
|
+
const request = await requestFromInputs(values, signal, "validate");
|
|
251
|
+
const result = await agent.validate(request, { signal });
|
|
252
|
+
if (values.diagnostics === "compact") {
|
|
253
|
+
for (const warning of result.warnings)
|
|
254
|
+
await writeStream(process.stdout, `${formatDiagnostic(warning, values.input ?? "<request>")}\n`);
|
|
255
|
+
if (result.warnings.length === 0)
|
|
256
|
+
await emit({ ok: true, operation: "validate", renderable: result.renderable, requestHash: result.requestHash });
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const { request: resolved, ...rest } = result;
|
|
260
|
+
await emit({ ...rest, request: resolved });
|
|
261
|
+
}
|
|
262
|
+
async function runPlan(agent, values, signal) {
|
|
263
|
+
const request = await requestFromInputs(values, signal, "plan");
|
|
264
|
+
const result = await agent.plan(request, { signal, detail: planDetail(values) });
|
|
265
|
+
await emit(result);
|
|
266
|
+
}
|
|
267
|
+
async function runSpeakStream(agent, values, signal) {
|
|
268
|
+
if (values.play)
|
|
269
|
+
invalid("$flags.play", "--play cannot be combined with --stream.");
|
|
270
|
+
const format = literal(values.format ?? "wav", "$flags.format", ["wav", "pcm", "ndjson"]);
|
|
271
|
+
const output = values.output ?? "-";
|
|
272
|
+
if (output !== "-" && format !== "wav")
|
|
273
|
+
invalid("$flags.format", "Streaming to a file writes WAV; use --output - for pcm or ndjson.");
|
|
274
|
+
const input = speechRequestFromFlags(values, "placeholder");
|
|
275
|
+
delete input["text"];
|
|
276
|
+
const source = values.text !== undefined
|
|
277
|
+
? (async function* () {
|
|
278
|
+
yield values.text ?? "";
|
|
279
|
+
})()
|
|
280
|
+
: readChunks(values.input ?? "-", signal);
|
|
281
|
+
const events = agent.speakStream(source, input, { signal });
|
|
282
|
+
const stdout = process.stdout;
|
|
283
|
+
let sampleRate = 0;
|
|
284
|
+
let file;
|
|
285
|
+
let fileFrames = 0;
|
|
286
|
+
let fileBytes = 0;
|
|
287
|
+
let filePath = "";
|
|
288
|
+
const sentences = [];
|
|
289
|
+
const openFile = async () => {
|
|
290
|
+
if (output === "-")
|
|
291
|
+
return;
|
|
292
|
+
const { open } = await import("node:fs/promises");
|
|
293
|
+
filePath = resolve(output);
|
|
294
|
+
if (!values.force) {
|
|
295
|
+
const existing = await existingFileSha256(filePath);
|
|
296
|
+
if (existing !== undefined)
|
|
297
|
+
invalid("$flags.output", `Refusing to overwrite ${filePath}.`, {
|
|
298
|
+
hint: "Pass --force or choose another path.",
|
|
299
|
+
repairOptions: [{ action: "use-force", description: "Overwrite with --force." }],
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
file = await open(filePath, "w", 0o644);
|
|
303
|
+
await file.write(wavHeader(0, sampleRate));
|
|
304
|
+
fileBytes = 44;
|
|
305
|
+
};
|
|
306
|
+
let headerWritten = false;
|
|
307
|
+
try {
|
|
308
|
+
for await (const event of events) {
|
|
309
|
+
if (event.type === "sentence") {
|
|
310
|
+
const line = {
|
|
311
|
+
type: "sentence",
|
|
312
|
+
index: event.index,
|
|
313
|
+
text: event.text,
|
|
314
|
+
kana: event.kana,
|
|
315
|
+
durationSeconds: Math.round(event.durationSeconds * 1000) / 1000,
|
|
316
|
+
startSeconds: sampleRate === 0 ? 0 : Math.round((event.startFrame / sampleRate) * 1000) / 1000,
|
|
317
|
+
warnings: event.warnings,
|
|
318
|
+
};
|
|
319
|
+
sentences.push(line);
|
|
320
|
+
if (format === "ndjson" && output === "-")
|
|
321
|
+
await writeWithBackpressure(stdout, `${JSON.stringify(line)}\n`, signal);
|
|
322
|
+
else if (values.progress)
|
|
323
|
+
await emit(line, true);
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (event.type === "audio") {
|
|
327
|
+
const block = event.block;
|
|
328
|
+
if (sampleRate === 0) {
|
|
329
|
+
sampleRate = block.sampleRate;
|
|
330
|
+
await openFile();
|
|
331
|
+
}
|
|
332
|
+
const bytes = pcm16Bytes(block.samples);
|
|
333
|
+
if (file !== undefined) {
|
|
334
|
+
await file.write(bytes);
|
|
335
|
+
fileFrames += block.samples.length;
|
|
336
|
+
fileBytes += bytes.length;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (format === "ndjson") {
|
|
340
|
+
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);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (format === "wav" && !headerWritten) {
|
|
344
|
+
await writeWithBackpressure(stdout, streamingWavHeader(sampleRate), signal);
|
|
345
|
+
headerWritten = true;
|
|
346
|
+
}
|
|
347
|
+
await writeWithBackpressure(stdout, bytes, signal);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const summary = {
|
|
351
|
+
ok: true,
|
|
352
|
+
operation: "speak",
|
|
353
|
+
stream: true,
|
|
354
|
+
format,
|
|
355
|
+
output: output === "-" ? "stdout" : filePath,
|
|
356
|
+
sentences: event.sentences,
|
|
357
|
+
frames: event.frames,
|
|
358
|
+
durationSeconds: Math.round(event.durationSeconds * 1000) / 1000,
|
|
359
|
+
sampleRate: sampleRate === 0 ? null : sampleRate,
|
|
360
|
+
...(values.progress || format === "ndjson" ? {} : { segments: sentences }),
|
|
361
|
+
};
|
|
362
|
+
if (format === "ndjson" && output === "-")
|
|
363
|
+
await writeWithBackpressure(stdout, `${JSON.stringify({ type: "end", ...summary })}\n`, signal);
|
|
364
|
+
else
|
|
365
|
+
await emit(summary, output === "-");
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
finally {
|
|
369
|
+
if (file !== undefined) {
|
|
370
|
+
const header = wavHeader(fileFrames, sampleRate);
|
|
371
|
+
await file.write(header, 0, 44, 0);
|
|
372
|
+
await file.close();
|
|
373
|
+
void fileBytes;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
274
376
|
}
|
|
275
377
|
async function runBatch(agent, values, signal, onFailure) {
|
|
276
378
|
const input = string(values.input, "$flags.input", 1, 4096);
|
|
@@ -279,6 +381,7 @@ async function runBatch(agent, values, signal, onFailure) {
|
|
|
279
381
|
const active = new Set();
|
|
280
382
|
const ids = new Set();
|
|
281
383
|
let line = 0;
|
|
384
|
+
let stop = false;
|
|
282
385
|
const run = async (text, index) => {
|
|
283
386
|
let id = null;
|
|
284
387
|
try {
|
|
@@ -291,12 +394,22 @@ async function runBatch(agent, values, signal, onFailure) {
|
|
|
291
394
|
invalid(`$line[${index}].id`, "Duplicate job id (case-insensitive).");
|
|
292
395
|
ids.add(id.toLowerCase());
|
|
293
396
|
const result = await agent.render(job["request"], { signal });
|
|
294
|
-
const
|
|
295
|
-
await emit({
|
|
397
|
+
const written = await writeAudioIdempotent(resolve(directory, `${id}.wav`), result.audio, values.force ?? false, signal);
|
|
398
|
+
await emit({
|
|
399
|
+
ok: true,
|
|
400
|
+
id,
|
|
401
|
+
line: index,
|
|
402
|
+
path: written.path,
|
|
403
|
+
written: written.written,
|
|
404
|
+
unchanged: written.unchanged,
|
|
405
|
+
...metadata(result),
|
|
406
|
+
});
|
|
296
407
|
}
|
|
297
408
|
catch (error) {
|
|
298
409
|
const failure = asKongyoroidError(error);
|
|
299
410
|
onFailure();
|
|
411
|
+
if (values["stop-on-error"])
|
|
412
|
+
stop = true;
|
|
300
413
|
process.exitCode = Math.max(Number(process.exitCode ?? 0), exitCodeOf(failure));
|
|
301
414
|
await emit({ ok: false, id, line: index, error: failure.toJSON() });
|
|
302
415
|
if (failure.code === "ABORTED")
|
|
@@ -305,6 +418,8 @@ async function runBatch(agent, values, signal, onFailure) {
|
|
|
305
418
|
};
|
|
306
419
|
try {
|
|
307
420
|
for await (const text of readLines(input, signal)) {
|
|
421
|
+
if (stop)
|
|
422
|
+
break;
|
|
308
423
|
line += 1;
|
|
309
424
|
if (line > LIMITS.batchLines)
|
|
310
425
|
invalid("$flags.input", `A batch is limited to ${LIMITS.batchLines} lines.`);
|
|
@@ -321,69 +436,226 @@ async function runBatch(agent, values, signal, onFailure) {
|
|
|
321
436
|
}
|
|
322
437
|
}
|
|
323
438
|
async function runDict(agent, values, positionals, signal) {
|
|
324
|
-
const action = literal(positionals[1], "$.dict", ["list", "add", "update", "delete"]);
|
|
439
|
+
const action = literal(positionals[1], "$.dict", ["list", "add", "update", "delete", "check"]);
|
|
440
|
+
const scope = literal(values.scope ?? "local", "$flags.scope", ["local", "voicevox"]);
|
|
441
|
+
const reading = values.reading ?? values.pronunciation;
|
|
442
|
+
if (scope === "voicevox") {
|
|
443
|
+
if (action === "check")
|
|
444
|
+
invalid("$flags.scope", "dict check is only available for the local dictionary.");
|
|
445
|
+
if (action === "list") {
|
|
446
|
+
await emit({ ok: true, operation: "dict.list", scope, words: await agent.voicevoxDictionary.list({ signal }) });
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (action === "delete") {
|
|
450
|
+
const uuid = string(values.id ?? positionals[2], "$flags.id", 1, 200);
|
|
451
|
+
if (values["dry-run"]) {
|
|
452
|
+
await emit({ ok: true, operation: "dict.delete", scope, dryRun: true, uuid });
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
await agent.voicevoxDictionary.remove(uuid, { signal });
|
|
456
|
+
await emit({ ok: true, operation: "dict.delete", scope, deleted: uuid });
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const priority = numberFlag(values, "priority");
|
|
460
|
+
const draft = {
|
|
461
|
+
surface: string(values.surface, "$flags.surface", 1, 200),
|
|
462
|
+
pronunciation: string(reading, "$flags.reading", 1, 200),
|
|
463
|
+
accentType: numberFlag(values, "accent") ?? invalid("$flags.accent", "Provide --accent N (0 = flat)."),
|
|
464
|
+
...(values["word-type"] === undefined ? {} : { wordType: values["word-type"] }),
|
|
465
|
+
...(priority === undefined ? {} : { priority }),
|
|
466
|
+
};
|
|
467
|
+
if (action === "add") {
|
|
468
|
+
if (values["dry-run"]) {
|
|
469
|
+
await emit({ ok: true, operation: "dict.add", scope, dryRun: true, word: draft });
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const uuid = await agent.voicevoxDictionary.add(draft, { signal });
|
|
473
|
+
await emit({ ok: true, operation: "dict.add", scope, uuid, word: draft });
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const uuid = string(values.id ?? positionals[2], "$flags.id", 1, 200);
|
|
477
|
+
await agent.voicevoxDictionary.update(uuid, draft, { signal });
|
|
478
|
+
await emit({ ok: true, operation: "dict.update", scope, updated: uuid, word: draft });
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
const path = values.dictionary ?? env("KONGYOROID_DICTIONARY") ?? "kongyoroid-dictionary.json";
|
|
482
|
+
const dictionary = (await loadDictionaryFile(path, true)) ?? new LocalDictionary();
|
|
325
483
|
if (action === "list") {
|
|
326
|
-
await emit({
|
|
484
|
+
await emit({
|
|
485
|
+
ok: true,
|
|
486
|
+
operation: "dict.list",
|
|
487
|
+
scope,
|
|
488
|
+
path: resolve(path),
|
|
489
|
+
entries: dictionary.list(),
|
|
490
|
+
digest: dictionary.digest(),
|
|
491
|
+
});
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (action === "check") {
|
|
495
|
+
const text = await textInput(values, signal);
|
|
496
|
+
const merged = agent.dictionary.merge(dictionary);
|
|
497
|
+
const analysed = await agent.reading(text, { signal, dictionary: merged, engine: "formant", strict: false });
|
|
498
|
+
await emit({
|
|
499
|
+
ok: true,
|
|
500
|
+
operation: "dict.check",
|
|
501
|
+
scope,
|
|
502
|
+
path: resolve(path),
|
|
503
|
+
text,
|
|
504
|
+
kana: analysed.kana,
|
|
505
|
+
hits: analysed.dictionaryHits,
|
|
506
|
+
phrases: analysed.phrases.map((p) => ({ text: p.text, accent: p.accent, accentSource: p.accentSource })),
|
|
507
|
+
warnings: analysed.warnings,
|
|
508
|
+
});
|
|
327
509
|
return;
|
|
328
510
|
}
|
|
329
511
|
if (action === "delete") {
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
512
|
+
const id = string(values.id ?? positionals[2], "$flags.id", 1, 100);
|
|
513
|
+
const entry = dictionary.get(id);
|
|
514
|
+
if (entry === undefined) {
|
|
515
|
+
await emit({
|
|
516
|
+
ok: true,
|
|
517
|
+
operation: "dict.delete",
|
|
518
|
+
scope,
|
|
519
|
+
path: resolve(path),
|
|
520
|
+
id,
|
|
521
|
+
deleted: false,
|
|
522
|
+
message: "No such entry: nothing to do.",
|
|
523
|
+
});
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
if (values["dry-run"]) {
|
|
527
|
+
await emit({ ok: true, operation: "dict.delete", scope, path: resolve(path), dryRun: true, entry });
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
dictionary.remove(id);
|
|
531
|
+
await writeTextFile(path, `${JSON.stringify(dictionary.toJSON(), null, 2)}\n`, true);
|
|
532
|
+
await emit({
|
|
533
|
+
ok: true,
|
|
534
|
+
operation: "dict.delete",
|
|
535
|
+
scope,
|
|
536
|
+
path: resolve(path),
|
|
537
|
+
id,
|
|
538
|
+
deleted: true,
|
|
539
|
+
digest: dictionary.digest(),
|
|
540
|
+
});
|
|
333
541
|
return;
|
|
334
542
|
}
|
|
543
|
+
const surface = string(values.surface, "$flags.surface", 1, 200);
|
|
544
|
+
const accent = numberFlag(values, "accent");
|
|
335
545
|
const priority = numberFlag(values, "priority");
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
...(
|
|
546
|
+
const input = {
|
|
547
|
+
...(values.id === undefined ? {} : { id: values.id }),
|
|
548
|
+
surface,
|
|
549
|
+
reading: string(reading, "$flags.reading", 1, 200),
|
|
550
|
+
...(accent === undefined ? {} : { accent }),
|
|
551
|
+
...(values.match === undefined ? {} : { match: literal(values.match, "$flags.match", ["word", "anywhere"]) }),
|
|
341
552
|
...(priority === undefined ? {} : { priority }),
|
|
342
553
|
};
|
|
343
|
-
if (action === "
|
|
344
|
-
|
|
345
|
-
|
|
554
|
+
if (action === "update" && values.id === undefined)
|
|
555
|
+
invalid("$flags.id", "dict update needs --id ENTRY_ID.", { hint: "List ids with: kongyoroid dict list" });
|
|
556
|
+
if (action === "add" && values.id === undefined) {
|
|
557
|
+
const existing = dictionary
|
|
558
|
+
.find(surface)
|
|
559
|
+
.find((entry) => entry.reading === input.reading && (accent === undefined || entry.accent === accent));
|
|
560
|
+
if (existing !== undefined) {
|
|
561
|
+
await emit({
|
|
562
|
+
ok: true,
|
|
563
|
+
operation: "dict.add",
|
|
564
|
+
scope,
|
|
565
|
+
path: resolve(path),
|
|
566
|
+
entry: existing,
|
|
567
|
+
created: false,
|
|
568
|
+
unchanged: true,
|
|
569
|
+
});
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (values["dry-run"]) {
|
|
574
|
+
await emit({ ok: true, operation: `dict.${action}`, scope, path: resolve(path), dryRun: true, entry: input });
|
|
346
575
|
return;
|
|
347
576
|
}
|
|
348
|
-
const
|
|
349
|
-
await
|
|
350
|
-
await emit({
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
strict: true,
|
|
577
|
+
const result = dictionary.upsert(input);
|
|
578
|
+
await writeTextFile(path, `${JSON.stringify(dictionary.toJSON(), null, 2)}\n`, true);
|
|
579
|
+
await emit({
|
|
580
|
+
ok: true,
|
|
581
|
+
operation: `dict.${action}`,
|
|
582
|
+
scope,
|
|
583
|
+
path: resolve(path),
|
|
584
|
+
entry: result.entry,
|
|
585
|
+
created: result.created,
|
|
586
|
+
digest: dictionary.digest(),
|
|
359
587
|
});
|
|
360
|
-
|
|
361
|
-
|
|
588
|
+
}
|
|
589
|
+
async function runCache(values, positionals) {
|
|
590
|
+
const action = literal(positionals[1], "$.cache", ["stats", "prune", "clear"]);
|
|
591
|
+
const directory = values["cache-dir"] ?? env("KONGYOROID_CACHE_DIR");
|
|
592
|
+
if (directory === undefined)
|
|
593
|
+
invalid("$flags.cache-dir", "Provide --cache-dir DIR (or set KONGYOROID_CACHE_DIR).", {
|
|
594
|
+
hint: "Example: kongyoroid cache stats --cache-dir ./cache",
|
|
595
|
+
});
|
|
596
|
+
const cache = new DiskCache(directory);
|
|
597
|
+
if (action === "stats") {
|
|
598
|
+
await emit({ ok: true, operation: "cache.stats", ...(await cache.stats()) });
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const before = await cache.stats();
|
|
602
|
+
if (values["dry-run"]) {
|
|
603
|
+
await emit({
|
|
604
|
+
ok: true,
|
|
605
|
+
operation: `cache.${action}`,
|
|
606
|
+
dryRun: true,
|
|
607
|
+
directory: cache.directory,
|
|
608
|
+
wouldRemove: action === "clear" ? before.entries : null,
|
|
609
|
+
...before,
|
|
610
|
+
});
|
|
362
611
|
return;
|
|
363
612
|
}
|
|
613
|
+
const removed = action === "clear"
|
|
614
|
+
? await cache.clear()
|
|
615
|
+
: await cache.prune(numberFlag(values, "max-bytes"), numberFlag(values, "max-entries"));
|
|
616
|
+
await emit({ ok: true, operation: `cache.${action}`, directory: cache.directory, removed, ...(await cache.stats()) });
|
|
617
|
+
}
|
|
618
|
+
async function main(argv) {
|
|
619
|
+
const { values, positionals } = parseCommandLine(argv);
|
|
620
|
+
const command = positionals[0];
|
|
364
621
|
if (values.version) {
|
|
365
622
|
await emit({ ok: true, name: PACKAGE_NAME, version: VERSION });
|
|
366
623
|
return;
|
|
367
624
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
625
|
+
if (command === "help") {
|
|
626
|
+
await writeStream(process.stdout, positionals[1] === undefined ? rootHelp() : commandHelp(positionals[1]));
|
|
627
|
+
return;
|
|
371
628
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
629
|
+
if (values.help || command === undefined) {
|
|
630
|
+
if (command === undefined && !values.help) {
|
|
631
|
+
await writeStream(process.stderr, rootHelp());
|
|
632
|
+
process.exitCode = EXIT_CODES.input;
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
await writeStream(process.stdout, command === undefined ? rootHelp() : commandHelp(command));
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (!Object.hasOwn(ALLOWED, command)) {
|
|
639
|
+
invalid("$.command", `Unknown command ${JSON.stringify(command)}.`, {
|
|
640
|
+
hint: `Commands: ${Object.keys(ALLOWED)
|
|
641
|
+
.filter((name) => name !== "help")
|
|
642
|
+
.join(", ")}. Run kongyoroid --help.`,
|
|
643
|
+
});
|
|
376
644
|
}
|
|
645
|
+
checkAllowed(command, values);
|
|
377
646
|
if (command === "schema") {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
: REQUEST_SCHEMA);
|
|
647
|
+
const kind = literal(values.kind ?? "request", "$flags.kind", ["request", "batch", "dictionary"]);
|
|
648
|
+
await emit(kind === "batch" ? BATCH_JOB_SCHEMA : kind === "dictionary" ? DICTIONARY_SCHEMA : REQUEST_SCHEMA);
|
|
381
649
|
return;
|
|
382
650
|
}
|
|
383
651
|
if (command === "capabilities") {
|
|
384
652
|
await emit(CAPABILITIES);
|
|
385
653
|
return;
|
|
386
654
|
}
|
|
655
|
+
if (command === "cache") {
|
|
656
|
+
await runCache(values, positionals);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
387
659
|
const controller = new AbortController();
|
|
388
660
|
const cancel = () => {
|
|
389
661
|
controller.abort();
|
|
@@ -394,32 +666,88 @@ async function main(argv) {
|
|
|
394
666
|
try {
|
|
395
667
|
if (command === "play") {
|
|
396
668
|
const path = string(positionals[1], "$.play.file", 1, 4096);
|
|
397
|
-
await emit({ ok: true, path: resolve(path), ...(await playWav(resolve(path), signal)) });
|
|
669
|
+
await emit({ ok: true, operation: "play", path: resolve(path), ...(await playWav(resolve(path), signal)) });
|
|
398
670
|
return;
|
|
399
671
|
}
|
|
400
|
-
|
|
672
|
+
if (command === "inspect") {
|
|
673
|
+
const path = string(positionals[1] ?? values.input, "$.inspect.file", 1, 4096);
|
|
674
|
+
const agent = new Kongyoroid({ cache: { enabled: false } });
|
|
675
|
+
const bytes = await readBytes(resolve(path));
|
|
676
|
+
const windowMs = numberFlag(values, "window-ms");
|
|
677
|
+
const result = agent.inspect(bytes, {
|
|
678
|
+
pitchTrack: values["pitch-track"] ?? false,
|
|
679
|
+
...(windowMs === undefined ? {} : { windowMs }),
|
|
680
|
+
});
|
|
681
|
+
await emit({ ok: true, operation: "inspect", path: resolve(path), ...result });
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (positionals.length > 1 && command !== "dict") {
|
|
685
|
+
invalid("$.command", `Unexpected argument ${JSON.stringify(positionals[1])} for ${command}.`, {
|
|
686
|
+
hint: `Run: kongyoroid ${command} --help`,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
const agent = await createAgent(values, command === "dict");
|
|
401
690
|
switch (command) {
|
|
402
691
|
case "speak": {
|
|
403
|
-
|
|
404
|
-
|
|
692
|
+
if (values.stream) {
|
|
693
|
+
await runSpeakStream(agent, values, signal);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const request = speechRequestFromFlags(values, await textInput(values, signal));
|
|
697
|
+
if (values["dry-run"]) {
|
|
698
|
+
await runValidateRequest(agent, request, values, signal);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
const result = await agent.render(request, { signal });
|
|
702
|
+
const plan = values["plan-out"] === undefined || result.engine !== "formant"
|
|
703
|
+
? undefined
|
|
704
|
+
: (await agent.plan(request, { signal, detail: "phonemes" })).plan;
|
|
705
|
+
await emit(await deliver(agent, result, values, signal, plan), values.output === "-");
|
|
405
706
|
return;
|
|
406
707
|
}
|
|
407
708
|
case "sing": {
|
|
408
709
|
const base = values.input === undefined ? {} : object(parseJson(await readText(values.input, signal)), "$");
|
|
409
710
|
if (base["kind"] !== undefined && base["kind"] !== "song")
|
|
410
711
|
invalid("$.kind", "sing accepts only song requests.");
|
|
411
|
-
const
|
|
412
|
-
|
|
712
|
+
const request = songRequestFromFlags(values, base);
|
|
713
|
+
if (values["dry-run"]) {
|
|
714
|
+
await runValidateRequest(agent, request, values, signal);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const result = await agent.render(request, { signal });
|
|
718
|
+
const plan = values["plan-out"] === undefined || result.engine !== "formant"
|
|
719
|
+
? undefined
|
|
720
|
+
: (await agent.plan(request, { signal, detail: "phonemes" })).plan;
|
|
721
|
+
await emit(await deliver(agent, result, values, signal, plan), values.output === "-");
|
|
413
722
|
return;
|
|
414
723
|
}
|
|
415
724
|
case "render": {
|
|
416
725
|
const input = string(values.input, "$flags.input", 1, 4096);
|
|
417
726
|
const request = parseJson(await readText(input, signal));
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
727
|
+
const withFlags = isObject(request)
|
|
728
|
+
? {
|
|
729
|
+
...request,
|
|
730
|
+
...(values.engine === undefined ? {} : { engine: values.engine }),
|
|
731
|
+
...(values.voice === undefined ? {} : { voice: values.voice }),
|
|
732
|
+
}
|
|
733
|
+
: request;
|
|
734
|
+
if (values["dry-run"]) {
|
|
735
|
+
await runValidateRequest(agent, withFlags, values, signal);
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
const result = await agent.render(withFlags, { signal });
|
|
739
|
+
const plan = values["plan-out"] === undefined || result.engine !== "formant"
|
|
740
|
+
? undefined
|
|
741
|
+
: (await agent.plan(withFlags, { signal, detail: "phonemes" })).plan;
|
|
742
|
+
await emit(await deliver(agent, result, values, signal, plan), values.output === "-");
|
|
421
743
|
return;
|
|
422
744
|
}
|
|
745
|
+
case "validate":
|
|
746
|
+
await runValidate(agent, values, signal);
|
|
747
|
+
return;
|
|
748
|
+
case "plan":
|
|
749
|
+
await runPlan(agent, values, signal);
|
|
750
|
+
return;
|
|
423
751
|
case "batch": {
|
|
424
752
|
let failures = 0;
|
|
425
753
|
await runBatch(agent, values, signal, () => {
|
|
@@ -430,28 +758,60 @@ async function main(argv) {
|
|
|
430
758
|
return;
|
|
431
759
|
}
|
|
432
760
|
case "reading": {
|
|
433
|
-
const text =
|
|
761
|
+
const text = values.kana !== undefined && values.text === undefined && values.input === undefined
|
|
762
|
+
? values.kana
|
|
763
|
+
: await textInput(values, signal);
|
|
434
764
|
const speakerRef = styleFlag(values, "speaker");
|
|
765
|
+
const entries = parseDictEntries(values);
|
|
435
766
|
const reading = await agent.reading(text, {
|
|
436
767
|
signal,
|
|
437
768
|
...(values.kana === undefined ? {} : { kana: values.kana }),
|
|
438
769
|
...(speakerRef === undefined ? {} : { speaker: speakerRef }),
|
|
770
|
+
...(values.engine === undefined ? {} : { engine: parseEngine(values.engine, "$flags.engine") }),
|
|
771
|
+
...(entries.length === 0
|
|
772
|
+
? {}
|
|
773
|
+
: {
|
|
774
|
+
dictionary: new LocalDictionary(entries.map((entry, index) => ({
|
|
775
|
+
id: `flag-${index + 1}`,
|
|
776
|
+
surface: entry.surface,
|
|
777
|
+
reading: entry.reading,
|
|
778
|
+
accent: entry.accent ?? null,
|
|
779
|
+
match: "word",
|
|
780
|
+
priority: 10,
|
|
781
|
+
caseSensitive: false,
|
|
782
|
+
}))),
|
|
783
|
+
}),
|
|
784
|
+
...(values["strict-reading"] === false ? { strict: false } : {}),
|
|
439
785
|
});
|
|
440
|
-
await emit({ ok: true, text, ...reading });
|
|
786
|
+
await emit({ ok: true, operation: "reading", text, ...reading });
|
|
441
787
|
return;
|
|
442
788
|
}
|
|
443
789
|
case "voices": {
|
|
444
790
|
const kind = literal(values.kind ?? "all", "$flags.kind", ["speech", "song", "all"]);
|
|
791
|
+
const engine = literal(values.engine ?? "all", "$flags.engine", ["formant", "voicevox", "auto", "all"]);
|
|
445
792
|
const query = values.query?.normalize("NFKC").toLowerCase();
|
|
446
|
-
const
|
|
447
|
-
|
|
793
|
+
const result = await agent.voices({ signal, engine, kind });
|
|
794
|
+
const voices = result.voices.filter((voice) => query === undefined ||
|
|
795
|
+
`${voice.id} ${voice.name} ${voice.character ?? ""}`.normalize("NFKC").toLowerCase().includes(query));
|
|
796
|
+
await emit({
|
|
797
|
+
ok: true,
|
|
798
|
+
operation: "voices",
|
|
799
|
+
kind,
|
|
800
|
+
engine,
|
|
801
|
+
count: voices.length,
|
|
802
|
+
voices,
|
|
803
|
+
voicevox: result.voicevox,
|
|
804
|
+
});
|
|
448
805
|
return;
|
|
449
806
|
}
|
|
450
807
|
case "doctor": {
|
|
451
|
-
const
|
|
808
|
+
const engine = values.engine === undefined
|
|
809
|
+
? undefined
|
|
810
|
+
: literal(values.engine, "$flags.engine", ["formant", "voicevox", "auto", "all"]);
|
|
811
|
+
const diagnosis = await agent.doctor({ signal, ...(engine === undefined ? {} : { engine }) });
|
|
452
812
|
const speakerRef = styleFlag(values, "speaker");
|
|
453
813
|
let style;
|
|
454
|
-
if (diagnosis.ok && speakerRef !== undefined) {
|
|
814
|
+
if (diagnosis.voicevox?.ok === true && speakerRef !== undefined) {
|
|
455
815
|
const all = await agent.styles.all({ signal });
|
|
456
816
|
const selected = all.find((s) => s.id === speakerRef || `${s.character}/${s.name}` === speakerRef || s.character === speakerRef);
|
|
457
817
|
if (selected === undefined)
|
|
@@ -460,7 +820,7 @@ async function main(argv) {
|
|
|
460
820
|
await agent.client.initializeSpeaker(selected.id, true, { signal });
|
|
461
821
|
style = { ...selected, initialized: await agent.client.isInitializedSpeaker(selected.id, { signal }) };
|
|
462
822
|
}
|
|
463
|
-
await emit({ ...diagnosis,
|
|
823
|
+
await emit({ ...diagnosis, operation: "doctor", ...(style === undefined ? {} : { style }) });
|
|
464
824
|
if (!diagnosis.ok)
|
|
465
825
|
process.exitCode = EXIT_CODES.engine;
|
|
466
826
|
return;
|
|
@@ -477,6 +837,12 @@ async function main(argv) {
|
|
|
477
837
|
process.removeListener("SIGTERM", cancel);
|
|
478
838
|
}
|
|
479
839
|
}
|
|
840
|
+
async function runValidateRequest(agent, request, values, signal) {
|
|
841
|
+
const result = await agent.validate(request, { signal });
|
|
842
|
+
await reportDiagnostics(values, result.warnings, values.input ?? "<request>");
|
|
843
|
+
const { request: resolved, ...rest } = result;
|
|
844
|
+
await emit({ ...rest, dryRun: true, request: resolved });
|
|
845
|
+
}
|
|
480
846
|
function isArgumentError(error) {
|
|
481
847
|
return (error instanceof Error &&
|
|
482
848
|
"code" in error &&
|
|
@@ -493,7 +859,7 @@ catch (error) {
|
|
|
493
859
|
message: error.message,
|
|
494
860
|
path: "$flags",
|
|
495
861
|
retryable: false,
|
|
496
|
-
hint: "Run kongyoroid --help.",
|
|
862
|
+
hint: "Run kongyoroid --help, or kongyoroid <command> --help for the accepted flags.",
|
|
497
863
|
}
|
|
498
864
|
: asKongyoroidError(error).toJSON();
|
|
499
865
|
process.exitCode = exitCodeOf(new KongyoroidError(data));
|