@kongyo2/kongyoroid 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -8
- package/dist/cache.d.ts +1 -1
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +3 -2
- package/dist/cache.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +17 -7
- package/dist/cli/help.js.map +1 -1
- package/dist/cli.js +194 -74
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/io.d.ts +1 -0
- package/dist/io.d.ts.map +1 -1
- package/dist/io.js +13 -2
- package/dist/io.js.map +1 -1
- package/dist/kongyoroid.d.ts +2 -2
- package/dist/kongyoroid.d.ts.map +1 -1
- package/dist/kongyoroid.js +15 -6
- package/dist/kongyoroid.js.map +1 -1
- package/dist/player.d.ts.map +1 -1
- package/dist/player.js +18 -22
- package/dist/player.js.map +1 -1
- package/dist/schema.js +1 -1
- package/dist/schema.js.map +1 -1
- package/dist/song/mml.js +1 -1
- package/dist/song/mml.js.map +1 -1
- package/dist/synth/prosody.d.ts +8 -0
- package/dist/synth/prosody.d.ts.map +1 -1
- package/dist/synth/prosody.js +74 -0
- package/dist/synth/prosody.js.map +1 -1
- package/dist/synth/speech.d.ts.map +1 -1
- package/dist/synth/speech.js +15 -11
- package/dist/synth/speech.js.map +1 -1
- package/dist/synth/stream.js +3 -3
- package/dist/synth/stream.js.map +1 -1
- package/dist/synth/textstream.d.ts +1 -1
- package/dist/synth/textstream.d.ts.map +1 -1
- package/dist/synth/textstream.js +102 -20
- package/dist/synth/textstream.js.map +1 -1
- package/dist/text/frontend.d.ts +1 -0
- package/dist/text/frontend.d.ts.map +1 -1
- package/dist/text/frontend.js +10 -1
- package/dist/text/frontend.js.map +1 -1
- package/dist/text/normalize.d.ts.map +1 -1
- package/dist/text/normalize.js +8 -0
- package/dist/text/normalize.js.map +1 -1
- package/dist/text/sentences.d.ts +1 -0
- package/dist/text/sentences.d.ts.map +1 -1
- package/dist/text/sentences.js +14 -7
- package/dist/text/sentences.js.map +1 -1
- package/dist/version.js +2 -2
- package/dist/wav.d.ts +1 -0
- package/dist/wav.d.ts.map +1 -1
- package/dist/wav.js +8 -6
- package/dist/wav.js.map +1 -1
- package/docs/cli.md +29 -19
- package/package.json +1 -3
package/dist/cli.js
CHANGED
|
@@ -5,21 +5,21 @@ import { commandHelp, rootHelp } from "./cli/help.js";
|
|
|
5
5
|
import { ALLOWED, assign, checkAllowed, env, numberFlag, parseCommandLine, parseDictEntries, styleFlag, } from "./cli/flags.js";
|
|
6
6
|
import { DiskCache } from "./cache.js";
|
|
7
7
|
import { EXIT_CODES, KongyoroidError, asKongyoroidError, exitCodeOf, formatDiagnostic, invalid } from "./errors.js";
|
|
8
|
-
import { existingFileSha256, readBytes, readChunks, readLines, readText, writeAudioIdempotent, writeStream, writeTextFile, writeWithBackpressure, } from "./io.js";
|
|
9
|
-
import { LIMITS } from "./limits.js";
|
|
8
|
+
import { ensureReadableFile, existingFileSha256, readBytes, readChunks, readLines, readText, writeAudioIdempotent, writeStream, writeTextFile, writeWithBackpressure, } from "./io.js";
|
|
9
|
+
import { DEFAULT_SAMPLE_RATE, LIMITS } from "./limits.js";
|
|
10
10
|
import { Kongyoroid } from "./kongyoroid.js";
|
|
11
11
|
import { playWav } from "./player.js";
|
|
12
12
|
import { parseEngine, parseJson } from "./request.js";
|
|
13
13
|
import { BATCH_JOB_SCHEMA, CAPABILITIES, DICTIONARY_SCHEMA, REQUEST_SCHEMA } from "./schema.js";
|
|
14
14
|
import { pcm16Bytes, streamingWavHeader } from "./synth/stream.js";
|
|
15
15
|
import { LocalDictionary } from "./text/dictionary.js";
|
|
16
|
-
import { isObject, keys, literal, object, string } from "./validate.js";
|
|
16
|
+
import { integer, isObject, keys, literal, object, string } from "./validate.js";
|
|
17
17
|
import { PACKAGE_NAME, VERSION } from "./version.js";
|
|
18
18
|
import { wavHeader } from "./wav.js";
|
|
19
19
|
async function emit(value, error = false) {
|
|
20
20
|
await writeStream(error ? process.stderr : process.stdout, `${JSON.stringify(value)}\n`);
|
|
21
21
|
}
|
|
22
|
-
function metadata(result) {
|
|
22
|
+
function metadata(result, extraWarnings = []) {
|
|
23
23
|
return {
|
|
24
24
|
engine: result.engine,
|
|
25
25
|
engineVersion: result.engineVersion,
|
|
@@ -39,21 +39,30 @@ function metadata(result) {
|
|
|
39
39
|
...(result.peak === undefined ? {} : { peak: result.peak, rms: result.rms, limitedSamples: result.limitedSamples }),
|
|
40
40
|
elapsedMs: result.elapsedMs,
|
|
41
41
|
timings: result.timings,
|
|
42
|
-
warnings: result.warnings,
|
|
42
|
+
warnings: extraWarnings.length === 0 ? result.warnings : [...result.warnings, ...extraWarnings],
|
|
43
43
|
adjustments: result.adjustments,
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
|
+
const PLAN_UNAVAILABLE = {
|
|
47
|
+
severity: "warning",
|
|
48
|
+
code: "PLAN_UNAVAILABLE",
|
|
49
|
+
message: "--plan-out was ignored: only the formant engine produces a synthesis plan.",
|
|
50
|
+
help: "Render with --engine formant (and no VOICEVOX style) to write the plan.",
|
|
51
|
+
path: "$flags.plan-out",
|
|
52
|
+
};
|
|
46
53
|
async function reportDiagnostics(values, warnings, location) {
|
|
47
54
|
if (values.diagnostics !== "compact")
|
|
48
55
|
return;
|
|
49
56
|
for (const warning of warnings)
|
|
50
57
|
await writeStream(process.stderr, `${formatDiagnostic(warning, location)}\n`);
|
|
51
58
|
}
|
|
52
|
-
async function deliver(
|
|
53
|
-
|
|
54
|
-
|
|
59
|
+
async function deliver(result, values, signal, plan) {
|
|
60
|
+
const planOut = values["plan-out"];
|
|
61
|
+
const extraWarnings = planOut !== undefined && plan === undefined ? [PLAN_UNAVAILABLE] : [];
|
|
62
|
+
if (planOut !== undefined && planOut !== "-" && plan !== undefined) {
|
|
63
|
+
await writeTextFile(planOut, `${JSON.stringify(plan, null, 2)}\n`, values.force ?? false);
|
|
55
64
|
}
|
|
56
|
-
await reportDiagnostics(values, result.warnings, values.input ?? "<request>");
|
|
65
|
+
await reportDiagnostics(values, [...result.warnings, ...extraWarnings], values.input ?? "<request>");
|
|
57
66
|
if (values.output === "-") {
|
|
58
67
|
if (values.play)
|
|
59
68
|
invalid("$flags.play", "--play cannot be combined with --output -.", {
|
|
@@ -63,8 +72,8 @@ async function deliver(agent, result, values, signal, plan) {
|
|
|
63
72
|
return {
|
|
64
73
|
ok: true,
|
|
65
74
|
output: "stdout",
|
|
66
|
-
...metadata(result),
|
|
67
|
-
...(plan === undefined ||
|
|
75
|
+
...metadata(result, extraWarnings),
|
|
76
|
+
...(plan === undefined || planOut !== "-" ? {} : { plan }),
|
|
68
77
|
};
|
|
69
78
|
}
|
|
70
79
|
const path = values.output ?? `kongyoroid-${result.kind}-${result.sha256.slice(0, 12)}.wav`;
|
|
@@ -75,28 +84,45 @@ async function deliver(agent, result, values, signal, plan) {
|
|
|
75
84
|
path: written.path,
|
|
76
85
|
written: written.written,
|
|
77
86
|
unchanged: written.unchanged,
|
|
78
|
-
...metadata(result),
|
|
87
|
+
...metadata(result, extraWarnings),
|
|
79
88
|
...(played === undefined ? {} : { player: played.player }),
|
|
80
|
-
...(plan === undefined ||
|
|
89
|
+
...(plan === undefined || planOut !== "-" ? {} : { plan }),
|
|
81
90
|
};
|
|
82
91
|
}
|
|
83
92
|
function speechRequestFromFlags(values, text) {
|
|
84
|
-
|
|
93
|
+
return withSpeechFlags(values, { kind: "speech", text });
|
|
94
|
+
}
|
|
95
|
+
function assignExclusive(request, values, first, second) {
|
|
96
|
+
const a = numberFlag(values, first.flag);
|
|
97
|
+
const b = numberFlag(values, second.flag);
|
|
98
|
+
if (a !== undefined && b !== undefined)
|
|
99
|
+
invalid(`$flags.${second.flag}`, `Use either --${first.flag} or --${second.flag}, not both.`);
|
|
100
|
+
if (a !== undefined) {
|
|
101
|
+
request[first.key] = a;
|
|
102
|
+
delete request[second.key];
|
|
103
|
+
}
|
|
104
|
+
if (b !== undefined) {
|
|
105
|
+
request[second.key] = b;
|
|
106
|
+
delete request[first.key];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function withSpeechFlags(values, base) {
|
|
110
|
+
const request = { ...base, kind: "speech" };
|
|
85
111
|
assign(request, "engine", values.engine);
|
|
86
112
|
assign(request, "voice", values.voice);
|
|
87
113
|
assign(request, "kana", values.kana);
|
|
88
114
|
const entries = parseDictEntries(values);
|
|
89
|
-
if (entries.length > 0)
|
|
90
|
-
|
|
115
|
+
if (entries.length > 0) {
|
|
116
|
+
const existing = Array.isArray(base["dictionary"]) ? base["dictionary"] : [];
|
|
117
|
+
request["dictionary"] = [...existing, ...entries];
|
|
118
|
+
}
|
|
91
119
|
if (values["strict-reading"] === false)
|
|
92
120
|
request["strictReading"] = false;
|
|
93
121
|
assign(request, "speaker", styleFlag(values, "speaker"));
|
|
94
122
|
assign(request, "speed", numberFlag(values, "speed"));
|
|
95
|
-
|
|
96
|
-
assign(request, "pitchSemitones", numberFlag(values, "pitch-semitones"));
|
|
123
|
+
assignExclusive(request, values, { flag: "pitch", key: "pitch" }, { flag: "pitch-semitones", key: "pitchSemitones" });
|
|
97
124
|
assign(request, "intonation", numberFlag(values, "intonation"));
|
|
98
|
-
|
|
99
|
-
assign(request, "gainDb", numberFlag(values, "gain-db"));
|
|
125
|
+
assignExclusive(request, values, { flag: "volume", key: "volume" }, { flag: "gain-db", key: "gainDb" });
|
|
100
126
|
assign(request, "breathiness", numberFlag(values, "breathiness"));
|
|
101
127
|
assign(request, "prePause", numberFlag(values, "pre-pause"));
|
|
102
128
|
assign(request, "postPause", numberFlag(values, "post-pause"));
|
|
@@ -109,6 +135,30 @@ function speechRequestFromFlags(values, text) {
|
|
|
109
135
|
assign(request, "seed", numberFlag(values, "seed"));
|
|
110
136
|
return request;
|
|
111
137
|
}
|
|
138
|
+
function applyVibratoFlags(request, values, base) {
|
|
139
|
+
const depth = numberFlag(values, "vibrato-depth");
|
|
140
|
+
const rate = numberFlag(values, "vibrato-rate");
|
|
141
|
+
const delay = numberFlag(values, "vibrato-delay-ms");
|
|
142
|
+
const fade = numberFlag(values, "vibrato-fade-ms");
|
|
143
|
+
const existing = isObject(base["vibrato"]) ? base["vibrato"] : undefined;
|
|
144
|
+
if (existing === undefined && delay === undefined && fade === undefined) {
|
|
145
|
+
assign(request, "vibratoDepth", depth);
|
|
146
|
+
assign(request, "vibratoRate", rate);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const vibrato = { ...existing };
|
|
150
|
+
if (existing === undefined) {
|
|
151
|
+
assign(vibrato, "depthCents", base["vibratoDepth"]);
|
|
152
|
+
assign(vibrato, "rateHz", base["vibratoRate"]);
|
|
153
|
+
delete request["vibratoDepth"];
|
|
154
|
+
delete request["vibratoRate"];
|
|
155
|
+
}
|
|
156
|
+
assign(vibrato, "depthCents", depth);
|
|
157
|
+
assign(vibrato, "rateHz", rate);
|
|
158
|
+
assign(vibrato, "delayMs", delay);
|
|
159
|
+
assign(vibrato, "fadeMs", fade);
|
|
160
|
+
request["vibrato"] = vibrato;
|
|
161
|
+
}
|
|
112
162
|
function songRequestFromFlags(values, base) {
|
|
113
163
|
const request = { ...base, kind: "song" };
|
|
114
164
|
if (values.mml !== undefined) {
|
|
@@ -130,29 +180,20 @@ function songRequestFromFlags(values, base) {
|
|
|
130
180
|
hint: 'Example: --lyrics "ドレミ" --melody "C4 D4 E4" --beats "1 1 2"',
|
|
131
181
|
});
|
|
132
182
|
}
|
|
183
|
+
if (request["notes"] === undefined) {
|
|
184
|
+
invalid(values.input === undefined ? "$flags.melody" : "$.notes", "A song needs notes: give --lyrics with --melody, --mml, or --input FILE containing a notes list.", {
|
|
185
|
+
hint: 'Examples: kongyoroid sing --lyrics "ドレミ" --melody "C4 D4 E4" | kongyoroid sing --mml "t120 o4 c d e" | kongyoroid sing --input song.json',
|
|
186
|
+
});
|
|
187
|
+
}
|
|
133
188
|
assign(request, "engine", values.engine);
|
|
134
189
|
assign(request, "voice", values.voice);
|
|
135
190
|
assign(request, "tempo", numberFlag(values, "tempo"));
|
|
136
191
|
assign(request, "singer", styleFlag(values, "singer"));
|
|
137
192
|
assign(request, "teacher", styleFlag(values, "teacher"));
|
|
138
193
|
assign(request, "transpose", numberFlag(values, "transpose"));
|
|
139
|
-
|
|
140
|
-
assign(request, "gainDb", numberFlag(values, "gain-db"));
|
|
194
|
+
assignExclusive(request, values, { flag: "volume", key: "volume" }, { flag: "gain-db", key: "gainDb" });
|
|
141
195
|
assign(request, "breathiness", numberFlag(values, "breathiness"));
|
|
142
|
-
|
|
143
|
-
assign(request, "vibratoRate", numberFlag(values, "vibrato-rate"));
|
|
144
|
-
const delay = numberFlag(values, "vibrato-delay-ms");
|
|
145
|
-
const fade = numberFlag(values, "vibrato-fade-ms");
|
|
146
|
-
if (delay !== undefined || fade !== undefined) {
|
|
147
|
-
const vibrato = {};
|
|
148
|
-
assign(vibrato, "delayMs", delay);
|
|
149
|
-
assign(vibrato, "fadeMs", fade);
|
|
150
|
-
assign(vibrato, "depthCents", numberFlag(values, "vibrato-depth"));
|
|
151
|
-
assign(vibrato, "rateHz", numberFlag(values, "vibrato-rate"));
|
|
152
|
-
delete request["vibratoDepth"];
|
|
153
|
-
delete request["vibratoRate"];
|
|
154
|
-
request["vibrato"] = vibrato;
|
|
155
|
-
}
|
|
196
|
+
applyVibratoFlags(request, values, base);
|
|
156
197
|
assign(request, "portamentoMs", numberFlag(values, "portamento-ms"));
|
|
157
198
|
assign(request, "scoopCents", numberFlag(values, "scoop-cents"));
|
|
158
199
|
assign(request, "scoopMs", numberFlag(values, "scoop-ms"));
|
|
@@ -164,38 +205,53 @@ function songRequestFromFlags(values, base) {
|
|
|
164
205
|
assign(request, "seed", numberFlag(values, "seed"));
|
|
165
206
|
return request;
|
|
166
207
|
}
|
|
167
|
-
|
|
208
|
+
function rejectTextAndInput(values) {
|
|
168
209
|
if (values.text !== undefined && values.input !== undefined)
|
|
169
210
|
invalid("$flags", "Use either --text or --input, not both.");
|
|
211
|
+
}
|
|
212
|
+
async function textInput(values, signal) {
|
|
213
|
+
rejectTextAndInput(values);
|
|
170
214
|
if (values.text !== undefined)
|
|
171
215
|
return values.text;
|
|
172
216
|
if (values.input !== undefined)
|
|
173
217
|
return (await readText(values.input, signal)).replace(/\r?\n$/u, "");
|
|
218
|
+
if (values.kana !== undefined)
|
|
219
|
+
return values.kana;
|
|
174
220
|
return invalid("$flags.text", "Provide --text or --input FILE (use - for standard input).", {
|
|
175
221
|
hint: 'Example: kongyoroid speak --text "こんにちは" -o hello.wav',
|
|
176
222
|
});
|
|
177
223
|
}
|
|
224
|
+
function overlayRequest(values, json) {
|
|
225
|
+
if (isObject(json) && json["kind"] === "song")
|
|
226
|
+
return songRequestFromFlags(values, json);
|
|
227
|
+
if (isObject(json) && json["kind"] === "speech" && typeof json["text"] === "string")
|
|
228
|
+
return withSpeechFlags(values, json);
|
|
229
|
+
if (isObject(json)) {
|
|
230
|
+
const overlay = { ...json };
|
|
231
|
+
assign(overlay, "engine", values.engine);
|
|
232
|
+
assign(overlay, "voice", values.voice);
|
|
233
|
+
return overlay;
|
|
234
|
+
}
|
|
235
|
+
return json;
|
|
236
|
+
}
|
|
178
237
|
async function requestFromInputs(values, signal, command) {
|
|
238
|
+
rejectTextAndInput(values);
|
|
179
239
|
const hasSpeech = values.text !== undefined || values.kana !== undefined;
|
|
180
240
|
const hasSong = values.lyrics !== undefined || values.melody !== undefined || values.mml !== undefined;
|
|
181
241
|
if (values.input !== undefined) {
|
|
182
242
|
const raw = await readText(values.input, signal);
|
|
183
|
-
const
|
|
184
|
-
if (
|
|
243
|
+
const looksLikeJson = raw.trim().startsWith("{");
|
|
244
|
+
if (!looksLikeJson && !hasSong)
|
|
185
245
|
return speechRequestFromFlags(values, raw.replace(/\r?\n$/u, ""));
|
|
186
|
-
|
|
187
|
-
if (isObject(json) && json["kind"] === "song")
|
|
188
|
-
return songRequestFromFlags(values, json);
|
|
189
|
-
if (isObject(json) && values.engine !== undefined)
|
|
190
|
-
return { ...json, engine: values.engine };
|
|
191
|
-
return json;
|
|
246
|
+
return overlayRequest(values, parseJson(raw));
|
|
192
247
|
}
|
|
193
248
|
if (hasSong)
|
|
194
249
|
return songRequestFromFlags(values, {});
|
|
195
|
-
if (hasSpeech
|
|
250
|
+
if (hasSpeech)
|
|
196
251
|
return speechRequestFromFlags(values, await textInput(values, signal));
|
|
197
|
-
|
|
198
|
-
|
|
252
|
+
const scoreHint = command === "speak" ? "" : ", or --lyrics/--melody/--mml";
|
|
253
|
+
return invalid("$flags.input", `Provide --input FILE|- (text or a request JSON) or --text${scoreHint} for ${command}.`, {
|
|
254
|
+
hint: `Examples: kongyoroid ${command} --text "こんにちは" | kongyoroid ${command} --input request.json`,
|
|
199
255
|
});
|
|
200
256
|
}
|
|
201
257
|
async function loadDictionaryFile(path, tolerateMissing) {
|
|
@@ -264,33 +320,84 @@ async function runPlan(agent, values, signal) {
|
|
|
264
320
|
const result = await agent.plan(request, { signal, detail: planDetail(values) });
|
|
265
321
|
await emit(result);
|
|
266
322
|
}
|
|
323
|
+
async function* single(text) {
|
|
324
|
+
yield text;
|
|
325
|
+
}
|
|
326
|
+
async function* prepend(head, iterator) {
|
|
327
|
+
for (const chunk of head)
|
|
328
|
+
yield chunk;
|
|
329
|
+
try {
|
|
330
|
+
while (true) {
|
|
331
|
+
const next = await iterator.next();
|
|
332
|
+
if (next.done === true)
|
|
333
|
+
return;
|
|
334
|
+
yield next.value;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
finally {
|
|
338
|
+
await iterator.return?.();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
async function streamInput(values, signal) {
|
|
342
|
+
const fromFlags = () => {
|
|
343
|
+
const request = speechRequestFromFlags(values, "placeholder");
|
|
344
|
+
delete request["text"];
|
|
345
|
+
return request;
|
|
346
|
+
};
|
|
347
|
+
if (values.text !== undefined)
|
|
348
|
+
return { request: fromFlags(), source: single(values.text) };
|
|
349
|
+
const iterator = readChunks(values.input ?? "-", signal)[Symbol.asyncIterator]();
|
|
350
|
+
const head = [];
|
|
351
|
+
let leading = "";
|
|
352
|
+
while (leading.trim().length === 0) {
|
|
353
|
+
const next = await iterator.next();
|
|
354
|
+
if (next.done === true)
|
|
355
|
+
break;
|
|
356
|
+
head.push(next.value);
|
|
357
|
+
leading += next.value;
|
|
358
|
+
}
|
|
359
|
+
if (!leading.trimStart().startsWith("{"))
|
|
360
|
+
return { request: fromFlags(), source: prepend(head, iterator) };
|
|
361
|
+
for (let next = await iterator.next(); next.done !== true; next = await iterator.next())
|
|
362
|
+
head.push(next.value);
|
|
363
|
+
const json = object(parseJson(head.join("")), "$");
|
|
364
|
+
if (json["kind"] !== undefined && json["kind"] !== "speech")
|
|
365
|
+
invalid("$.kind", "speak accepts only speech requests; use sing or render for songs.");
|
|
366
|
+
const text = json["text"];
|
|
367
|
+
if (typeof text !== "string")
|
|
368
|
+
invalid("$.text", "A request JSON given to --stream needs a text string to stream.", {
|
|
369
|
+
hint: 'Example: {"kind":"speech","text":"一文目。二文目。","voice":"soft"}',
|
|
370
|
+
});
|
|
371
|
+
const request = withSpeechFlags(values, json);
|
|
372
|
+
delete request["text"];
|
|
373
|
+
return { request, source: single(text) };
|
|
374
|
+
}
|
|
267
375
|
async function runSpeakStream(agent, values, signal) {
|
|
268
376
|
if (values.play)
|
|
269
377
|
invalid("$flags.play", "--play cannot be combined with --stream.");
|
|
378
|
+
if (values.kana !== undefined)
|
|
379
|
+
invalid("$flags.kana", "--kana cannot be combined with --stream; streaming reads each sentence from the text.", {
|
|
380
|
+
hint: "Give readings with --dict-entry SURFACE=READING[:ACCENT], or drop --stream to speak the kana at once.",
|
|
381
|
+
});
|
|
382
|
+
rejectTextAndInput(values);
|
|
270
383
|
const format = literal(values.format ?? "wav", "$flags.format", ["wav", "pcm", "ndjson"]);
|
|
271
384
|
const output = values.output ?? "-";
|
|
272
385
|
if (output !== "-" && format !== "wav")
|
|
273
386
|
invalid("$flags.format", "Streaming to a file writes WAV; use --output - for pcm or ndjson.");
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
const source = values
|
|
277
|
-
|
|
278
|
-
yield values.text ?? "";
|
|
279
|
-
})()
|
|
280
|
-
: readChunks(values.input ?? "-", signal);
|
|
281
|
-
const events = agent.speakStream(source, input, { signal });
|
|
387
|
+
const flushRaw = numberFlag(values, "flush-ms");
|
|
388
|
+
const flushMs = flushRaw === undefined ? undefined : integer(flushRaw, "$flags.flush-ms", 1, 600_000);
|
|
389
|
+
const { request: input, source } = await streamInput(values, signal);
|
|
390
|
+
const events = agent.speakStream(source, input, { signal, ...(flushMs === undefined ? {} : { flushMs }) });
|
|
282
391
|
const stdout = process.stdout;
|
|
283
392
|
let sampleRate = 0;
|
|
284
393
|
let file;
|
|
285
394
|
let fileFrames = 0;
|
|
286
|
-
|
|
287
|
-
let filePath = "";
|
|
395
|
+
const filePath = output === "-" ? "" : resolve(output);
|
|
288
396
|
const sentences = [];
|
|
289
397
|
const openFile = async () => {
|
|
290
|
-
if (output === "-")
|
|
398
|
+
if (output === "-" || file !== undefined)
|
|
291
399
|
return;
|
|
292
400
|
const { open } = await import("node:fs/promises");
|
|
293
|
-
filePath = resolve(output);
|
|
294
401
|
if (!values.force) {
|
|
295
402
|
const existing = await existingFileSha256(filePath);
|
|
296
403
|
if (existing !== undefined)
|
|
@@ -299,11 +406,13 @@ async function runSpeakStream(agent, values, signal) {
|
|
|
299
406
|
repairOptions: [{ action: "use-force", description: "Overwrite with --force." }],
|
|
300
407
|
});
|
|
301
408
|
}
|
|
409
|
+
if (sampleRate === 0)
|
|
410
|
+
sampleRate = numberFlag(values, "sample-rate") ?? DEFAULT_SAMPLE_RATE;
|
|
302
411
|
file = await open(filePath, "w", 0o644);
|
|
303
412
|
await file.write(wavHeader(0, sampleRate));
|
|
304
|
-
fileBytes = 44;
|
|
305
413
|
};
|
|
306
414
|
let headerWritten = false;
|
|
415
|
+
let completed = false;
|
|
307
416
|
try {
|
|
308
417
|
for await (const event of events) {
|
|
309
418
|
if (event.type === "sentence") {
|
|
@@ -333,7 +442,6 @@ async function runSpeakStream(agent, values, signal) {
|
|
|
333
442
|
if (file !== undefined) {
|
|
334
443
|
await file.write(bytes);
|
|
335
444
|
fileFrames += block.samples.length;
|
|
336
|
-
fileBytes += bytes.length;
|
|
337
445
|
continue;
|
|
338
446
|
}
|
|
339
447
|
if (format === "ndjson") {
|
|
@@ -347,12 +455,16 @@ async function runSpeakStream(agent, values, signal) {
|
|
|
347
455
|
await writeWithBackpressure(stdout, bytes, signal);
|
|
348
456
|
continue;
|
|
349
457
|
}
|
|
458
|
+
completed = true;
|
|
459
|
+
if (output !== "-")
|
|
460
|
+
await openFile();
|
|
350
461
|
const summary = {
|
|
351
462
|
ok: true,
|
|
352
463
|
operation: "speak",
|
|
353
464
|
stream: true,
|
|
354
465
|
format,
|
|
355
466
|
output: output === "-" ? "stdout" : filePath,
|
|
467
|
+
...(output === "-" ? {} : { written: true }),
|
|
356
468
|
sentences: event.sentences,
|
|
357
469
|
frames: event.frames,
|
|
358
470
|
durationSeconds: Math.round(event.durationSeconds * 1000) / 1000,
|
|
@@ -370,8 +482,8 @@ async function runSpeakStream(agent, values, signal) {
|
|
|
370
482
|
const header = wavHeader(fileFrames, sampleRate);
|
|
371
483
|
await file.write(header, 0, 44, 0);
|
|
372
484
|
await file.close();
|
|
373
|
-
void fileBytes;
|
|
374
485
|
}
|
|
486
|
+
void completed;
|
|
375
487
|
}
|
|
376
488
|
}
|
|
377
489
|
async function runBatch(agent, values, signal, onFailure) {
|
|
@@ -598,6 +710,13 @@ async function runCache(values, positionals) {
|
|
|
598
710
|
await emit({ ok: true, operation: "cache.stats", ...(await cache.stats()) });
|
|
599
711
|
return;
|
|
600
712
|
}
|
|
713
|
+
const maxBytes = numberFlag(values, "max-bytes");
|
|
714
|
+
const maxEntries = numberFlag(values, "max-entries");
|
|
715
|
+
if (action === "prune" && maxBytes === undefined && maxEntries === undefined) {
|
|
716
|
+
invalid("$flags.max-bytes", "cache prune needs --max-bytes N and/or --max-entries N.", {
|
|
717
|
+
hint: "Example: kongyoroid cache prune --cache-dir ./cache --max-bytes 200000000",
|
|
718
|
+
});
|
|
719
|
+
}
|
|
601
720
|
const before = await cache.stats();
|
|
602
721
|
if (values["dry-run"]) {
|
|
603
722
|
await emit({
|
|
@@ -605,14 +724,12 @@ async function runCache(values, positionals) {
|
|
|
605
724
|
operation: `cache.${action}`,
|
|
606
725
|
dryRun: true,
|
|
607
726
|
directory: cache.directory,
|
|
608
|
-
wouldRemove: action === "clear" ? before.entries :
|
|
727
|
+
wouldRemove: action === "clear" ? before.entries : await cache.prune(maxBytes, maxEntries, true),
|
|
609
728
|
...before,
|
|
610
729
|
});
|
|
611
730
|
return;
|
|
612
731
|
}
|
|
613
|
-
const removed = action === "clear"
|
|
614
|
-
? await cache.clear()
|
|
615
|
-
: await cache.prune(numberFlag(values, "max-bytes"), numberFlag(values, "max-entries"));
|
|
732
|
+
const removed = action === "clear" ? await cache.clear() : await cache.prune(maxBytes, maxEntries);
|
|
616
733
|
await emit({ ok: true, operation: `cache.${action}`, directory: cache.directory, removed, ...(await cache.stats()) });
|
|
617
734
|
}
|
|
618
735
|
async function main(argv) {
|
|
@@ -665,8 +782,9 @@ async function main(argv) {
|
|
|
665
782
|
const signal = controller.signal;
|
|
666
783
|
try {
|
|
667
784
|
if (command === "play") {
|
|
668
|
-
const path = string(positionals[1], "$.play.file", 1, 4096);
|
|
669
|
-
await
|
|
785
|
+
const path = resolve(string(positionals[1], "$.play.file", 1, 4096));
|
|
786
|
+
await ensureReadableFile(path);
|
|
787
|
+
await emit({ ok: true, operation: "play", path, ...(await playWav(path, signal)) });
|
|
670
788
|
return;
|
|
671
789
|
}
|
|
672
790
|
if (command === "inspect") {
|
|
@@ -689,11 +807,13 @@ async function main(argv) {
|
|
|
689
807
|
const agent = await createAgent(values, command === "dict");
|
|
690
808
|
switch (command) {
|
|
691
809
|
case "speak": {
|
|
692
|
-
if (values.stream) {
|
|
810
|
+
if (values.stream && !values["dry-run"]) {
|
|
693
811
|
await runSpeakStream(agent, values, signal);
|
|
694
812
|
return;
|
|
695
813
|
}
|
|
696
|
-
const request =
|
|
814
|
+
const request = await requestFromInputs(values, signal, "speak");
|
|
815
|
+
if (isObject(request) && request["kind"] !== "speech")
|
|
816
|
+
invalid("$.kind", "speak accepts only speech requests; use sing or render for songs.");
|
|
697
817
|
if (values["dry-run"]) {
|
|
698
818
|
await runValidateRequest(agent, request, values, signal);
|
|
699
819
|
return;
|
|
@@ -702,7 +822,7 @@ async function main(argv) {
|
|
|
702
822
|
const plan = values["plan-out"] === undefined || result.engine !== "formant"
|
|
703
823
|
? undefined
|
|
704
824
|
: (await agent.plan(request, { signal, detail: "phonemes" })).plan;
|
|
705
|
-
await emit(await deliver(
|
|
825
|
+
await emit(await deliver(result, values, signal, plan), values.output === "-");
|
|
706
826
|
return;
|
|
707
827
|
}
|
|
708
828
|
case "sing": {
|
|
@@ -718,7 +838,7 @@ async function main(argv) {
|
|
|
718
838
|
const plan = values["plan-out"] === undefined || result.engine !== "formant"
|
|
719
839
|
? undefined
|
|
720
840
|
: (await agent.plan(request, { signal, detail: "phonemes" })).plan;
|
|
721
|
-
await emit(await deliver(
|
|
841
|
+
await emit(await deliver(result, values, signal, plan), values.output === "-");
|
|
722
842
|
return;
|
|
723
843
|
}
|
|
724
844
|
case "render": {
|
|
@@ -739,7 +859,7 @@ async function main(argv) {
|
|
|
739
859
|
const plan = values["plan-out"] === undefined || result.engine !== "formant"
|
|
740
860
|
? undefined
|
|
741
861
|
: (await agent.plan(withFlags, { signal, detail: "phonemes" })).plan;
|
|
742
|
-
await emit(await deliver(
|
|
862
|
+
await emit(await deliver(result, values, signal, plan), values.output === "-");
|
|
743
863
|
return;
|
|
744
864
|
}
|
|
745
865
|
case "validate":
|