@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.
Files changed (206) hide show
  1. package/README.md +239 -0
  2. package/dist/cache.d.ts +32 -7
  3. package/dist/cache.d.ts.map +1 -1
  4. package/dist/cache.js +139 -9
  5. package/dist/cache.js.map +1 -1
  6. package/dist/cli/flags.d.ts +270 -0
  7. package/dist/cli/flags.d.ts.map +1 -0
  8. package/dist/cli/flags.js +240 -0
  9. package/dist/cli/flags.js.map +1 -0
  10. package/dist/cli/help.d.ts +11 -0
  11. package/dist/cli/help.d.ts.map +1 -0
  12. package/dist/cli/help.js +346 -0
  13. package/dist/cli/help.js.map +1 -0
  14. package/dist/cli.js +729 -243
  15. package/dist/cli.js.map +1 -1
  16. package/dist/errors.d.ts +43 -3
  17. package/dist/errors.d.ts.map +1 -1
  18. package/dist/errors.js +56 -3
  19. package/dist/errors.js.map +1 -1
  20. package/dist/index.d.ts +59 -22
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +32 -14
  23. package/dist/index.js.map +1 -1
  24. package/dist/io.d.ts +12 -0
  25. package/dist/io.d.ts.map +1 -1
  26. package/dist/io.js +161 -6
  27. package/dist/io.js.map +1 -1
  28. package/dist/kongyoroid.d.ts +237 -10
  29. package/dist/kongyoroid.d.ts.map +1 -1
  30. package/dist/kongyoroid.js +787 -87
  31. package/dist/kongyoroid.js.map +1 -1
  32. package/dist/limits.d.ts +5 -0
  33. package/dist/limits.d.ts.map +1 -1
  34. package/dist/limits.js +6 -1
  35. package/dist/limits.js.map +1 -1
  36. package/dist/player.d.ts.map +1 -1
  37. package/dist/player.js +18 -22
  38. package/dist/player.js.map +1 -1
  39. package/dist/request.d.ts +1 -0
  40. package/dist/request.d.ts.map +1 -1
  41. package/dist/request.js +102 -15
  42. package/dist/request.js.map +1 -1
  43. package/dist/schema.d.ts +3 -0
  44. package/dist/schema.d.ts.map +1 -1
  45. package/dist/schema.js +271 -68
  46. package/dist/schema.js.map +1 -1
  47. package/dist/song/mml.d.ts +15 -0
  48. package/dist/song/mml.d.ts.map +1 -0
  49. package/dist/song/mml.js +184 -0
  50. package/dist/song/mml.js.map +1 -0
  51. package/dist/song/pitch.d.ts.map +1 -0
  52. package/dist/{pitch.js → song/pitch.js} +1 -1
  53. package/dist/song/pitch.js.map +1 -0
  54. package/dist/song/score.d.ts +92 -0
  55. package/dist/song/score.d.ts.map +1 -0
  56. package/dist/song/score.js +454 -0
  57. package/dist/song/score.js.map +1 -0
  58. package/dist/synth/acoustics.d.ts +27 -0
  59. package/dist/synth/acoustics.d.ts.map +1 -0
  60. package/dist/synth/acoustics.js +608 -0
  61. package/dist/synth/acoustics.js.map +1 -0
  62. package/dist/synth/analysis.d.ts +22 -0
  63. package/dist/synth/analysis.d.ts.map +1 -0
  64. package/dist/synth/analysis.js +170 -0
  65. package/dist/synth/analysis.js.map +1 -0
  66. package/dist/synth/engine.d.ts +115 -0
  67. package/dist/synth/engine.d.ts.map +1 -0
  68. package/dist/synth/engine.js +223 -0
  69. package/dist/synth/engine.js.map +1 -0
  70. package/dist/synth/filters.d.ts +60 -0
  71. package/dist/synth/filters.d.ts.map +1 -0
  72. package/dist/synth/filters.js +166 -0
  73. package/dist/synth/filters.js.map +1 -0
  74. package/dist/synth/glottal.d.ts +20 -0
  75. package/dist/synth/glottal.d.ts.map +1 -0
  76. package/dist/synth/glottal.js +76 -0
  77. package/dist/synth/glottal.js.map +1 -0
  78. package/dist/synth/phonemes.d.ts +42 -0
  79. package/dist/synth/phonemes.d.ts.map +1 -0
  80. package/dist/synth/phonemes.js +311 -0
  81. package/dist/synth/phonemes.js.map +1 -0
  82. package/dist/synth/plan.d.ts +127 -0
  83. package/dist/synth/plan.d.ts.map +1 -0
  84. package/dist/synth/plan.js +81 -0
  85. package/dist/synth/plan.js.map +1 -0
  86. package/dist/synth/prosody.d.ts +38 -0
  87. package/dist/synth/prosody.d.ts.map +1 -0
  88. package/dist/synth/prosody.js +120 -0
  89. package/dist/synth/prosody.js.map +1 -0
  90. package/dist/synth/random.d.ts +15 -0
  91. package/dist/synth/random.d.ts.map +1 -0
  92. package/dist/synth/random.js +59 -0
  93. package/dist/synth/random.js.map +1 -0
  94. package/dist/synth/renderer.d.ts +77 -0
  95. package/dist/synth/renderer.d.ts.map +1 -0
  96. package/dist/synth/renderer.js +343 -0
  97. package/dist/synth/renderer.js.map +1 -0
  98. package/dist/synth/song.d.ts +30 -0
  99. package/dist/synth/song.d.ts.map +1 -0
  100. package/dist/synth/song.js +453 -0
  101. package/dist/synth/song.js.map +1 -0
  102. package/dist/synth/speech.d.ts +49 -0
  103. package/dist/synth/speech.d.ts.map +1 -0
  104. package/dist/synth/speech.js +358 -0
  105. package/dist/synth/speech.js.map +1 -0
  106. package/dist/synth/stream.d.ts +26 -0
  107. package/dist/synth/stream.d.ts.map +1 -0
  108. package/dist/synth/stream.js +67 -0
  109. package/dist/synth/stream.js.map +1 -0
  110. package/dist/synth/textstream.d.ts +35 -0
  111. package/dist/synth/textstream.d.ts.map +1 -0
  112. package/dist/synth/textstream.js +209 -0
  113. package/dist/synth/textstream.js.map +1 -0
  114. package/dist/synth/voice.d.ts +64 -0
  115. package/dist/synth/voice.d.ts.map +1 -0
  116. package/dist/synth/voice.js +208 -0
  117. package/dist/synth/voice.js.map +1 -0
  118. package/dist/text/dictionary.d.ts +50 -0
  119. package/dist/text/dictionary.d.ts.map +1 -0
  120. package/dist/text/dictionary.js +0 -0
  121. package/dist/text/dictionary.js.map +1 -0
  122. package/dist/text/frontend.d.ts +35 -0
  123. package/dist/text/frontend.d.ts.map +1 -0
  124. package/dist/text/frontend.js +559 -0
  125. package/dist/text/frontend.js.map +1 -0
  126. package/dist/text/lexicon.d.ts +10 -0
  127. package/dist/text/lexicon.d.ts.map +1 -0
  128. package/dist/text/lexicon.js +763 -0
  129. package/dist/text/lexicon.js.map +1 -0
  130. package/dist/{mora.d.ts → text/mora.d.ts} +15 -3
  131. package/dist/text/mora.d.ts.map +1 -0
  132. package/dist/{mora.js → text/mora.js} +87 -9
  133. package/dist/text/mora.js.map +1 -0
  134. package/dist/text/normalize.d.ts +34 -0
  135. package/dist/text/normalize.d.ts.map +1 -0
  136. package/dist/text/normalize.js +504 -0
  137. package/dist/text/normalize.js.map +1 -0
  138. package/dist/text/notation.d.ts +32 -0
  139. package/dist/text/notation.d.ts.map +1 -0
  140. package/dist/text/notation.js +182 -0
  141. package/dist/text/notation.js.map +1 -0
  142. package/dist/text/reading.d.ts +52 -0
  143. package/dist/text/reading.d.ts.map +1 -0
  144. package/dist/text/reading.js +32 -0
  145. package/dist/text/reading.js.map +1 -0
  146. package/dist/text/sentences.d.ts +11 -0
  147. package/dist/text/sentences.d.ts.map +1 -0
  148. package/dist/text/sentences.js +91 -0
  149. package/dist/text/sentences.js.map +1 -0
  150. package/dist/{text.d.ts → text/split.d.ts} +2 -2
  151. package/dist/text/split.d.ts.map +1 -0
  152. package/dist/{text.js → text/split.js} +2 -2
  153. package/dist/text/split.js.map +1 -0
  154. package/dist/types.d.ts +58 -3
  155. package/dist/types.d.ts.map +1 -1
  156. package/dist/version.d.ts +4 -0
  157. package/dist/version.d.ts.map +1 -1
  158. package/dist/version.js +5 -1
  159. package/dist/version.js.map +1 -1
  160. package/dist/voicevox/client.d.ts +1 -1
  161. package/dist/voicevox/client.d.ts.map +1 -1
  162. package/dist/voicevox/dictionary.js +1 -1
  163. package/dist/voicevox/dictionary.js.map +1 -1
  164. package/dist/voicevox/song.js +1 -1
  165. package/dist/voicevox/song.js.map +1 -1
  166. package/dist/voicevox/speech.d.ts +1 -0
  167. package/dist/voicevox/speech.d.ts.map +1 -1
  168. package/dist/voicevox/speech.js +8 -2
  169. package/dist/voicevox/speech.js.map +1 -1
  170. package/dist/wav.d.ts +1 -0
  171. package/dist/wav.d.ts.map +1 -1
  172. package/dist/wav.js +8 -6
  173. package/dist/wav.js.map +1 -1
  174. package/docs/cli.md +261 -0
  175. package/examples/batch.jsonl +4 -3
  176. package/examples/builtin/dictionary.json +7 -0
  177. package/examples/builtin/song-compact.json +13 -0
  178. package/examples/builtin/song-mml.json +9 -0
  179. package/examples/builtin/song-notes.json +25 -0
  180. package/examples/builtin/speech-dictionary.json +9 -0
  181. package/examples/builtin/speech-kana.json +7 -0
  182. package/examples/builtin/speech.json +7 -0
  183. package/package.json +10 -4
  184. package/dist/formant.d.ts +0 -34
  185. package/dist/formant.d.ts.map +0 -1
  186. package/dist/formant.js +0 -622
  187. package/dist/formant.js.map +0 -1
  188. package/dist/mora.d.ts.map +0 -1
  189. package/dist/mora.js.map +0 -1
  190. package/dist/notation.d.ts +0 -15
  191. package/dist/notation.d.ts.map +0 -1
  192. package/dist/notation.js +0 -114
  193. package/dist/notation.js.map +0 -1
  194. package/dist/pitch.d.ts.map +0 -1
  195. package/dist/pitch.js.map +0 -1
  196. package/dist/score.d.ts +0 -37
  197. package/dist/score.d.ts.map +0 -1
  198. package/dist/score.js +0 -166
  199. package/dist/score.js.map +0 -1
  200. package/dist/text.d.ts.map +0 -1
  201. package/dist/text.js.map +0 -1
  202. package/examples/song-notes.json +0 -24
  203. /package/dist/{pitch.d.ts → song/pitch.d.ts} +0 -0
  204. /package/examples/{song.json → voicevox/song.json} +0 -0
  205. /package/examples/{speech-kana.json → voicevox/speech-kana.json} +0 -0
  206. /package/examples/{speech.json → voicevox/speech.json} +0 -0
