@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.
Files changed (199) hide show
  1. package/README.md +233 -0
  2. package/dist/cache.d.ts +32 -7
  3. package/dist/cache.d.ts.map +1 -1
  4. package/dist/cache.js +138 -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 +336 -0
  13. package/dist/cli/help.js.map +1 -0
  14. package/dist/cli.js +598 -232
  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 +11 -0
  25. package/dist/io.d.ts.map +1 -1
  26. package/dist/io.js +149 -5
  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 +777 -86
  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/request.d.ts +1 -0
  37. package/dist/request.d.ts.map +1 -1
  38. package/dist/request.js +102 -15
  39. package/dist/request.js.map +1 -1
  40. package/dist/schema.d.ts +3 -0
  41. package/dist/schema.d.ts.map +1 -1
  42. package/dist/schema.js +271 -68
  43. package/dist/schema.js.map +1 -1
  44. package/dist/song/mml.d.ts +15 -0
  45. package/dist/song/mml.d.ts.map +1 -0
  46. package/dist/song/mml.js +184 -0
  47. package/dist/song/mml.js.map +1 -0
  48. package/dist/song/pitch.d.ts.map +1 -0
  49. package/dist/{pitch.js → song/pitch.js} +1 -1
  50. package/dist/song/pitch.js.map +1 -0
  51. package/dist/song/score.d.ts +92 -0
  52. package/dist/song/score.d.ts.map +1 -0
  53. package/dist/song/score.js +454 -0
  54. package/dist/song/score.js.map +1 -0
  55. package/dist/synth/acoustics.d.ts +27 -0
  56. package/dist/synth/acoustics.d.ts.map +1 -0
  57. package/dist/synth/acoustics.js +608 -0
  58. package/dist/synth/acoustics.js.map +1 -0
  59. package/dist/synth/analysis.d.ts +22 -0
  60. package/dist/synth/analysis.d.ts.map +1 -0
  61. package/dist/synth/analysis.js +170 -0
  62. package/dist/synth/analysis.js.map +1 -0
  63. package/dist/synth/engine.d.ts +115 -0
  64. package/dist/synth/engine.d.ts.map +1 -0
  65. package/dist/synth/engine.js +223 -0
  66. package/dist/synth/engine.js.map +1 -0
  67. package/dist/synth/filters.d.ts +60 -0
  68. package/dist/synth/filters.d.ts.map +1 -0
  69. package/dist/synth/filters.js +166 -0
  70. package/dist/synth/filters.js.map +1 -0
  71. package/dist/synth/glottal.d.ts +20 -0
  72. package/dist/synth/glottal.d.ts.map +1 -0
  73. package/dist/synth/glottal.js +76 -0
  74. package/dist/synth/glottal.js.map +1 -0
  75. package/dist/synth/phonemes.d.ts +42 -0
  76. package/dist/synth/phonemes.d.ts.map +1 -0
  77. package/dist/synth/phonemes.js +311 -0
  78. package/dist/synth/phonemes.js.map +1 -0
  79. package/dist/synth/plan.d.ts +127 -0
  80. package/dist/synth/plan.d.ts.map +1 -0
  81. package/dist/synth/plan.js +81 -0
  82. package/dist/synth/plan.js.map +1 -0
  83. package/dist/synth/prosody.d.ts +30 -0
  84. package/dist/synth/prosody.d.ts.map +1 -0
  85. package/dist/synth/prosody.js +46 -0
  86. package/dist/synth/prosody.js.map +1 -0
  87. package/dist/synth/random.d.ts +15 -0
  88. package/dist/synth/random.d.ts.map +1 -0
  89. package/dist/synth/random.js +59 -0
  90. package/dist/synth/random.js.map +1 -0
  91. package/dist/synth/renderer.d.ts +77 -0
  92. package/dist/synth/renderer.d.ts.map +1 -0
  93. package/dist/synth/renderer.js +343 -0
  94. package/dist/synth/renderer.js.map +1 -0
  95. package/dist/synth/song.d.ts +30 -0
  96. package/dist/synth/song.d.ts.map +1 -0
  97. package/dist/synth/song.js +453 -0
  98. package/dist/synth/song.js.map +1 -0
  99. package/dist/synth/speech.d.ts +49 -0
  100. package/dist/synth/speech.d.ts.map +1 -0
  101. package/dist/synth/speech.js +354 -0
  102. package/dist/synth/speech.js.map +1 -0
  103. package/dist/synth/stream.d.ts +26 -0
  104. package/dist/synth/stream.d.ts.map +1 -0
  105. package/dist/synth/stream.js +67 -0
  106. package/dist/synth/stream.js.map +1 -0
  107. package/dist/synth/textstream.d.ts +35 -0
  108. package/dist/synth/textstream.d.ts.map +1 -0
  109. package/dist/synth/textstream.js +127 -0
  110. package/dist/synth/textstream.js.map +1 -0
  111. package/dist/synth/voice.d.ts +64 -0
  112. package/dist/synth/voice.d.ts.map +1 -0
  113. package/dist/synth/voice.js +208 -0
  114. package/dist/synth/voice.js.map +1 -0
  115. package/dist/text/dictionary.d.ts +50 -0
  116. package/dist/text/dictionary.d.ts.map +1 -0
  117. package/dist/text/dictionary.js +0 -0
  118. package/dist/text/dictionary.js.map +1 -0
  119. package/dist/text/frontend.d.ts +34 -0
  120. package/dist/text/frontend.d.ts.map +1 -0
  121. package/dist/text/frontend.js +550 -0
  122. package/dist/text/frontend.js.map +1 -0
  123. package/dist/text/lexicon.d.ts +10 -0
  124. package/dist/text/lexicon.d.ts.map +1 -0
  125. package/dist/text/lexicon.js +763 -0
  126. package/dist/text/lexicon.js.map +1 -0
  127. package/dist/{mora.d.ts → text/mora.d.ts} +15 -3
  128. package/dist/text/mora.d.ts.map +1 -0
  129. package/dist/{mora.js → text/mora.js} +87 -9
  130. package/dist/text/mora.js.map +1 -0
  131. package/dist/text/normalize.d.ts +34 -0
  132. package/dist/text/normalize.d.ts.map +1 -0
  133. package/dist/text/normalize.js +496 -0
  134. package/dist/text/normalize.js.map +1 -0
  135. package/dist/text/notation.d.ts +32 -0
  136. package/dist/text/notation.d.ts.map +1 -0
  137. package/dist/text/notation.js +182 -0
  138. package/dist/text/notation.js.map +1 -0
  139. package/dist/text/reading.d.ts +52 -0
  140. package/dist/text/reading.d.ts.map +1 -0
  141. package/dist/text/reading.js +32 -0
  142. package/dist/text/reading.js.map +1 -0
  143. package/dist/text/sentences.d.ts +10 -0
  144. package/dist/text/sentences.d.ts.map +1 -0
  145. package/dist/text/sentences.js +84 -0
  146. package/dist/text/sentences.js.map +1 -0
  147. package/dist/{text.d.ts → text/split.d.ts} +2 -2
  148. package/dist/text/split.d.ts.map +1 -0
  149. package/dist/{text.js → text/split.js} +2 -2
  150. package/dist/text/split.js.map +1 -0
  151. package/dist/types.d.ts +58 -3
  152. package/dist/types.d.ts.map +1 -1
  153. package/dist/version.d.ts +4 -0
  154. package/dist/version.d.ts.map +1 -1
  155. package/dist/version.js +5 -1
  156. package/dist/version.js.map +1 -1
  157. package/dist/voicevox/client.d.ts +1 -1
  158. package/dist/voicevox/client.d.ts.map +1 -1
  159. package/dist/voicevox/dictionary.js +1 -1
  160. package/dist/voicevox/dictionary.js.map +1 -1
  161. package/dist/voicevox/song.js +1 -1
  162. package/dist/voicevox/song.js.map +1 -1
  163. package/dist/voicevox/speech.d.ts +1 -0
  164. package/dist/voicevox/speech.d.ts.map +1 -1
  165. package/dist/voicevox/speech.js +8 -2
  166. package/dist/voicevox/speech.js.map +1 -1
  167. package/docs/cli.md +251 -0
  168. package/examples/batch.jsonl +4 -3
  169. package/examples/builtin/dictionary.json +7 -0
  170. package/examples/builtin/song-compact.json +13 -0
  171. package/examples/builtin/song-mml.json +9 -0
  172. package/examples/builtin/song-notes.json +25 -0
  173. package/examples/builtin/speech-dictionary.json +9 -0
  174. package/examples/builtin/speech-kana.json +7 -0
  175. package/examples/builtin/speech.json +7 -0
  176. package/package.json +11 -3
  177. package/dist/formant.d.ts +0 -34
  178. package/dist/formant.d.ts.map +0 -1
  179. package/dist/formant.js +0 -622
  180. package/dist/formant.js.map +0 -1
  181. package/dist/mora.d.ts.map +0 -1
  182. package/dist/mora.js.map +0 -1
  183. package/dist/notation.d.ts +0 -15
  184. package/dist/notation.d.ts.map +0 -1
  185. package/dist/notation.js +0 -114
  186. package/dist/notation.js.map +0 -1
  187. package/dist/pitch.d.ts.map +0 -1
  188. package/dist/pitch.js.map +0 -1
  189. package/dist/score.d.ts +0 -37
  190. package/dist/score.d.ts.map +0 -1
  191. package/dist/score.js +0 -166
  192. package/dist/score.js.map +0 -1
  193. package/dist/text.d.ts.map +0 -1
  194. package/dist/text.js.map +0 -1
  195. package/examples/song-notes.json +0 -24
  196. /package/dist/{pitch.d.ts → song/pitch.d.ts} +0 -0
  197. /package/examples/{song.json → voicevox/song.json} +0 -0
  198. /package/examples/{speech-kana.json → voicevox/speech-kana.json} +0 -0
  199. /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";
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({ 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,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 resolved = parseRequest(this.withDefaults(request));
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" ? await this.dictionaryDigest(options) : "";
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" ? [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"));
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
- 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));
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
- elapsedMs: Math.round((performance.now() - start) * 100) / 100,
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 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,
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(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");
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 endpoint = this.client.endpoint.origin;
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