@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/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";
|
|
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";
|
|
10
17
|
import { integer, isObject } from "./validate.js";
|
|
11
|
-
import { VERSION } from "./version.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,299 @@ 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 compiled = await this.compile(request, options);
|
|
399
|
+
return {
|
|
400
|
+
ok: true,
|
|
401
|
+
operation: "plan",
|
|
402
|
+
engine: "formant",
|
|
403
|
+
requestHash: compiled.requestHash,
|
|
404
|
+
plan: summarizePlan(compiled.plan, compiled.planHash, options.detail ?? "summary"),
|
|
405
|
+
request: compiled.request,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
async renderPlan(plan, options = {}) {
|
|
409
|
+
const start = performance.now();
|
|
410
|
+
checkAbort(options.signal);
|
|
411
|
+
const encoded = await encodePlanAsync(plan, options);
|
|
412
|
+
const renderMs = performance.now() - start;
|
|
413
|
+
return this.formantResult({
|
|
414
|
+
audio: encoded.audio,
|
|
415
|
+
kana: plan.reading?.kana,
|
|
416
|
+
chunks: 1,
|
|
417
|
+
warnings: plan.warnings,
|
|
418
|
+
adjustments: plan.adjustments,
|
|
419
|
+
stats: encoded.stats,
|
|
420
|
+
voice: plan.voice.id,
|
|
421
|
+
}, plan.kind, false, options.requestHash ?? "", {
|
|
422
|
+
readMs: options.readMs ?? 0,
|
|
423
|
+
planMs: options.planMs ?? 0,
|
|
424
|
+
renderMs,
|
|
425
|
+
encodeMs: 0,
|
|
426
|
+
totalMs: (options.readMs ?? 0) + (options.planMs ?? 0) + renderMs,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
formantResult(entry, kind, cached, hash, timings) {
|
|
430
|
+
return {
|
|
431
|
+
audio: entry.audio,
|
|
432
|
+
info: audioInfo(inspectWav(entry.audio)),
|
|
433
|
+
engine: "formant",
|
|
434
|
+
kind,
|
|
435
|
+
voice: entry.voice,
|
|
436
|
+
styles: {},
|
|
437
|
+
kana: entry.kana,
|
|
438
|
+
chunks: entry.chunks,
|
|
439
|
+
cached,
|
|
440
|
+
sha256: createHash("sha256").update(entry.audio).digest("hex"),
|
|
441
|
+
requestHash: hash,
|
|
442
|
+
elapsedMs: round(timings.totalMs),
|
|
443
|
+
timings: {
|
|
444
|
+
readMs: round(timings.readMs),
|
|
445
|
+
planMs: round(timings.planMs),
|
|
446
|
+
renderMs: round(timings.renderMs),
|
|
447
|
+
encodeMs: round(timings.encodeMs),
|
|
448
|
+
totalMs: round(timings.totalMs),
|
|
449
|
+
},
|
|
450
|
+
warnings: entry.warnings,
|
|
451
|
+
adjustments: entry.adjustments,
|
|
452
|
+
peak: entry.stats === undefined ? undefined : round(entry.stats.peak, 4),
|
|
453
|
+
rms: entry.stats === undefined ? undefined : round(entry.stats.rms, 4),
|
|
454
|
+
limitedSamples: entry.stats?.limitedSamples,
|
|
455
|
+
engineVersion: ENGINE_VERSION,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
async cacheGet(key, warnings) {
|
|
459
|
+
if (this.cache === undefined)
|
|
460
|
+
return undefined;
|
|
461
|
+
try {
|
|
462
|
+
const bytes = await this.cache.get(key);
|
|
463
|
+
return bytes === undefined ? undefined : unpackRender(bytes);
|
|
464
|
+
}
|
|
465
|
+
catch (error) {
|
|
466
|
+
if (this.cacheStrict)
|
|
467
|
+
throw asKongyoroidError(error);
|
|
468
|
+
warnings.push({
|
|
469
|
+
severity: "warning",
|
|
470
|
+
code: "CACHE_READ_FAILED",
|
|
471
|
+
message: `The render cache could not be read: ${error instanceof Error ? error.message : String(error)}`,
|
|
472
|
+
});
|
|
473
|
+
return undefined;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
async cacheSet(key, entry, warnings) {
|
|
477
|
+
if (this.cache === undefined)
|
|
478
|
+
return;
|
|
479
|
+
try {
|
|
480
|
+
await this.cache.set(key, packRender(entry));
|
|
481
|
+
}
|
|
482
|
+
catch (error) {
|
|
483
|
+
if (this.cacheStrict)
|
|
484
|
+
throw asKongyoroidError(error);
|
|
485
|
+
warnings.push({
|
|
486
|
+
severity: "warning",
|
|
487
|
+
code: "CACHE_WRITE_FAILED",
|
|
488
|
+
message: `The render was produced but could not be cached: ${error instanceof Error ? error.message : String(error)}`,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
146
492
|
async render(request, options = {}) {
|
|
147
493
|
const start = performance.now();
|
|
148
494
|
checkAbort(options.signal);
|
|
149
|
-
const
|
|
495
|
+
const parsed = this.parse(request);
|
|
496
|
+
const resolved = parsed.request;
|
|
150
497
|
const engine = await this.resolveEngine(resolved.engine, options);
|
|
498
|
+
const cacheWarnings = [];
|
|
151
499
|
const styles = engine === "voicevox" ? await this.resolveStyles(resolved, options) : {};
|
|
152
500
|
const manifest = engine === "voicevox" ? await this.client.manifest(options) : undefined;
|
|
153
|
-
const dictionary = engine === "voicevox" && resolved.kind === "speech"
|
|
501
|
+
const dictionary = engine === "voicevox" && resolved.kind === "speech"
|
|
502
|
+
? await this.dictionaryDigest(options)
|
|
503
|
+
: this.dictionary.digest();
|
|
154
504
|
const key = dictionary === undefined
|
|
155
505
|
? 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
|
-
|
|
506
|
+
: cacheKey(engine, VERSION, engine === "voicevox"
|
|
507
|
+
? [this.client.endpoint.href, manifest?.version ?? "", styles, dictionary]
|
|
508
|
+
: [ENGINE_VERSION, FRONTEND_VERSION, PLAN_VERSION, dictionary], resolved);
|
|
509
|
+
const cached = key === undefined ? undefined : await this.cacheGet(key, cacheWarnings);
|
|
510
|
+
checkAbort(options.signal);
|
|
511
|
+
let timings = { readMs: 0, planMs: 0, renderMs: 0, encodeMs: 0, totalMs: 0 };
|
|
512
|
+
let entry;
|
|
513
|
+
if (cached !== undefined) {
|
|
514
|
+
entry = cached;
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
const shared = key === undefined ? undefined : this.inFlight.get(key);
|
|
518
|
+
if (shared !== undefined) {
|
|
519
|
+
entry = await shared;
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
const job = this.semaphore.run(async () => {
|
|
523
|
+
checkAbort(options.signal);
|
|
524
|
+
if (engine === "formant") {
|
|
525
|
+
const sampleRate = resolved.sampleRate ?? DEFAULT_SAMPLE_RATE;
|
|
526
|
+
const compiled = await compileRequest(resolved, sampleRate, this.compileOptions(options));
|
|
527
|
+
const renderStart = performance.now();
|
|
528
|
+
const encoded = await encodePlanAsync(compiled.plan, options.signal === undefined ? {} : { signal: options.signal });
|
|
529
|
+
const renderMs = performance.now() - renderStart;
|
|
530
|
+
timings = { readMs: compiled.readMs, planMs: compiled.planMs, renderMs, encodeMs: 0, totalMs: 0 };
|
|
531
|
+
return {
|
|
532
|
+
audio: encoded.audio,
|
|
533
|
+
kana: compiled.plan.reading?.kana,
|
|
534
|
+
chunks: compiled.plan.reading?.sentences.length ?? 1,
|
|
535
|
+
warnings: compiled.plan.warnings,
|
|
536
|
+
adjustments: compiled.plan.adjustments,
|
|
537
|
+
stats: encoded.stats,
|
|
538
|
+
voice: compiled.plan.voice.id,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
if (resolved.kind === "speech") {
|
|
542
|
+
const speaker = styles.speaker;
|
|
543
|
+
if (speaker === undefined)
|
|
544
|
+
throw asKongyoroidError(new Error("speaker was not resolved"));
|
|
545
|
+
const result = await synthesizeSpeech(this.client, resolved, speaker, {
|
|
546
|
+
...options,
|
|
547
|
+
concurrency: this.concurrency,
|
|
548
|
+
});
|
|
549
|
+
return {
|
|
550
|
+
audio: result.audio,
|
|
551
|
+
kana: result.kana,
|
|
552
|
+
chunks: result.chunks,
|
|
553
|
+
warnings: [],
|
|
554
|
+
adjustments: [],
|
|
555
|
+
stats: undefined,
|
|
556
|
+
voice: undefined,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
const singer = styles.singer;
|
|
560
|
+
const teacher = styles.teacher;
|
|
561
|
+
if (singer === undefined || teacher === undefined)
|
|
562
|
+
throw asKongyoroidError(new Error("singer was not resolved"));
|
|
563
|
+
const result = await synthesizeSong(this.client, resolved, singer, teacher, options);
|
|
564
|
+
return {
|
|
565
|
+
audio: result.audio,
|
|
566
|
+
kana: undefined,
|
|
567
|
+
chunks: 1,
|
|
568
|
+
warnings: [],
|
|
569
|
+
adjustments: [],
|
|
570
|
+
stats: undefined,
|
|
571
|
+
voice: undefined,
|
|
572
|
+
};
|
|
573
|
+
}, options.signal);
|
|
574
|
+
if (key !== undefined) {
|
|
575
|
+
this.inFlight.set(key, job);
|
|
576
|
+
job
|
|
577
|
+
.finally(() => {
|
|
578
|
+
if (this.inFlight.get(key) === job)
|
|
579
|
+
this.inFlight.delete(key);
|
|
580
|
+
})
|
|
581
|
+
.catch(() => undefined);
|
|
179
582
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
583
|
+
entry = await job;
|
|
584
|
+
if (key !== undefined)
|
|
585
|
+
await this.cacheSet(key, entry, cacheWarnings);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
checkAbort(options.signal);
|
|
589
|
+
const total = performance.now() - start;
|
|
590
|
+
const finalTimings = { ...timings, totalMs: total };
|
|
591
|
+
const warnings = cacheWarnings.length === 0 ? entry.warnings : [...entry.warnings, ...cacheWarnings];
|
|
592
|
+
if (engine === "formant") {
|
|
593
|
+
return this.formantResult({ ...entry, warnings }, resolved.kind, cached !== undefined, parsed.requestHash, finalTimings);
|
|
594
|
+
}
|
|
185
595
|
return {
|
|
186
596
|
audio: entry.audio,
|
|
187
597
|
info: audioInfo(inspectWav(entry.audio)),
|
|
188
598
|
engine,
|
|
189
599
|
kind: resolved.kind,
|
|
600
|
+
voice: undefined,
|
|
190
601
|
styles,
|
|
191
602
|
kana: entry.kana,
|
|
192
603
|
chunks: entry.chunks,
|
|
193
604
|
cached: cached !== undefined,
|
|
194
605
|
sha256: createHash("sha256").update(entry.audio).digest("hex"),
|
|
195
|
-
|
|
606
|
+
requestHash: parsed.requestHash,
|
|
607
|
+
elapsedMs: round(total),
|
|
608
|
+
timings: { readMs: 0, planMs: 0, renderMs: round(total), encodeMs: 0, totalMs: round(total) },
|
|
609
|
+
warnings,
|
|
610
|
+
adjustments: [],
|
|
611
|
+
peak: undefined,
|
|
612
|
+
rms: undefined,
|
|
613
|
+
limitedSamples: undefined,
|
|
614
|
+
engineVersion: manifest?.version ?? "voicevox",
|
|
196
615
|
};
|
|
197
616
|
}
|
|
198
617
|
speak(input, options = {}) {
|
|
@@ -201,41 +620,207 @@ export class Kongyoroid {
|
|
|
201
620
|
sing(input, options = {}) {
|
|
202
621
|
return this.render({ kind: "song", ...input }, options);
|
|
203
622
|
}
|
|
623
|
+
async *renderStream(request, options = {}) {
|
|
624
|
+
const compiled = await this.compile(request, options.signal === undefined ? {} : { signal: options.signal });
|
|
625
|
+
const iterator = renderPcmStream(compiled.plan, options);
|
|
626
|
+
while (true) {
|
|
627
|
+
const next = await iterator.next();
|
|
628
|
+
if (next.done)
|
|
629
|
+
return { plan: compiled.plan, stats: next.value };
|
|
630
|
+
yield next.value;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
speakStream(chunks, input = {}, options = {}) {
|
|
634
|
+
const parsed = this.parse({ ...input, kind: "speech", text: "placeholder" });
|
|
635
|
+
const resolved = parsed.request;
|
|
636
|
+
if (resolved.kind !== "speech")
|
|
637
|
+
invalid("$.kind", "Streaming synthesis needs a speech request.");
|
|
638
|
+
if (resolved.engine !== "formant") {
|
|
639
|
+
invalid("$.engine", "Streaming synthesis is only available with the formant engine.", {
|
|
640
|
+
repairOptions: [{ action: "use-formant-engine", description: "Use engine formant for streaming." }],
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
return synthesizeTextStream(chunks, resolved, {
|
|
644
|
+
...options,
|
|
645
|
+
dictionary: this.dictionary,
|
|
646
|
+
sampleRate: options.sampleRate ?? resolved.sampleRate ?? DEFAULT_SAMPLE_RATE,
|
|
647
|
+
});
|
|
648
|
+
}
|
|
204
649
|
async reading(text, options = {}) {
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
650
|
+
const engine = await this.resolveEngine(options.engine ?? (options.speaker !== undefined ? "voicevox" : this.defaults.engine), options);
|
|
651
|
+
if (engine === "voicevox") {
|
|
652
|
+
const speaker = await this.styles.resolve(options.speaker ?? this.defaults.speaker, "speaker", options);
|
|
653
|
+
const phrases = options.kana === undefined
|
|
654
|
+
? (await this.client.audioQuery(text, speaker.id, options)).accent_phrases
|
|
655
|
+
: await this.client.accentPhrases(formatKanaNotation(parseKanaNotation(options.kana, "$.kana")), speaker.id, true, options);
|
|
656
|
+
return {
|
|
657
|
+
engine: "voicevox",
|
|
658
|
+
frontend: "voicevox",
|
|
659
|
+
kana: accentPhrasesToKana(phrases),
|
|
660
|
+
speaker,
|
|
661
|
+
warnings: [],
|
|
662
|
+
dictionaryHits: [],
|
|
663
|
+
sentences: [],
|
|
664
|
+
phrases: phrases.map((phrase) => ({
|
|
665
|
+
text: phrase.moras.map((mora) => mora.text).join(""),
|
|
666
|
+
accent: phrase.accent,
|
|
667
|
+
accentSource: "frontend",
|
|
668
|
+
boundary: phrase.pause_mora !== null ? "pause" : "phrase",
|
|
669
|
+
pause: phrase.pause_mora !== null,
|
|
670
|
+
interrogative: phrase.is_interrogative,
|
|
671
|
+
moras: phrase.moras.map((mora) => ({
|
|
672
|
+
text: mora.text,
|
|
673
|
+
consonant: mora.consonant,
|
|
674
|
+
vowel: mora.vowel,
|
|
675
|
+
consonantLength: mora.consonant_length,
|
|
676
|
+
vowelLength: mora.vowel_length,
|
|
677
|
+
pitch: mora.pitch,
|
|
678
|
+
})),
|
|
224
679
|
})),
|
|
225
|
-
}
|
|
226
|
-
}
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
const request = this.parse({
|
|
683
|
+
kind: "speech",
|
|
684
|
+
text,
|
|
685
|
+
...(options.kana === undefined ? {} : { kana: options.kana }),
|
|
686
|
+
...(options.strict === undefined ? {} : { strictReading: options.strict }),
|
|
687
|
+
}).request;
|
|
688
|
+
if (request.kind !== "speech")
|
|
689
|
+
invalid("$.kind", "Reading analysis needs a speech request.");
|
|
690
|
+
const read = await readForRequest(request, {
|
|
691
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
692
|
+
dictionary: options.dictionary === undefined ? this.dictionary : this.dictionary.merge(options.dictionary),
|
|
693
|
+
});
|
|
694
|
+
return readingFromPlan(read.reading);
|
|
227
695
|
}
|
|
228
|
-
async voices(
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
696
|
+
async voices(options = {}) {
|
|
697
|
+
const engine = options.engine ?? "all";
|
|
698
|
+
const kind = options.kind ?? "all";
|
|
699
|
+
const voices = [];
|
|
700
|
+
if (engine === "formant" || engine === "all" || engine === "auto") {
|
|
701
|
+
for (const profile of BUILTIN_VOICES) {
|
|
702
|
+
const described = describeVoice(profile);
|
|
703
|
+
voices.push({
|
|
704
|
+
id: described.id,
|
|
705
|
+
engine: "formant",
|
|
706
|
+
name: described.name,
|
|
707
|
+
character: null,
|
|
708
|
+
description: described.description,
|
|
709
|
+
kinds: ["speech", "song"],
|
|
710
|
+
styleId: null,
|
|
711
|
+
type: null,
|
|
712
|
+
baseF0: described.baseF0,
|
|
713
|
+
f0Range: described.f0Range,
|
|
714
|
+
hash: described.hash,
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
let voicevox = { queried: false, available: false, error: null };
|
|
719
|
+
if (engine === "voicevox" || engine === "all" || engine === "auto") {
|
|
720
|
+
try {
|
|
721
|
+
const all = await this.styles.all(options);
|
|
722
|
+
const filtered = all.filter((style) => kind === "all"
|
|
723
|
+
? true
|
|
724
|
+
: kind === "speech"
|
|
725
|
+
? style.type === "talk" || style.type === "streaming_talk"
|
|
726
|
+
: style.type === "frame_decode" || style.type === "sing" || style.type === "singing_teacher");
|
|
727
|
+
for (const style of filtered)
|
|
728
|
+
voices.push(voicevoxDescriptor(style));
|
|
729
|
+
voicevox = { queried: true, available: true, error: null };
|
|
730
|
+
}
|
|
731
|
+
catch (error) {
|
|
732
|
+
const failure = asKongyoroidError(error);
|
|
733
|
+
if (failure.code === "ABORTED" || engine === "voicevox")
|
|
734
|
+
throw failure;
|
|
735
|
+
voicevox = { queried: true, available: false, error: failure.toJSON() };
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return { voices, voicevox };
|
|
235
739
|
}
|
|
236
740
|
async doctor(options = {}) {
|
|
237
|
-
const
|
|
741
|
+
const engine = options.engine ?? this.defaults.engine;
|
|
742
|
+
const checkVoicevox = engine === "voicevox" || engine === "auto" || engine === "all";
|
|
743
|
+
const checkFormant = engine === "formant" || engine === "auto" || engine === "all" || options.frontend === true;
|
|
744
|
+
let frontend = { available: false, version: FRONTEND_VERSION, loadMs: null, error: null };
|
|
745
|
+
let synthesis = { ok: false, ms: 0, frames: 0, error: null };
|
|
746
|
+
if (checkFormant) {
|
|
747
|
+
const started = performance.now();
|
|
748
|
+
try {
|
|
749
|
+
await loadFrontend();
|
|
750
|
+
frontend = {
|
|
751
|
+
available: true,
|
|
752
|
+
version: FRONTEND_VERSION,
|
|
753
|
+
loadMs: round(performance.now() - started),
|
|
754
|
+
error: null,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
catch (error) {
|
|
758
|
+
frontend = {
|
|
759
|
+
available: false,
|
|
760
|
+
version: FRONTEND_VERSION,
|
|
761
|
+
loadMs: round(performance.now() - started),
|
|
762
|
+
error: asKongyoroidError(error).toJSON(),
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
const synthStart = performance.now();
|
|
766
|
+
try {
|
|
767
|
+
const compiled = await this.compile({ kind: "speech", engine: "formant", text: "てすと", kana: "テ'_スト", prePause: 0, postPause: 0 }, options);
|
|
768
|
+
const encoded = await encodePlanAsync(compiled.plan, options.signal === undefined ? {} : { signal: options.signal });
|
|
769
|
+
synthesis = {
|
|
770
|
+
ok: encoded.audio.length > 44 && encoded.stats.peak > 0.01,
|
|
771
|
+
ms: round(performance.now() - synthStart),
|
|
772
|
+
frames: compiled.plan.frames,
|
|
773
|
+
error: null,
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
catch (error) {
|
|
777
|
+
synthesis = {
|
|
778
|
+
ok: false,
|
|
779
|
+
ms: round(performance.now() - synthStart),
|
|
780
|
+
frames: 0,
|
|
781
|
+
error: asKongyoroidError(error).toJSON(),
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
let cache = { directory: this.diskCache?.directory ?? null, stats: null, error: null };
|
|
786
|
+
if (this.cache !== undefined) {
|
|
787
|
+
try {
|
|
788
|
+
cache = { directory: this.diskCache?.directory ?? null, stats: await this.cache.stats(), error: null };
|
|
789
|
+
}
|
|
790
|
+
catch (error) {
|
|
791
|
+
cache = { directory: this.diskCache?.directory ?? null, stats: null, error: asKongyoroidError(error).toJSON() };
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
let voicevox = null;
|
|
795
|
+
if (checkVoicevox)
|
|
796
|
+
voicevox = await this.voicevoxDoctor(options);
|
|
797
|
+
const formantOk = !checkFormant || (frontend.available && synthesis.ok);
|
|
798
|
+
const voicevoxOk = voicevox === null || voicevox.ok;
|
|
799
|
+
const ok = engine === "auto"
|
|
800
|
+
? formantOk
|
|
801
|
+
: engine === "all"
|
|
802
|
+
? formantOk && voicevoxOk
|
|
803
|
+
: engine === "voicevox"
|
|
804
|
+
? voicevoxOk
|
|
805
|
+
: formantOk;
|
|
806
|
+
return {
|
|
807
|
+
ok,
|
|
808
|
+
name: PACKAGE_NAME,
|
|
809
|
+
version: VERSION,
|
|
810
|
+
engineVersion: ENGINE_VERSION,
|
|
811
|
+
node: process.version,
|
|
812
|
+
defaultEngine: this.defaults.engine,
|
|
813
|
+
formant: { ok: formantOk, voices: BUILTIN_VOICES.map((voice) => voice.id), frontend, synthesis },
|
|
814
|
+
cache,
|
|
815
|
+
dictionary: this.dictionary.size === 0 ? null : { entries: this.dictionary.size, digest: this.dictionary.digest() },
|
|
816
|
+
voicevox,
|
|
817
|
+
error: ok ? null : (voicevox?.error ?? frontend.error ?? synthesis.error ?? null),
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
async voicevoxDoctor(options = {}) {
|
|
821
|
+
let endpoint = "";
|
|
238
822
|
try {
|
|
823
|
+
endpoint = this.client.endpoint.origin;
|
|
239
824
|
const version = await this.client.version(options);
|
|
240
825
|
const manifest = await this.client.manifest(options);
|
|
241
826
|
const devices = await this.client.supportedDevices(options).catch(() => null);
|
|
@@ -271,5 +856,111 @@ export class Kongyoroid {
|
|
|
271
856
|
};
|
|
272
857
|
}
|
|
273
858
|
}
|
|
859
|
+
inspect(audio, options = {}) {
|
|
860
|
+
const layout = inspectWav(audio);
|
|
861
|
+
const decoded = decodeWav(audio);
|
|
862
|
+
const stats = signalStats(decoded.samples);
|
|
863
|
+
const window = Math.max(256, Math.round(((options.windowMs ?? 40) / 1000) * layout.sampleRate));
|
|
864
|
+
const hop = Math.round(window / 2);
|
|
865
|
+
const track = [];
|
|
866
|
+
const voiced = [];
|
|
867
|
+
let windows = 0;
|
|
868
|
+
for (let start = 0; start + window <= decoded.samples.length; start += hop) {
|
|
869
|
+
windows += 1;
|
|
870
|
+
const estimate = estimatePitch(decoded.samples, layout.sampleRate, start, start + window, 50, 1500);
|
|
871
|
+
const frameStats = signalStats(decoded.samples, start, start + window);
|
|
872
|
+
const isVoiced = estimate.clarity > 0.7 && frameStats.rms > 0.01;
|
|
873
|
+
if (isVoiced)
|
|
874
|
+
voiced.push(estimate.hz);
|
|
875
|
+
if (options.pitchTrack === true)
|
|
876
|
+
track.push({
|
|
877
|
+
seconds: round(start / layout.sampleRate, 4),
|
|
878
|
+
hz: isVoiced ? round(estimate.hz, 1) : 0,
|
|
879
|
+
clarity: round(estimate.clarity, 3),
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
const sorted = [...voiced].sort((a, b) => a - b);
|
|
883
|
+
const median = sorted.length === 0 ? null : (sorted[Math.floor(sorted.length / 2)] ?? null);
|
|
884
|
+
return {
|
|
885
|
+
info: audioInfo(layout),
|
|
886
|
+
bytes: audio.byteLength,
|
|
887
|
+
sha256: createHash("sha256").update(audio).digest("hex"),
|
|
888
|
+
peak: round(stats.peak, 4),
|
|
889
|
+
rms: round(stats.rms, 4),
|
|
890
|
+
dc: round(stats.dc, 5),
|
|
891
|
+
clipped: stats.clipped,
|
|
892
|
+
finite: stats.finite,
|
|
893
|
+
pitch: {
|
|
894
|
+
medianHz: median === null ? null : round(median, 1),
|
|
895
|
+
minHz: sorted.length === 0 ? null : round(sorted[0] ?? 0, 1),
|
|
896
|
+
maxHz: sorted.length === 0 ? null : round(sorted[sorted.length - 1] ?? 0, 1),
|
|
897
|
+
voicedRatio: windows === 0 ? 0 : round(voiced.length / windows, 3),
|
|
898
|
+
...(options.pitchTrack === true ? { track } : {}),
|
|
899
|
+
},
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
frontendStatus() {
|
|
903
|
+
return frontendState();
|
|
904
|
+
}
|
|
905
|
+
voiceProfile(id) {
|
|
906
|
+
return resolveVoice(id ?? this.defaults.voice, "$.voice");
|
|
907
|
+
}
|
|
908
|
+
voiceHash(id) {
|
|
909
|
+
return voiceProfileHash(this.voiceProfile(id));
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
function voicevoxDescriptor(style) {
|
|
913
|
+
const speech = style.type === "talk" || style.type === "streaming_talk";
|
|
914
|
+
return {
|
|
915
|
+
id: `voicevox:${style.id}`,
|
|
916
|
+
engine: "voicevox",
|
|
917
|
+
name: style.name,
|
|
918
|
+
character: style.character,
|
|
919
|
+
description: null,
|
|
920
|
+
kinds: speech ? ["speech"] : ["song"],
|
|
921
|
+
styleId: style.id,
|
|
922
|
+
type: style.type,
|
|
923
|
+
baseF0: null,
|
|
924
|
+
f0Range: null,
|
|
925
|
+
hash: null,
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
export function readingFromPlan(plan) {
|
|
929
|
+
return {
|
|
930
|
+
engine: "formant",
|
|
931
|
+
frontend: plan.frontend,
|
|
932
|
+
kana: plan.kana,
|
|
933
|
+
speaker: null,
|
|
934
|
+
warnings: plan.warnings,
|
|
935
|
+
dictionaryHits: plan.dictionaryHits.map((hit) => ({
|
|
936
|
+
surface: hit.surface,
|
|
937
|
+
reading: hit.reading,
|
|
938
|
+
accent: hit.accent,
|
|
939
|
+
source: hit.source,
|
|
940
|
+
applied: hit.applied,
|
|
941
|
+
})),
|
|
942
|
+
sentences: plan.sentences.map((sentence) => ({
|
|
943
|
+
text: sentence.text,
|
|
944
|
+
kana: sentence.kana,
|
|
945
|
+
start: sentence.sourceSpan.start,
|
|
946
|
+
end: sentence.sourceSpan.end,
|
|
947
|
+
})),
|
|
948
|
+
phrases: plan.phrases.map((phrase) => ({
|
|
949
|
+
text: phrase.moras.map((mora) => mora.text).join(""),
|
|
950
|
+
accent: phrase.accent,
|
|
951
|
+
accentSource: phrase.accentSource,
|
|
952
|
+
boundary: phrase.boundary,
|
|
953
|
+
pause: phrase.boundary !== "phrase" && phrase.boundary !== "end",
|
|
954
|
+
interrogative: phrase.interrogative,
|
|
955
|
+
moras: phrase.moras.map((mora) => ({
|
|
956
|
+
text: mora.text,
|
|
957
|
+
consonant: mora.consonant,
|
|
958
|
+
vowel: mora.vowel,
|
|
959
|
+
consonantLength: null,
|
|
960
|
+
vowelLength: 0,
|
|
961
|
+
pitch: 0,
|
|
962
|
+
})),
|
|
963
|
+
})),
|
|
964
|
+
};
|
|
274
965
|
}
|
|
275
966
|
//# sourceMappingURL=kongyoroid.js.map
|