@@ -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 { integer, isObject } from "./validate.js";
11
- import { VERSION } from "./version.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";
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({ kana: entry.kana ?? null, chunks: entry.chunks }));
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
- this.client = new VoicevoxClient(options);
61
- this.styles = new StyleCatalog(this.client);
62
- this.dictionary = new Dictionary(this.client);
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
- speaker: options.speaker === undefined ? undefined : parseStyleRef(options.speaker, "$.speaker"),
66
- singer: options.singer === undefined ? undefined : parseStyleRef(options.singer, "$.singer"),
67
- teacher: options.teacher === undefined ? undefined : parseStyleRef(options.teacher, "$.teacher"),
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 memory = new MemoryCache(options.cacheBytes ?? 64 * 1024 * 1024);
72
- this.cache = options.cacheDir === undefined ? memory : new LayeredCache([memory, new DiskCache(options.cacheDir)]);
73
- this.probeTimeoutMs = integer(options.probeTimeoutMs ?? 3000, "$.probeTimeoutMs", 100, 600_000);
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
- if (merged["engine"] === undefined && this.defaults.engine !== undefined)
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["kind"] === "speech" && merged["speaker"] === undefined && this.defaults.speaker !== undefined) {
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("\u0000"))
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 resolved = parseRequest(this.withDefaults(request));
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" ? await this.dictionaryDigest(options) : "";
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" ? [this.client.endpoint.origin, manifest?.version ?? "", styles, dictionary] : [], resolved);
157
- const cached = key === undefined ? undefined : unpackRender((await this.cache.get(key)) ?? new Uint8Array(0));
158
- const entry = cached ??
159
- (await this.semaphore.run(async () => {
160
- checkAbort(options.signal);
161
- if (engine === "formant") {
162
- const audio = await renderFormant(resolved, resolved.sampleRate ?? DEFAULT_SAMPLE_RATE, options);
163
- return { audio, kana: resolved.kind === "speech" ? resolved.kana : undefined, chunks: 1 };
164
- }
165
- if (resolved.kind === "speech") {
166
- const speaker = styles.speaker;
167
- if (speaker === undefined)
168
- throw asKongyoroidError(new Error("speaker was not resolved"));
169
- const result = await synthesizeSpeech(this.client, resolved, speaker, {
170
- ...options,
171
- concurrency: this.concurrency,
172
- });
173
- return { audio: result.audio, kana: result.kana, chunks: result.chunks };
174
- }
175
- const singer = styles.singer;
176
- const teacher = styles.teacher;
177
- if (singer === undefined || teacher === undefined) {
178
- throw asKongyoroidError(new Error("singer was not resolved"));
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
- const result = await synthesizeSong(this.client, resolved, singer, teacher, options);
181
- return { audio: result.audio, kana: undefined, chunks: 1 };
182
- }, options.signal));
183
- if (key !== undefined && cached === undefined)
184
- await this.cache.set(key, packRender(entry));
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
- elapsedMs: Math.round((performance.now() - start) * 100) / 100,
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 speaker = await this.styles.resolve(options.speaker ?? this.defaults.speaker, "speaker", options);
206
- const phrases = options.kana === undefined
207
- ? (await this.client.audioQuery(text, speaker.id, options)).accent_phrases
208
- : await this.client.accentPhrases(formatKanaNotation(parseKanaNotation(options.kana, "$.kana")), speaker.id, true, options);
209
- return {
210
- kana: accentPhrasesToKana(phrases),
211
- speaker,
212
- phrases: phrases.map((phrase) => ({
213
- text: phrase.moras.map((mora) => mora.text).join(""),
214
- accent: phrase.accent,
215
- pause: phrase.pause_mora !== null,
216
- interrogative: phrase.is_interrogative,
217
- moras: phrase.moras.map((mora) => ({
218
- text: mora.text,
219
- consonant: mora.consonant,
220
- vowel: mora.vowel,
221
- consonantLength: mora.consonant_length,
222
- vowelLength: mora.vowel_length,
223
- pitch: mora.pitch,
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(kind = "all", options = {}) {
229
- const all = await this.styles.all(options);
230
- if (kind === "all")
231
- return all;
232
- return all.filter((style) => kind === "speech"
233
- ? style.type === "talk" || style.type === "streaming_talk"
234
- : style.type === "frame_decode" || style.type === "sing" || style.type === "singing_teacher");
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 endpoint = this.client.endpoint.origin;
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