@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/kongyoroid.js
CHANGED
|
@@ -1,29 +1,107 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { performance } from "node:perf_hooks";
|
|
3
|
+
import process from "node:process";
|
|
3
4
|
import { DiskCache, LayeredCache, MemoryCache, cacheKey } from "./cache.js";
|
|
4
5
|
import { Semaphore } from "./concurrency.js";
|
|
5
|
-
import { KongyoroidError, asKongyoroidError, checkAbort } from "./errors.js";
|
|
6
|
-
import { formatKanaNotation, parseKanaNotation } from "./notation.js";
|
|
7
|
-
import { renderFormant } from "./formant.js";
|
|
6
|
+
import { KongyoroidError, asKongyoroidError, checkAbort, invalid } from "./errors.js";
|
|
8
7
|
import { DEFAULT_SAMPLE_RATE, LIMITS } from "./limits.js";
|
|
9
|
-
import { parseRequest, parseStyleRef } from "./request.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
8
|
+
import { parseEngine, parseRequest, parseStyleRef, requestHash } from "./request.js";
|
|
9
|
+
import { compileRequest, readForRequest, summarizePlan } from "./synth/engine.js";
|
|
10
|
+
import { encodePlanAsync, renderPcmStream } from "./synth/stream.js";
|
|
11
|
+
import { synthesizeTextStream } from "./synth/textstream.js";
|
|
12
|
+
import { BUILTIN_VOICES, describeVoice, resolveVoice, voiceProfileHash } from "./synth/voice.js";
|
|
13
|
+
import { estimatePitch, signalStats } from "./synth/analysis.js";
|
|
14
|
+
import { LocalDictionary, dictionaryFromJson } from "./text/dictionary.js";
|
|
15
|
+
import { frontendState, loadFrontend } from "./text/frontend.js";
|
|
16
|
+
import { formatKanaNotation, parseKanaNotation } from "./text/notation.js";
|
|
17
|
+
import { integer, isObject, literal } from "./validate.js";
|
|
18
|
+
import { ENGINE_VERSION, FRONTEND_VERSION, PACKAGE_NAME, PLAN_VERSION, VERSION } from "./version.js";
|
|
12
19
|
import { VoicevoxClient } from "./voicevox/client.js";
|
|
13
20
|
import { Dictionary } from "./voicevox/dictionary.js";
|
|
14
21
|
import { accentPhrasesToKana, synthesizeSpeech } from "./voicevox/speech.js";
|
|
15
22
|
import { synthesizeSong } from "./voicevox/song.js";
|
|
16
23
|
import { StyleCatalog } from "./voicevox/styles.js";
|
|
17
|
-
import { audioInfo, inspectWav } from "./wav.js";
|
|
24
|
+
import { audioInfo, decodeWav, inspectWav } from "./wav.js";
|
|
18
25
|
const PROBE_RETRY_MS = 30_000;
|
|
26
|
+
const PACK_VERSION = 2;
|
|
19
27
|
function packRender(entry) {
|
|
20
|
-
const meta = new TextEncoder().encode(JSON.stringify({
|
|
28
|
+
const meta = new TextEncoder().encode(JSON.stringify({
|
|
29
|
+
v: PACK_VERSION,
|
|
30
|
+
kana: entry.kana ?? null,
|
|
31
|
+
chunks: entry.chunks,
|
|
32
|
+
warnings: entry.warnings,
|
|
33
|
+
adjustments: entry.adjustments,
|
|
34
|
+
stats: entry.stats ?? null,
|
|
35
|
+
voice: entry.voice ?? null,
|
|
36
|
+
}));
|
|
21
37
|
const out = new Uint8Array(4 + meta.length + entry.audio.length);
|
|
22
38
|
new DataView(out.buffer).setUint32(0, meta.length, true);
|
|
23
39
|
out.set(meta, 4);
|
|
24
40
|
out.set(entry.audio, 4 + meta.length);
|
|
25
41
|
return out;
|
|
26
42
|
}
|
|
43
|
+
function statsFrom(value) {
|
|
44
|
+
if (!isObject(value))
|
|
45
|
+
return undefined;
|
|
46
|
+
const peak = value["peak"];
|
|
47
|
+
const rms = value["rms"];
|
|
48
|
+
const limitedSamples = value["limitedSamples"];
|
|
49
|
+
const frames = value["frames"];
|
|
50
|
+
if (typeof peak !== "number" ||
|
|
51
|
+
typeof rms !== "number" ||
|
|
52
|
+
typeof limitedSamples !== "number" ||
|
|
53
|
+
typeof frames !== "number") {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return { peak, rms, limitedSamples, frames };
|
|
57
|
+
}
|
|
58
|
+
function diagnosticsFrom(value) {
|
|
59
|
+
if (!Array.isArray(value))
|
|
60
|
+
return [];
|
|
61
|
+
const out = [];
|
|
62
|
+
for (const item of value) {
|
|
63
|
+
if (!isObject(item))
|
|
64
|
+
continue;
|
|
65
|
+
const severity = item["severity"];
|
|
66
|
+
const code = item["code"];
|
|
67
|
+
const message = item["message"];
|
|
68
|
+
if (severity !== "error" && severity !== "warning" && severity !== "advice")
|
|
69
|
+
continue;
|
|
70
|
+
if (typeof code !== "string" || typeof message !== "string")
|
|
71
|
+
continue;
|
|
72
|
+
const sourceSpan = item["sourceSpan"];
|
|
73
|
+
out.push({
|
|
74
|
+
severity,
|
|
75
|
+
code,
|
|
76
|
+
message,
|
|
77
|
+
...(typeof item["path"] === "string" ? { path: item["path"] } : {}),
|
|
78
|
+
...(typeof item["help"] === "string" ? { help: item["help"] } : {}),
|
|
79
|
+
...(typeof item["surface"] === "string" ? { surface: item["surface"] } : {}),
|
|
80
|
+
...(isObject(sourceSpan) &&
|
|
81
|
+
typeof sourceSpan["start"] === "number" &&
|
|
82
|
+
typeof sourceSpan["end"] === "number" &&
|
|
83
|
+
sourceSpan["unit"] === "unicode-code-point"
|
|
84
|
+
? { sourceSpan: { start: sourceSpan["start"], end: sourceSpan["end"], unit: "unicode-code-point" } }
|
|
85
|
+
: {}),
|
|
86
|
+
...(item["detail"] === undefined ? {} : { detail: item["detail"] }),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
function adjustmentsFrom(value) {
|
|
92
|
+
if (!Array.isArray(value))
|
|
93
|
+
return [];
|
|
94
|
+
const out = [];
|
|
95
|
+
for (const item of value) {
|
|
96
|
+
if (!isObject(item))
|
|
97
|
+
continue;
|
|
98
|
+
const code = item["code"];
|
|
99
|
+
const message = item["message"];
|
|
100
|
+
if (typeof code === "string" && typeof message === "string")
|
|
101
|
+
out.push({ ...item, code, message });
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
27
105
|
function unpackRender(bytes) {
|
|
28
106
|
if (bytes.length < 4)
|
|
29
107
|
return undefined;
|
|
@@ -32,7 +110,7 @@ function unpackRender(bytes) {
|
|
|
32
110
|
return undefined;
|
|
33
111
|
try {
|
|
34
112
|
const meta = JSON.parse(new TextDecoder().decode(bytes.subarray(4, 4 + length)));
|
|
35
|
-
if (!isObject(meta))
|
|
113
|
+
if (!isObject(meta) || meta["v"] !== PACK_VERSION)
|
|
36
114
|
return undefined;
|
|
37
115
|
const audio = bytes.slice(4 + length);
|
|
38
116
|
inspectWav(audio);
|
|
@@ -40,58 +118,156 @@ function unpackRender(bytes) {
|
|
|
40
118
|
audio,
|
|
41
119
|
kana: typeof meta["kana"] === "string" ? meta["kana"] : undefined,
|
|
42
120
|
chunks: typeof meta["chunks"] === "number" ? meta["chunks"] : 1,
|
|
121
|
+
warnings: diagnosticsFrom(meta["warnings"]),
|
|
122
|
+
adjustments: adjustmentsFrom(meta["adjustments"]),
|
|
123
|
+
stats: statsFrom(meta["stats"]),
|
|
124
|
+
voice: typeof meta["voice"] === "string" ? meta["voice"] : undefined,
|
|
43
125
|
};
|
|
44
126
|
}
|
|
45
127
|
catch {
|
|
46
128
|
return undefined;
|
|
47
129
|
}
|
|
48
130
|
}
|
|
131
|
+
function round(value, digits = 2) {
|
|
132
|
+
const factor = 10 ** digits;
|
|
133
|
+
return Math.round(value * factor) / factor;
|
|
134
|
+
}
|
|
49
135
|
export class Kongyoroid {
|
|
50
|
-
client;
|
|
51
|
-
styles;
|
|
52
136
|
dictionary;
|
|
137
|
+
clientInstance;
|
|
138
|
+
stylesInstance;
|
|
139
|
+
voicevoxDictionaryInstance;
|
|
140
|
+
voicevoxOptions;
|
|
53
141
|
defaults;
|
|
54
142
|
semaphore;
|
|
55
143
|
cache;
|
|
144
|
+
cacheStrict;
|
|
145
|
+
diskCache;
|
|
56
146
|
concurrency;
|
|
57
147
|
probeTimeoutMs;
|
|
148
|
+
inFlight = new Map();
|
|
58
149
|
probe;
|
|
59
150
|
constructor(options = {}) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
151
|
+
const voicevox = {
|
|
152
|
+
...(options.endpoint === undefined ? {} : { endpoint: options.endpoint }),
|
|
153
|
+
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
154
|
+
...(options.retries === undefined ? {} : { retries: options.retries }),
|
|
155
|
+
...(options.maxResponseBytes === undefined ? {} : { maxResponseBytes: options.maxResponseBytes }),
|
|
156
|
+
...(options.fetch === undefined ? {} : { fetch: options.fetch }),
|
|
157
|
+
...(options.headers === undefined ? {} : { headers: options.headers }),
|
|
158
|
+
...(options.speaker === undefined ? {} : { speaker: options.speaker }),
|
|
159
|
+
...(options.singer === undefined ? {} : { singer: options.singer }),
|
|
160
|
+
...(options.teacher === undefined ? {} : { teacher: options.teacher }),
|
|
161
|
+
...(options.probeTimeoutMs === undefined ? {} : { probeTimeoutMs: options.probeTimeoutMs }),
|
|
162
|
+
...(options.voicevox ?? {}),
|
|
163
|
+
};
|
|
164
|
+
this.voicevoxOptions = voicevox;
|
|
63
165
|
this.defaults = {
|
|
64
|
-
engine: options.engine,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
166
|
+
engine: options.engine ?? "formant",
|
|
167
|
+
voice: options.voice === undefined ? undefined : resolveVoice(options.voice, "$.voice").id,
|
|
168
|
+
speaker: voicevox.speaker === undefined ? undefined : parseStyleRef(voicevox.speaker, "$.speaker"),
|
|
169
|
+
singer: voicevox.singer === undefined ? undefined : parseStyleRef(voicevox.singer, "$.singer"),
|
|
170
|
+
teacher: voicevox.teacher === undefined ? undefined : parseStyleRef(voicevox.teacher, "$.teacher"),
|
|
171
|
+
strictReading: options.strictReading,
|
|
68
172
|
};
|
|
173
|
+
if (options.engine !== undefined)
|
|
174
|
+
parseEngine(options.engine, "$.engine");
|
|
175
|
+
this.dictionary =
|
|
176
|
+
options.dictionary === undefined
|
|
177
|
+
? new LocalDictionary()
|
|
178
|
+
: options.dictionary instanceof LocalDictionary
|
|
179
|
+
? options.dictionary
|
|
180
|
+
: dictionaryFromJson(options.dictionary, "$.dictionary");
|
|
69
181
|
this.concurrency = integer(options.concurrency ?? 2, "$.concurrency", 1, LIMITS.concurrency);
|
|
70
182
|
this.semaphore = new Semaphore(this.concurrency);
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
183
|
+
const cacheOptions = {
|
|
184
|
+
...(options.cacheDir === undefined ? {} : { directory: options.cacheDir }),
|
|
185
|
+
...(options.cacheBytes === undefined ? {} : { memoryBytes: options.cacheBytes }),
|
|
186
|
+
...(options.cache ?? {}),
|
|
187
|
+
};
|
|
188
|
+
this.cacheStrict = cacheOptions.strict ?? false;
|
|
189
|
+
if (cacheOptions.enabled === false) {
|
|
190
|
+
this.cache = undefined;
|
|
191
|
+
this.diskCache = undefined;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
const memory = new MemoryCache(cacheOptions.memoryBytes ?? 64 * 1024 * 1024);
|
|
195
|
+
if (cacheOptions.directory === undefined) {
|
|
196
|
+
this.cache = memory;
|
|
197
|
+
this.diskCache = undefined;
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
this.diskCache = new DiskCache(cacheOptions.directory, {
|
|
201
|
+
...(cacheOptions.maxBytes === undefined ? {} : { maxBytes: cacheOptions.maxBytes }),
|
|
202
|
+
...(cacheOptions.maxEntries === undefined ? {} : { maxEntries: cacheOptions.maxEntries }),
|
|
203
|
+
});
|
|
204
|
+
this.cache = new LayeredCache([memory, this.diskCache]);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
this.probeTimeoutMs = integer(voicevox.probeTimeoutMs ?? 3000, "$.probeTimeoutMs", 100, 600_000);
|
|
208
|
+
}
|
|
209
|
+
get client() {
|
|
210
|
+
this.clientInstance ??= new VoicevoxClient(this.voicevoxOptions);
|
|
211
|
+
return this.clientInstance;
|
|
212
|
+
}
|
|
213
|
+
get styles() {
|
|
214
|
+
this.stylesInstance ??= new StyleCatalog(this.client);
|
|
215
|
+
return this.stylesInstance;
|
|
216
|
+
}
|
|
217
|
+
get voicevoxDictionary() {
|
|
218
|
+
this.voicevoxDictionaryInstance ??= new Dictionary(this.client);
|
|
219
|
+
return this.voicevoxDictionaryInstance;
|
|
220
|
+
}
|
|
221
|
+
get defaultEngine() {
|
|
222
|
+
return this.defaults.engine;
|
|
223
|
+
}
|
|
224
|
+
get voices$() {
|
|
225
|
+
return BUILTIN_VOICES;
|
|
74
226
|
}
|
|
75
|
-
clearCache() {
|
|
76
|
-
return this.cache.clear();
|
|
227
|
+
async clearCache() {
|
|
228
|
+
return this.cache === undefined ? 0 : this.cache.clear();
|
|
229
|
+
}
|
|
230
|
+
async cacheStats() {
|
|
231
|
+
return this.cache === undefined ? null : this.cache.stats();
|
|
232
|
+
}
|
|
233
|
+
async pruneCache(maxBytes, maxEntries) {
|
|
234
|
+
return this.diskCache === undefined ? 0 : this.diskCache.prune(maxBytes, maxEntries);
|
|
77
235
|
}
|
|
78
236
|
withDefaults(request) {
|
|
79
237
|
if (!isObject(request))
|
|
80
238
|
return request;
|
|
81
239
|
const merged = { ...request };
|
|
82
|
-
|
|
240
|
+
const hasStyle = merged["speaker"] !== undefined || merged["singer"] !== undefined || merged["teacher"] !== undefined;
|
|
241
|
+
if (merged["engine"] === undefined && (this.defaults.engine !== "formant" || !hasStyle))
|
|
83
242
|
merged["engine"] = this.defaults.engine;
|
|
84
|
-
if (merged["
|
|
243
|
+
if (merged["voice"] === undefined && this.defaults.voice !== undefined)
|
|
244
|
+
merged["voice"] = this.defaults.voice;
|
|
245
|
+
if (merged["strictReading"] === undefined && this.defaults.strictReading !== undefined)
|
|
246
|
+
merged["strictReading"] = this.defaults.strictReading;
|
|
247
|
+
const engine = merged["engine"];
|
|
248
|
+
const wantsVoicevox = engine === "voicevox" || engine === "auto" || hasStyle;
|
|
249
|
+
if (wantsVoicevox &&
|
|
250
|
+
merged["kind"] === "speech" &&
|
|
251
|
+
merged["speaker"] === undefined &&
|
|
252
|
+
this.defaults.speaker !== undefined) {
|
|
85
253
|
merged["speaker"] = this.defaults.speaker;
|
|
86
254
|
}
|
|
87
|
-
if (merged["kind"] === "song") {
|
|
255
|
+
if (wantsVoicevox && merged["kind"] === "song") {
|
|
88
256
|
if (merged["singer"] === undefined && this.defaults.singer !== undefined)
|
|
89
257
|
merged["singer"] = this.defaults.singer;
|
|
90
258
|
if (merged["teacher"] === undefined && this.defaults.teacher !== undefined)
|
|
91
259
|
merged["teacher"] = this.defaults.teacher;
|
|
92
260
|
}
|
|
261
|
+
if (merged["engine"] === "formant" && hasStyle) {
|
|
262
|
+
if (this.defaults.engine === "formant" && isObject(request) && request["engine"] === undefined)
|
|
263
|
+
delete merged["engine"];
|
|
264
|
+
}
|
|
93
265
|
return merged;
|
|
94
266
|
}
|
|
267
|
+
parse(request) {
|
|
268
|
+
const resolved = parseRequest(this.withDefaults(request));
|
|
269
|
+
return { request: resolved, requestHash: requestHash({ ...resolved, dictionaryDigest: this.dictionary.digest() }) };
|
|
270
|
+
}
|
|
95
271
|
resolveEngine(selector, options = {}) {
|
|
96
272
|
if (selector !== "auto")
|
|
97
273
|
return Promise.resolve(selector);
|
|
@@ -121,7 +297,7 @@ export class Kongyoroid {
|
|
|
121
297
|
try {
|
|
122
298
|
const words = await this.client.dictionary(options);
|
|
123
299
|
const rows = words
|
|
124
|
-
.map((word) => [word.uuid, word.surface, word.pronunciation, word.accentType, word.priority].join("
|
|
300
|
+
.map((word) => [word.uuid, word.surface, word.pronunciation, word.accentType, word.priority].join(""))
|
|
125
301
|
.sort();
|
|
126
302
|
return createHash("sha256").update(rows.join("\n")).digest("hex");
|
|
127
303
|
}
|
|
@@ -143,56 +319,301 @@ export class Kongyoroid {
|
|
|
143
319
|
: await this.styles.resolve(undefined, "teacher", options);
|
|
144
320
|
return { singer, teacher };
|
|
145
321
|
}
|
|
322
|
+
compileOptions(options) {
|
|
323
|
+
return {
|
|
324
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
325
|
+
dictionary: this.dictionary,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
async compile(request, options = {}) {
|
|
329
|
+
const parsed = this.parse(request);
|
|
330
|
+
const engine = await this.resolveEngine(parsed.request.engine, options);
|
|
331
|
+
if (engine !== "formant") {
|
|
332
|
+
invalid("$.engine", "Plans can only be compiled for the formant engine.", {
|
|
333
|
+
hint: 'Set engine to "formant" (or omit the VOICEVOX style) to inspect the plan.',
|
|
334
|
+
repairOptions: [{ action: "use-formant-engine", description: "Use the built-in engine for plan inspection." }],
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
const sampleRate = parsed.request.sampleRate ?? DEFAULT_SAMPLE_RATE;
|
|
338
|
+
const compiled = await compileRequest(parsed.request, sampleRate, this.compileOptions(options));
|
|
339
|
+
return { ...compiled, request: parsed.request, requestHash: parsed.requestHash, engine: "formant" };
|
|
340
|
+
}
|
|
341
|
+
async validate(request, options = {}) {
|
|
342
|
+
const parsed = this.parse(request);
|
|
343
|
+
const resolved = parsed.request;
|
|
344
|
+
const engine = resolved.engine === "auto" ? "auto" : resolved.engine;
|
|
345
|
+
if (engine !== "formant") {
|
|
346
|
+
return {
|
|
347
|
+
ok: true,
|
|
348
|
+
operation: "validate",
|
|
349
|
+
engine,
|
|
350
|
+
kind: resolved.kind,
|
|
351
|
+
renderable: "unknown",
|
|
352
|
+
estimate: null,
|
|
353
|
+
voice: undefined,
|
|
354
|
+
reading: null,
|
|
355
|
+
notes: resolved.kind === "song" ? resolved.notes.length : null,
|
|
356
|
+
warnings: [
|
|
357
|
+
{
|
|
358
|
+
severity: "advice",
|
|
359
|
+
code: "EXTERNAL_ENGINE_NOT_PROBED",
|
|
360
|
+
message: "The request targets VOICEVOX; structure was validated but the engine was not contacted.",
|
|
361
|
+
help: "Run doctor --engine voicevox to check the endpoint.",
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
adjustments: [],
|
|
365
|
+
requestHash: parsed.requestHash,
|
|
366
|
+
planHash: null,
|
|
367
|
+
request: resolved,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
const sampleRate = resolved.sampleRate ?? DEFAULT_SAMPLE_RATE;
|
|
371
|
+
const compiled = await compileRequest(resolved, sampleRate, this.compileOptions(options));
|
|
372
|
+
const plan = compiled.plan;
|
|
373
|
+
return {
|
|
374
|
+
ok: true,
|
|
375
|
+
operation: "validate",
|
|
376
|
+
engine: "formant",
|
|
377
|
+
kind: resolved.kind,
|
|
378
|
+
renderable: true,
|
|
379
|
+
estimate: {
|
|
380
|
+
durationSeconds: round(plan.frames / sampleRate, 3),
|
|
381
|
+
frames: plan.frames,
|
|
382
|
+
wavBytes: 44 + plan.frames * 2,
|
|
383
|
+
sampleRate,
|
|
384
|
+
},
|
|
385
|
+
voice: plan.voice.id,
|
|
386
|
+
reading: plan.reading === null
|
|
387
|
+
? null
|
|
388
|
+
: { kana: plan.reading.kana, frontend: plan.reading.frontend, moraCount: plan.reading.moraCount },
|
|
389
|
+
notes: resolved.kind === "song" ? resolved.notes.length : null,
|
|
390
|
+
warnings: plan.warnings,
|
|
391
|
+
adjustments: plan.adjustments,
|
|
392
|
+
requestHash: parsed.requestHash,
|
|
393
|
+
planHash: compiled.planHash,
|
|
394
|
+
request: resolved,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
async plan(request, options = {}) {
|
|
398
|
+
const resolved = typeof options === "string" ? { detail: options } : options;
|
|
399
|
+
const detail = literal(resolved.detail ?? "summary", "$.detail", ["summary", "phonemes", "acoustics"]);
|
|
400
|
+
const compiled = await this.compile(request, resolved);
|
|
401
|
+
return {
|
|
402
|
+
ok: true,
|
|
403
|
+
operation: "plan",
|
|
404
|
+
engine: "formant",
|
|
405
|
+
requestHash: compiled.requestHash,
|
|
406
|
+
plan: summarizePlan(compiled.plan, compiled.planHash, detail),
|
|
407
|
+
request: compiled.request,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
async renderPlan(plan, options = {}) {
|
|
411
|
+
const start = performance.now();
|
|
412
|
+
checkAbort(options.signal);
|
|
413
|
+
const encoded = await encodePlanAsync(plan, options);
|
|
414
|
+
const renderMs = performance.now() - start;
|
|
415
|
+
return this.formantResult({
|
|
416
|
+
audio: encoded.audio,
|
|
417
|
+
kana: plan.reading?.kana,
|
|
418
|
+
chunks: 1,
|
|
419
|
+
warnings: plan.warnings,
|
|
420
|
+
adjustments: plan.adjustments,
|
|
421
|
+
stats: encoded.stats,
|
|
422
|
+
voice: plan.voice.id,
|
|
423
|
+
}, plan.kind, false, options.requestHash ?? "", {
|
|
424
|
+
readMs: options.readMs ?? 0,
|
|
425
|
+
planMs: options.planMs ?? 0,
|
|
426
|
+
renderMs,
|
|
427
|
+
encodeMs: 0,
|
|
428
|
+
totalMs: (options.readMs ?? 0) + (options.planMs ?? 0) + renderMs,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
formantResult(entry, kind, cached, hash, timings) {
|
|
432
|
+
return {
|
|
433
|
+
audio: entry.audio,
|
|
434
|
+
info: audioInfo(inspectWav(entry.audio)),
|
|
435
|
+
engine: "formant",
|
|
436
|
+
kind,
|
|
437
|
+
voice: entry.voice,
|
|
438
|
+
styles: {},
|
|
439
|
+
kana: entry.kana,
|
|
440
|
+
chunks: entry.chunks,
|
|
441
|
+
cached,
|
|
442
|
+
sha256: createHash("sha256").update(entry.audio).digest("hex"),
|
|
443
|
+
requestHash: hash,
|
|
444
|
+
elapsedMs: round(timings.totalMs),
|
|
445
|
+
timings: {
|
|
446
|
+
readMs: round(timings.readMs),
|
|
447
|
+
planMs: round(timings.planMs),
|
|
448
|
+
renderMs: round(timings.renderMs),
|
|
449
|
+
encodeMs: round(timings.encodeMs),
|
|
450
|
+
totalMs: round(timings.totalMs),
|
|
451
|
+
},
|
|
452
|
+
warnings: entry.warnings,
|
|
453
|
+
adjustments: entry.adjustments,
|
|
454
|
+
peak: entry.stats === undefined ? undefined : round(entry.stats.peak, 4),
|
|
455
|
+
rms: entry.stats === undefined ? undefined : round(entry.stats.rms, 4),
|
|
456
|
+
limitedSamples: entry.stats?.limitedSamples,
|
|
457
|
+
engineVersion: ENGINE_VERSION,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
async cacheGet(key, warnings) {
|
|
461
|
+
if (this.cache === undefined)
|
|
462
|
+
return undefined;
|
|
463
|
+
try {
|
|
464
|
+
const bytes = await this.cache.get(key);
|
|
465
|
+
return bytes === undefined ? undefined : unpackRender(bytes);
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
if (this.cacheStrict)
|
|
469
|
+
throw asKongyoroidError(error);
|
|
470
|
+
warnings.push({
|
|
471
|
+
severity: "warning",
|
|
472
|
+
code: "CACHE_READ_FAILED",
|
|
473
|
+
message: `The render cache could not be read: ${error instanceof Error ? error.message : String(error)}`,
|
|
474
|
+
});
|
|
475
|
+
return undefined;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
async cacheSet(key, entry, warnings) {
|
|
479
|
+
if (this.cache === undefined)
|
|
480
|
+
return;
|
|
481
|
+
try {
|
|
482
|
+
await this.cache.set(key, packRender(entry));
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
if (this.cacheStrict)
|
|
486
|
+
throw asKongyoroidError(error);
|
|
487
|
+
warnings.push({
|
|
488
|
+
severity: "warning",
|
|
489
|
+
code: "CACHE_WRITE_FAILED",
|
|
490
|
+
message: `The render was produced but could not be cached: ${error instanceof Error ? error.message : String(error)}`,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
146
494
|
async render(request, options = {}) {
|
|
147
495
|
const start = performance.now();
|
|
148
496
|
checkAbort(options.signal);
|
|
149
|
-
const
|
|
497
|
+
const parsed = this.parse(request);
|
|
498
|
+
const resolved = parsed.request;
|
|
150
499
|
const engine = await this.resolveEngine(resolved.engine, options);
|
|
500
|
+
const cacheWarnings = [];
|
|
151
501
|
const styles = engine === "voicevox" ? await this.resolveStyles(resolved, options) : {};
|
|
152
502
|
const manifest = engine === "voicevox" ? await this.client.manifest(options) : undefined;
|
|
153
|
-
const dictionary = engine === "voicevox" && resolved.kind === "speech"
|
|
503
|
+
const dictionary = engine === "voicevox" && resolved.kind === "speech"
|
|
504
|
+
? await this.dictionaryDigest(options)
|
|
505
|
+
: this.dictionary.digest();
|
|
154
506
|
const key = dictionary === undefined
|
|
155
507
|
? undefined
|
|
156
|
-
: cacheKey(engine, VERSION, engine === "voicevox"
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
508
|
+
: cacheKey(engine, VERSION, engine === "voicevox"
|
|
509
|
+
? [this.client.endpoint.href, manifest?.version ?? "", styles, dictionary]
|
|
510
|
+
: [ENGINE_VERSION, FRONTEND_VERSION, PLAN_VERSION, dictionary], resolved);
|
|
511
|
+
const cached = key === undefined ? undefined : await this.cacheGet(key, cacheWarnings);
|
|
512
|
+
checkAbort(options.signal);
|
|
513
|
+
let timings = { readMs: 0, planMs: 0, renderMs: 0, encodeMs: 0, totalMs: 0 };
|
|
514
|
+
let entry;
|
|
515
|
+
if (cached !== undefined) {
|
|
516
|
+
entry = cached;
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
const shared = key === undefined ? undefined : this.inFlight.get(key);
|
|
520
|
+
if (shared !== undefined) {
|
|
521
|
+
entry = await shared;
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
const job = this.semaphore.run(async () => {
|
|
525
|
+
checkAbort(options.signal);
|
|
526
|
+
if (engine === "formant") {
|
|
527
|
+
const sampleRate = resolved.sampleRate ?? DEFAULT_SAMPLE_RATE;
|
|
528
|
+
const compiled = await compileRequest(resolved, sampleRate, this.compileOptions(options));
|
|
529
|
+
const renderStart = performance.now();
|
|
530
|
+
const encoded = await encodePlanAsync(compiled.plan, options.signal === undefined ? {} : { signal: options.signal });
|
|
531
|
+
const renderMs = performance.now() - renderStart;
|
|
532
|
+
timings = { readMs: compiled.readMs, planMs: compiled.planMs, renderMs, encodeMs: 0, totalMs: 0 };
|
|
533
|
+
return {
|
|
534
|
+
audio: encoded.audio,
|
|
535
|
+
kana: compiled.plan.reading?.kana,
|
|
536
|
+
chunks: compiled.plan.reading?.sentences.length ?? 1,
|
|
537
|
+
warnings: compiled.plan.warnings,
|
|
538
|
+
adjustments: compiled.plan.adjustments,
|
|
539
|
+
stats: encoded.stats,
|
|
540
|
+
voice: compiled.plan.voice.id,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
if (resolved.kind === "speech") {
|
|
544
|
+
const speaker = styles.speaker;
|
|
545
|
+
if (speaker === undefined)
|
|
546
|
+
throw asKongyoroidError(new Error("speaker was not resolved"));
|
|
547
|
+
const result = await synthesizeSpeech(this.client, resolved, speaker, {
|
|
548
|
+
...options,
|
|
549
|
+
concurrency: this.concurrency,
|
|
550
|
+
});
|
|
551
|
+
return {
|
|
552
|
+
audio: result.audio,
|
|
553
|
+
kana: result.kana,
|
|
554
|
+
chunks: result.chunks,
|
|
555
|
+
warnings: [],
|
|
556
|
+
adjustments: [],
|
|
557
|
+
stats: undefined,
|
|
558
|
+
voice: undefined,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
const singer = styles.singer;
|
|
562
|
+
const teacher = styles.teacher;
|
|
563
|
+
if (singer === undefined || teacher === undefined)
|
|
564
|
+
throw asKongyoroidError(new Error("singer was not resolved"));
|
|
565
|
+
const result = await synthesizeSong(this.client, resolved, singer, teacher, options);
|
|
566
|
+
return {
|
|
567
|
+
audio: result.audio,
|
|
568
|
+
kana: undefined,
|
|
569
|
+
chunks: 1,
|
|
570
|
+
warnings: [],
|
|
571
|
+
adjustments: [],
|
|
572
|
+
stats: undefined,
|
|
573
|
+
voice: undefined,
|
|
574
|
+
};
|
|
575
|
+
}, options.signal);
|
|
576
|
+
if (key !== undefined) {
|
|
577
|
+
this.inFlight.set(key, job);
|
|
578
|
+
job
|
|
579
|
+
.finally(() => {
|
|
580
|
+
if (this.inFlight.get(key) === job)
|
|
581
|
+
this.inFlight.delete(key);
|
|
582
|
+
})
|
|
583
|
+
.catch(() => undefined);
|
|
179
584
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
585
|
+
entry = await job;
|
|
586
|
+
if (key !== undefined)
|
|
587
|
+
await this.cacheSet(key, entry, cacheWarnings);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
checkAbort(options.signal);
|
|
591
|
+
const total = performance.now() - start;
|
|
592
|
+
const finalTimings = { ...timings, totalMs: total };
|
|
593
|
+
const warnings = cacheWarnings.length === 0 ? entry.warnings : [...entry.warnings, ...cacheWarnings];
|
|
594
|
+
if (engine === "formant") {
|
|
595
|
+
return this.formantResult({ ...entry, warnings }, resolved.kind, cached !== undefined, parsed.requestHash, finalTimings);
|
|
596
|
+
}
|
|
185
597
|
return {
|
|
186
598
|
audio: entry.audio,
|
|
187
599
|
info: audioInfo(inspectWav(entry.audio)),
|
|
188
600
|
engine,
|
|
189
601
|
kind: resolved.kind,
|
|
602
|
+
voice: undefined,
|
|
190
603
|
styles,
|
|
191
604
|
kana: entry.kana,
|
|
192
605
|
chunks: entry.chunks,
|
|
193
606
|
cached: cached !== undefined,
|
|
194
607
|
sha256: createHash("sha256").update(entry.audio).digest("hex"),
|
|
195
|
-
|
|
608
|
+
requestHash: parsed.requestHash,
|
|
609
|
+
elapsedMs: round(total),
|
|
610
|
+
timings: { readMs: 0, planMs: 0, renderMs: round(total), encodeMs: 0, totalMs: round(total) },
|
|
611
|
+
warnings,
|
|
612
|
+
adjustments: [],
|
|
613
|
+
peak: undefined,
|
|
614
|
+
rms: undefined,
|
|
615
|
+
limitedSamples: undefined,
|
|
616
|
+
engineVersion: manifest?.version ?? "voicevox",
|
|
196
617
|
};
|
|
197
618
|
}
|
|
198
619
|
speak(input, options = {}) {
|
|
@@ -201,41 +622,214 @@ export class Kongyoroid {
|
|
|
201
622
|
sing(input, options = {}) {
|
|
202
623
|
return this.render({ kind: "song", ...input }, options);
|
|
203
624
|
}
|
|
625
|
+
async *renderStream(request, options = {}) {
|
|
626
|
+
const compiled = await this.compile(request, options.signal === undefined ? {} : { signal: options.signal });
|
|
627
|
+
const iterator = renderPcmStream(compiled.plan, options);
|
|
628
|
+
while (true) {
|
|
629
|
+
const next = await iterator.next();
|
|
630
|
+
if (next.done)
|
|
631
|
+
return { plan: compiled.plan, stats: next.value };
|
|
632
|
+
yield next.value;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
speakStream(chunks, input = {}, options = {}) {
|
|
636
|
+
const parsed = this.parse({ ...input, kind: "speech", text: "placeholder" });
|
|
637
|
+
const request = parsed.request;
|
|
638
|
+
if (request.kind !== "speech")
|
|
639
|
+
invalid("$.kind", "Streaming synthesis needs a speech request.");
|
|
640
|
+
if (request.kana !== undefined) {
|
|
641
|
+
invalid("$.kana", "Streaming synthesis reads every sentence from the streamed text; kana cannot be given.", {
|
|
642
|
+
hint: "Drop kana and use a dictionary for readings, or synthesize the whole text at once with kana.",
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
if (request.engine === "voicevox") {
|
|
646
|
+
invalid("$.engine", "Streaming synthesis is only available with the formant engine.", {
|
|
647
|
+
hint: 'Set engine to "formant" (or "auto", which streams with the built-in engine) and drop the speaker.',
|
|
648
|
+
repairOptions: [{ action: "use-formant-engine", description: "Use engine formant for streaming." }],
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
const resolved = request.engine === "formant" ? request : { ...request, engine: "formant" };
|
|
652
|
+
return synthesizeTextStream(chunks, resolved, {
|
|
653
|
+
...options,
|
|
654
|
+
dictionary: this.dictionary,
|
|
655
|
+
sampleRate: options.sampleRate ?? resolved.sampleRate ?? DEFAULT_SAMPLE_RATE,
|
|
656
|
+
});
|
|
657
|
+
}
|
|
204
658
|
async reading(text, options = {}) {
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
659
|
+
const engine = await this.resolveEngine(options.engine ?? (options.speaker !== undefined ? "voicevox" : this.defaults.engine), options);
|
|
660
|
+
if (engine === "voicevox") {
|
|
661
|
+
const speaker = await this.styles.resolve(options.speaker ?? this.defaults.speaker, "speaker", options);
|
|
662
|
+
const phrases = options.kana === undefined
|
|
663
|
+
? (await this.client.audioQuery(text, speaker.id, options)).accent_phrases
|
|
664
|
+
: await this.client.accentPhrases(formatKanaNotation(parseKanaNotation(options.kana, "$.kana")), speaker.id, true, options);
|
|
665
|
+
return {
|
|
666
|
+
engine: "voicevox",
|
|
667
|
+
frontend: "voicevox",
|
|
668
|
+
kana: accentPhrasesToKana(phrases),
|
|
669
|
+
speaker,
|
|
670
|
+
warnings: [],
|
|
671
|
+
dictionaryHits: [],
|
|
672
|
+
sentences: [],
|
|
673
|
+
phrases: phrases.map((phrase) => ({
|
|
674
|
+
text: phrase.moras.map((mora) => mora.text).join(""),
|
|
675
|
+
accent: phrase.accent,
|
|
676
|
+
accentSource: "frontend",
|
|
677
|
+
boundary: phrase.pause_mora !== null ? "pause" : "phrase",
|
|
678
|
+
pause: phrase.pause_mora !== null,
|
|
679
|
+
interrogative: phrase.is_interrogative,
|
|
680
|
+
moras: phrase.moras.map((mora) => ({
|
|
681
|
+
text: mora.text,
|
|
682
|
+
consonant: mora.consonant,
|
|
683
|
+
vowel: mora.vowel,
|
|
684
|
+
consonantLength: mora.consonant_length,
|
|
685
|
+
vowelLength: mora.vowel_length,
|
|
686
|
+
pitch: mora.pitch,
|
|
687
|
+
})),
|
|
224
688
|
})),
|
|
225
|
-
}
|
|
226
|
-
}
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
const request = this.parse({
|
|
692
|
+
kind: "speech",
|
|
693
|
+
text,
|
|
694
|
+
...(options.kana === undefined ? {} : { kana: options.kana }),
|
|
695
|
+
...(options.strict === undefined ? {} : { strictReading: options.strict }),
|
|
696
|
+
}).request;
|
|
697
|
+
if (request.kind !== "speech")
|
|
698
|
+
invalid("$.kind", "Reading analysis needs a speech request.");
|
|
699
|
+
const read = await readForRequest(request, {
|
|
700
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
701
|
+
dictionary: options.dictionary === undefined ? this.dictionary : this.dictionary.merge(options.dictionary),
|
|
702
|
+
});
|
|
703
|
+
return readingFromPlan(read.reading);
|
|
227
704
|
}
|
|
228
|
-
async voices(
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
705
|
+
async voices(options = {}) {
|
|
706
|
+
const engine = options.engine ?? "all";
|
|
707
|
+
const kind = options.kind ?? "all";
|
|
708
|
+
const voices = [];
|
|
709
|
+
if (engine === "formant" || engine === "all" || engine === "auto") {
|
|
710
|
+
for (const profile of BUILTIN_VOICES) {
|
|
711
|
+
const described = describeVoice(profile);
|
|
712
|
+
voices.push({
|
|
713
|
+
id: described.id,
|
|
714
|
+
engine: "formant",
|
|
715
|
+
name: described.name,
|
|
716
|
+
character: null,
|
|
717
|
+
description: described.description,
|
|
718
|
+
kinds: ["speech", "song"],
|
|
719
|
+
styleId: null,
|
|
720
|
+
type: null,
|
|
721
|
+
baseF0: described.baseF0,
|
|
722
|
+
f0Range: described.f0Range,
|
|
723
|
+
hash: described.hash,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
let voicevox = { queried: false, available: false, error: null };
|
|
728
|
+
if (engine === "voicevox" || engine === "all" || engine === "auto") {
|
|
729
|
+
try {
|
|
730
|
+
const all = await this.styles.all(options);
|
|
731
|
+
const filtered = all.filter((style) => kind === "all"
|
|
732
|
+
? true
|
|
733
|
+
: kind === "speech"
|
|
734
|
+
? style.type === "talk" || style.type === "streaming_talk"
|
|
735
|
+
: style.type === "frame_decode" || style.type === "sing" || style.type === "singing_teacher");
|
|
736
|
+
for (const style of filtered)
|
|
737
|
+
voices.push(voicevoxDescriptor(style));
|
|
738
|
+
voicevox = { queried: true, available: true, error: null };
|
|
739
|
+
}
|
|
740
|
+
catch (error) {
|
|
741
|
+
const failure = asKongyoroidError(error);
|
|
742
|
+
if (failure.code === "ABORTED" || engine === "voicevox")
|
|
743
|
+
throw failure;
|
|
744
|
+
voicevox = { queried: true, available: false, error: failure.toJSON() };
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return { voices, voicevox };
|
|
235
748
|
}
|
|
236
749
|
async doctor(options = {}) {
|
|
237
|
-
const
|
|
750
|
+
const engine = options.engine ?? this.defaults.engine;
|
|
751
|
+
const checkVoicevox = engine === "voicevox" || engine === "auto" || engine === "all";
|
|
752
|
+
const checkFormant = engine === "formant" || engine === "auto" || engine === "all" || options.frontend === true;
|
|
753
|
+
let frontend = { available: false, version: FRONTEND_VERSION, loadMs: null, error: null };
|
|
754
|
+
let synthesis = { ok: false, ms: 0, frames: 0, error: null };
|
|
755
|
+
if (checkFormant) {
|
|
756
|
+
const started = performance.now();
|
|
757
|
+
try {
|
|
758
|
+
await loadFrontend();
|
|
759
|
+
frontend = {
|
|
760
|
+
available: true,
|
|
761
|
+
version: FRONTEND_VERSION,
|
|
762
|
+
loadMs: round(performance.now() - started),
|
|
763
|
+
error: null,
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
catch (error) {
|
|
767
|
+
frontend = {
|
|
768
|
+
available: false,
|
|
769
|
+
version: FRONTEND_VERSION,
|
|
770
|
+
loadMs: round(performance.now() - started),
|
|
771
|
+
error: asKongyoroidError(error).toJSON(),
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
const synthStart = performance.now();
|
|
775
|
+
try {
|
|
776
|
+
const compiled = await this.compile({ kind: "speech", engine: "formant", text: "てすと", kana: "テ'_スト", prePause: 0, postPause: 0 }, options);
|
|
777
|
+
const encoded = await encodePlanAsync(compiled.plan, options.signal === undefined ? {} : { signal: options.signal });
|
|
778
|
+
synthesis = {
|
|
779
|
+
ok: encoded.audio.length > 44 && encoded.stats.peak > 0.01,
|
|
780
|
+
ms: round(performance.now() - synthStart),
|
|
781
|
+
frames: compiled.plan.frames,
|
|
782
|
+
error: null,
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
catch (error) {
|
|
786
|
+
synthesis = {
|
|
787
|
+
ok: false,
|
|
788
|
+
ms: round(performance.now() - synthStart),
|
|
789
|
+
frames: 0,
|
|
790
|
+
error: asKongyoroidError(error).toJSON(),
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
let cache = { directory: this.diskCache?.directory ?? null, stats: null, error: null };
|
|
795
|
+
if (this.cache !== undefined) {
|
|
796
|
+
try {
|
|
797
|
+
cache = { directory: this.diskCache?.directory ?? null, stats: await this.cache.stats(), error: null };
|
|
798
|
+
}
|
|
799
|
+
catch (error) {
|
|
800
|
+
cache = { directory: this.diskCache?.directory ?? null, stats: null, error: asKongyoroidError(error).toJSON() };
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
let voicevox = null;
|
|
804
|
+
if (checkVoicevox)
|
|
805
|
+
voicevox = await this.voicevoxDoctor(options);
|
|
806
|
+
const formantOk = !checkFormant || (frontend.available && synthesis.ok);
|
|
807
|
+
const voicevoxOk = voicevox === null || voicevox.ok;
|
|
808
|
+
const ok = engine === "auto"
|
|
809
|
+
? formantOk
|
|
810
|
+
: engine === "all"
|
|
811
|
+
? formantOk && voicevoxOk
|
|
812
|
+
: engine === "voicevox"
|
|
813
|
+
? voicevoxOk
|
|
814
|
+
: formantOk;
|
|
815
|
+
return {
|
|
816
|
+
ok,
|
|
817
|
+
name: PACKAGE_NAME,
|
|
818
|
+
version: VERSION,
|
|
819
|
+
engineVersion: ENGINE_VERSION,
|
|
820
|
+
node: process.version,
|
|
821
|
+
defaultEngine: this.defaults.engine,
|
|
822
|
+
formant: { ok: formantOk, voices: BUILTIN_VOICES.map((voice) => voice.id), frontend, synthesis },
|
|
823
|
+
cache,
|
|
824
|
+
dictionary: this.dictionary.size === 0 ? null : { entries: this.dictionary.size, digest: this.dictionary.digest() },
|
|
825
|
+
voicevox,
|
|
826
|
+
error: ok ? null : (voicevox?.error ?? frontend.error ?? synthesis.error ?? null),
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
async voicevoxDoctor(options = {}) {
|
|
830
|
+
let endpoint = "";
|
|
238
831
|
try {
|
|
832
|
+
endpoint = this.client.endpoint.origin;
|
|
239
833
|
const version = await this.client.version(options);
|
|
240
834
|
const manifest = await this.client.manifest(options);
|
|
241
835
|
const devices = await this.client.supportedDevices(options).catch(() => null);
|
|
@@ -271,5 +865,111 @@ export class Kongyoroid {
|
|
|
271
865
|
};
|
|
272
866
|
}
|
|
273
867
|
}
|
|
868
|
+
inspect(audio, options = {}) {
|
|
869
|
+
const layout = inspectWav(audio);
|
|
870
|
+
const decoded = decodeWav(audio);
|
|
871
|
+
const stats = signalStats(decoded.samples);
|
|
872
|
+
const window = Math.max(256, Math.round(((options.windowMs ?? 40) / 1000) * layout.sampleRate));
|
|
873
|
+
const hop = Math.round(window / 2);
|
|
874
|
+
const track = [];
|
|
875
|
+
const voiced = [];
|
|
876
|
+
let windows = 0;
|
|
877
|
+
for (let start = 0; start + window <= decoded.samples.length; start += hop) {
|
|
878
|
+
windows += 1;
|
|
879
|
+
const estimate = estimatePitch(decoded.samples, layout.sampleRate, start, start + window, 50, 1500);
|
|
880
|
+
const frameStats = signalStats(decoded.samples, start, start + window);
|
|
881
|
+
const isVoiced = estimate.clarity > 0.7 && frameStats.rms > 0.01;
|
|
882
|
+
if (isVoiced)
|
|
883
|
+
voiced.push(estimate.hz);
|
|
884
|
+
if (options.pitchTrack === true)
|
|
885
|
+
track.push({
|
|
886
|
+
seconds: round(start / layout.sampleRate, 4),
|
|
887
|
+
hz: isVoiced ? round(estimate.hz, 1) : 0,
|
|
888
|
+
clarity: round(estimate.clarity, 3),
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
const sorted = [...voiced].sort((a, b) => a - b);
|
|
892
|
+
const median = sorted.length === 0 ? null : (sorted[Math.floor(sorted.length / 2)] ?? null);
|
|
893
|
+
return {
|
|
894
|
+
info: audioInfo(layout),
|
|
895
|
+
bytes: audio.byteLength,
|
|
896
|
+
sha256: createHash("sha256").update(audio).digest("hex"),
|
|
897
|
+
peak: round(stats.peak, 4),
|
|
898
|
+
rms: round(stats.rms, 4),
|
|
899
|
+
dc: round(stats.dc, 5),
|
|
900
|
+
clipped: stats.clipped,
|
|
901
|
+
finite: stats.finite,
|
|
902
|
+
pitch: {
|
|
903
|
+
medianHz: median === null ? null : round(median, 1),
|
|
904
|
+
minHz: sorted.length === 0 ? null : round(sorted[0] ?? 0, 1),
|
|
905
|
+
maxHz: sorted.length === 0 ? null : round(sorted[sorted.length - 1] ?? 0, 1),
|
|
906
|
+
voicedRatio: windows === 0 ? 0 : round(voiced.length / windows, 3),
|
|
907
|
+
...(options.pitchTrack === true ? { track } : {}),
|
|
908
|
+
},
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
frontendStatus() {
|
|
912
|
+
return frontendState();
|
|
913
|
+
}
|
|
914
|
+
voiceProfile(id) {
|
|
915
|
+
return resolveVoice(id ?? this.defaults.voice, "$.voice");
|
|
916
|
+
}
|
|
917
|
+
voiceHash(id) {
|
|
918
|
+
return voiceProfileHash(this.voiceProfile(id));
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
function voicevoxDescriptor(style) {
|
|
922
|
+
const speech = style.type === "talk" || style.type === "streaming_talk";
|
|
923
|
+
return {
|
|
924
|
+
id: `voicevox:${style.id}`,
|
|
925
|
+
engine: "voicevox",
|
|
926
|
+
name: style.name,
|
|
927
|
+
character: style.character,
|
|
928
|
+
description: null,
|
|
929
|
+
kinds: speech ? ["speech"] : ["song"],
|
|
930
|
+
styleId: style.id,
|
|
931
|
+
type: style.type,
|
|
932
|
+
baseF0: null,
|
|
933
|
+
f0Range: null,
|
|
934
|
+
hash: null,
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
export function readingFromPlan(plan) {
|
|
938
|
+
return {
|
|
939
|
+
engine: "formant",
|
|
940
|
+
frontend: plan.frontend,
|
|
941
|
+
kana: plan.kana,
|
|
942
|
+
speaker: null,
|
|
943
|
+
warnings: plan.warnings,
|
|
944
|
+
dictionaryHits: plan.dictionaryHits.map((hit) => ({
|
|
945
|
+
surface: hit.surface,
|
|
946
|
+
reading: hit.reading,
|
|
947
|
+
accent: hit.accent,
|
|
948
|
+
source: hit.source,
|
|
949
|
+
applied: hit.applied,
|
|
950
|
+
})),
|
|
951
|
+
sentences: plan.sentences.map((sentence) => ({
|
|
952
|
+
text: sentence.text,
|
|
953
|
+
kana: sentence.kana,
|
|
954
|
+
start: sentence.sourceSpan.start,
|
|
955
|
+
end: sentence.sourceSpan.end,
|
|
956
|
+
})),
|
|
957
|
+
phrases: plan.phrases.map((phrase) => ({
|
|
958
|
+
text: phrase.moras.map((mora) => mora.text).join(""),
|
|
959
|
+
accent: phrase.accent,
|
|
960
|
+
accentSource: phrase.accentSource,
|
|
961
|
+
boundary: phrase.boundary,
|
|
962
|
+
pause: phrase.boundary !== "phrase" && phrase.boundary !== "end",
|
|
963
|
+
interrogative: phrase.interrogative,
|
|
964
|
+
moras: phrase.moras.map((mora) => ({
|
|
965
|
+
text: mora.text,
|
|
966
|
+
consonant: mora.consonant,
|
|
967
|
+
vowel: mora.vowel,
|
|
968
|
+
consonantLength: null,
|
|
969
|
+
vowelLength: 0,
|
|
970
|
+
pitch: 0,
|
|
971
|
+
})),
|
|
972
|
+
})),
|
|
973
|
+
};
|
|
274
974
|
}
|
|
275
975
|
//# sourceMappingURL=kongyoroid.js.map
|