@onjmin/koe 1.0.8 → 1.0.10
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 +440 -375
- package/dist/index.d.ts +432 -9
- package/dist/index.js +854 -87
- package/dist/index.js.map +1 -1
- package/dist/utautts/frame-intonation-v8.json +34590 -0
- package/dist/utautts/hts/COPYRIGHT.txt +30 -0
- package/dist/utautts/hts/README.md +11 -0
- package/dist/utautts/hts/tohoku-f01-neutral.htsvoice +0 -0
- package/dist/utautts/jpreprocess_wasm/jpreprocess_wasm.js +436 -0
- package/dist/utautts/jpreprocess_wasm/jpreprocess_wasm_bg.wasm +0 -0
- package/dist/utautts/jpreprocess_wasm/naist-jdic/char_def.bin.gz +0 -0
- package/dist/utautts/jpreprocess_wasm/naist-jdic/dict.da.gz +0 -0
- package/dist/utautts/jpreprocess_wasm/naist-jdic/dict.vals.gz +0 -0
- package/dist/utautts/jpreprocess_wasm/naist-jdic/dict.words.gz +0 -0
- package/dist/utautts/jpreprocess_wasm/naist-jdic/dict.wordsidx.gz +0 -0
- package/dist/utautts/jpreprocess_wasm/naist-jdic/matrix.mtx.gz +0 -0
- package/dist/utautts/jpreprocess_wasm/naist-jdic/metadata.json.gz +0 -0
- package/dist/utautts/jpreprocess_wasm/naist-jdic/unk.bin.gz +0 -0
- package/dist/utautts/utautts.wasm +0 -0
- package/dist/utautts/wasm_exec.js +575 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1759,10 +1759,18 @@ var Worldline = class _Worldline {
|
|
|
1759
1759
|
* {@link MIN_WORLDLINE_SAMPLES} (too short for stable F0 analysis).
|
|
1760
1760
|
*/
|
|
1761
1761
|
renderPhrase(params) {
|
|
1762
|
-
const {
|
|
1762
|
+
const {
|
|
1763
|
+
units,
|
|
1764
|
+
pitch,
|
|
1765
|
+
gender = 0.5,
|
|
1766
|
+
tension = 0.5,
|
|
1767
|
+
breathiness = 0.5,
|
|
1768
|
+
voicing = 1,
|
|
1769
|
+
tempo = 120
|
|
1770
|
+
} = params;
|
|
1763
1771
|
if (units.length === 0) return null;
|
|
1764
1772
|
const WL = this.wasm;
|
|
1765
|
-
const
|
|
1773
|
+
const FS2 = WORLDLINE_SAMPLE_RATE;
|
|
1766
1774
|
let totalMs = 0;
|
|
1767
1775
|
for (const u of units) {
|
|
1768
1776
|
const endMs = u.posMs + u.lengthMs;
|
|
@@ -1781,19 +1789,19 @@ var Worldline = class _Worldline {
|
|
|
1781
1789
|
pointersToFree.push(samplePtr);
|
|
1782
1790
|
WL.HEAPF64.set(u.pcm, samplePtr >> 3);
|
|
1783
1791
|
const sv = (off, val, type) => WL.setValue(reqPtr + off, val, type);
|
|
1784
|
-
sv(0,
|
|
1792
|
+
sv(0, FS2, "i32");
|
|
1785
1793
|
sv(4, u.pcm.length, "i32");
|
|
1786
1794
|
sv(8, samplePtr, "*");
|
|
1787
1795
|
sv(12, 0, "i32");
|
|
1788
1796
|
sv(16, 0, "*");
|
|
1789
|
-
sv(20, 69, "i32");
|
|
1797
|
+
sv(20, u.tone ?? 69, "i32");
|
|
1790
1798
|
sv(24, 100, "double");
|
|
1791
1799
|
sv(32, 0, "double");
|
|
1792
|
-
sv(40, u.lengthMs, "double");
|
|
1800
|
+
sv(40, u.requiredLengthMs ?? u.lengthMs, "double");
|
|
1793
1801
|
sv(48, u.consonantMs, "double");
|
|
1794
1802
|
const cutMs = u.cutMs ?? WL_FRAME_MS * 2;
|
|
1795
1803
|
sv(56, cutMs, "double");
|
|
1796
|
-
sv(64, 100, "double");
|
|
1804
|
+
sv(64, u.volume ?? 100, "double");
|
|
1797
1805
|
sv(72, 0, "double");
|
|
1798
1806
|
sv(80, tempo, "double");
|
|
1799
1807
|
sv(88, 0, "i32");
|
|
@@ -1804,7 +1812,16 @@ var Worldline = class _Worldline {
|
|
|
1804
1812
|
sv(108, 0, "i32");
|
|
1805
1813
|
sv(112, 0, "i32");
|
|
1806
1814
|
sv(116, 100, "i32");
|
|
1807
|
-
WL._PhraseSynthAddRequest(
|
|
1815
|
+
WL._PhraseSynthAddRequest(
|
|
1816
|
+
ps,
|
|
1817
|
+
reqPtr,
|
|
1818
|
+
u.posMs,
|
|
1819
|
+
u.skipMs,
|
|
1820
|
+
u.lengthMs,
|
|
1821
|
+
u.fadeInMs,
|
|
1822
|
+
u.fadeOutMs,
|
|
1823
|
+
0
|
|
1824
|
+
);
|
|
1808
1825
|
}
|
|
1809
1826
|
totalMs += WL_FRAME_MS * 2;
|
|
1810
1827
|
const nFrames = Math.ceil(totalMs / WL_FRAME_MS) + 4;
|
|
@@ -1832,7 +1849,16 @@ var Worldline = class _Worldline {
|
|
|
1832
1849
|
WL.HEAPF64.set(tArr, tPtr >> 3);
|
|
1833
1850
|
WL.HEAPF64.set(bArr, bPtr >> 3);
|
|
1834
1851
|
WL.HEAPF64.set(vArr, vPtr >> 3);
|
|
1835
|
-
WL._PhraseSynthSetCurves(
|
|
1852
|
+
WL._PhraseSynthSetCurves(
|
|
1853
|
+
ps,
|
|
1854
|
+
f0Ptr,
|
|
1855
|
+
gPtr,
|
|
1856
|
+
tPtr,
|
|
1857
|
+
bPtr,
|
|
1858
|
+
vPtr,
|
|
1859
|
+
nFrames,
|
|
1860
|
+
WL_FRAME_MS
|
|
1861
|
+
);
|
|
1836
1862
|
}
|
|
1837
1863
|
if (f0Ptr) WL._free(f0Ptr);
|
|
1838
1864
|
if (gPtr) WL._free(gPtr);
|
|
@@ -1871,8 +1897,12 @@ var Worldline = class _Worldline {
|
|
|
1871
1897
|
} = params;
|
|
1872
1898
|
if (!pcm || pcm.length < MIN_WORLDLINE_SAMPLES) return null;
|
|
1873
1899
|
const WL = this.wasm;
|
|
1874
|
-
const
|
|
1875
|
-
const basePitch = sampleCurve(
|
|
1900
|
+
const FS2 = WORLDLINE_SAMPLE_RATE;
|
|
1901
|
+
const basePitch = sampleCurve(
|
|
1902
|
+
preMs + durationMs / 2,
|
|
1903
|
+
pitch,
|
|
1904
|
+
preMs + durationMs
|
|
1905
|
+
);
|
|
1876
1906
|
const midiNote = Math.round(69 + 12 * Math.log2(basePitch / 440));
|
|
1877
1907
|
const posMs = 0;
|
|
1878
1908
|
const reqLen = preMs + durationMs;
|
|
@@ -1892,7 +1922,7 @@ var Worldline = class _Worldline {
|
|
|
1892
1922
|
}
|
|
1893
1923
|
WL.HEAPF64.set(pcm, samplePtr >> 3);
|
|
1894
1924
|
const sv = (off, val, type) => WL.setValue(reqPtr + off, val, type);
|
|
1895
|
-
sv(0,
|
|
1925
|
+
sv(0, FS2, "i32");
|
|
1896
1926
|
sv(4, pcm.length, "i32");
|
|
1897
1927
|
sv(8, samplePtr, "*");
|
|
1898
1928
|
sv(12, 0, "i32");
|
|
@@ -3147,119 +3177,846 @@ function encodeOto(entries) {
|
|
|
3147
3177
|
return encodeShiftJis(formatOto(entries));
|
|
3148
3178
|
}
|
|
3149
3179
|
|
|
3180
|
+
// src/utautts/assets.ts
|
|
3181
|
+
var DEFAULT_CACHE = "koe-tts-assets-v1";
|
|
3182
|
+
async function openCache(name) {
|
|
3183
|
+
if (name === null) return null;
|
|
3184
|
+
try {
|
|
3185
|
+
if (typeof caches === "undefined") return null;
|
|
3186
|
+
return await caches.open(name ?? DEFAULT_CACHE);
|
|
3187
|
+
} catch {
|
|
3188
|
+
return null;
|
|
3189
|
+
}
|
|
3190
|
+
}
|
|
3191
|
+
async function cachedCopyIsCurrent(url, cached, signal) {
|
|
3192
|
+
let head;
|
|
3193
|
+
try {
|
|
3194
|
+
head = await fetch(url, { method: "HEAD", signal, cache: "no-cache" });
|
|
3195
|
+
} catch {
|
|
3196
|
+
return true;
|
|
3197
|
+
}
|
|
3198
|
+
if (!head.ok) return true;
|
|
3199
|
+
for (const name of ["etag", "last-modified", "content-length"]) {
|
|
3200
|
+
const remote = head.headers.get(name);
|
|
3201
|
+
const local = cached.headers.get(name);
|
|
3202
|
+
if (remote && local) return remote === local;
|
|
3203
|
+
}
|
|
3204
|
+
return true;
|
|
3205
|
+
}
|
|
3206
|
+
async function fetchAsset(url, options = {}) {
|
|
3207
|
+
const { onProgress, signal, revalidate = true } = options;
|
|
3208
|
+
const cache = await openCache(options.cacheName);
|
|
3209
|
+
if (cache) {
|
|
3210
|
+
try {
|
|
3211
|
+
const hit = await cache.match(url);
|
|
3212
|
+
if (hit && (!revalidate || await cachedCopyIsCurrent(url, hit, signal))) {
|
|
3213
|
+
const total2 = Number(hit.headers.get("content-length")) || 0;
|
|
3214
|
+
onProgress?.({ url, loaded: total2, total: total2, fromCache: true });
|
|
3215
|
+
return hit;
|
|
3216
|
+
}
|
|
3217
|
+
} catch {
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
const response = await fetch(url, { signal });
|
|
3221
|
+
if (!response.ok) {
|
|
3222
|
+
throw new Error(`fetch ${url}: HTTP ${response.status}`);
|
|
3223
|
+
}
|
|
3224
|
+
const total = Number(response.headers.get("content-length")) || 0;
|
|
3225
|
+
let body;
|
|
3226
|
+
if (response.body && onProgress) {
|
|
3227
|
+
const reader = response.body.getReader();
|
|
3228
|
+
const parts = [];
|
|
3229
|
+
let loaded = 0;
|
|
3230
|
+
onProgress({ url, loaded, total, fromCache: false });
|
|
3231
|
+
for (; ; ) {
|
|
3232
|
+
const { done, value } = await reader.read();
|
|
3233
|
+
if (done) break;
|
|
3234
|
+
parts.push(value);
|
|
3235
|
+
loaded += value.byteLength;
|
|
3236
|
+
onProgress({ url, loaded, total, fromCache: false });
|
|
3237
|
+
}
|
|
3238
|
+
body = new Uint8Array(new ArrayBuffer(loaded));
|
|
3239
|
+
let offset = 0;
|
|
3240
|
+
for (const part of parts) {
|
|
3241
|
+
body.set(part, offset);
|
|
3242
|
+
offset += part.byteLength;
|
|
3243
|
+
}
|
|
3244
|
+
} else {
|
|
3245
|
+
body = new Uint8Array(await response.arrayBuffer());
|
|
3246
|
+
onProgress?.({
|
|
3247
|
+
url,
|
|
3248
|
+
loaded: body.byteLength,
|
|
3249
|
+
total: total || body.byteLength,
|
|
3250
|
+
fromCache: false
|
|
3251
|
+
});
|
|
3252
|
+
}
|
|
3253
|
+
const headers = new Headers();
|
|
3254
|
+
for (const name of ["content-type", "etag", "last-modified"]) {
|
|
3255
|
+
const value = response.headers.get(name);
|
|
3256
|
+
if (value) headers.set(name, value);
|
|
3257
|
+
}
|
|
3258
|
+
headers.set("content-length", String(body.byteLength));
|
|
3259
|
+
const buffered = new Response(body, { status: 200, headers });
|
|
3260
|
+
if (cache) {
|
|
3261
|
+
try {
|
|
3262
|
+
await cache.put(url, buffered.clone());
|
|
3263
|
+
} catch {
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
return buffered;
|
|
3267
|
+
}
|
|
3268
|
+
async function fetchAssetBytes(url, options) {
|
|
3269
|
+
return new Uint8Array(await (await fetchAsset(url, options)).arrayBuffer());
|
|
3270
|
+
}
|
|
3271
|
+
async function fetchAssetText(url, options) {
|
|
3272
|
+
return (await fetchAsset(url, options)).text();
|
|
3273
|
+
}
|
|
3274
|
+
var NAIST_JDIC_FILES = [
|
|
3275
|
+
"metadata.json",
|
|
3276
|
+
"char_def.bin",
|
|
3277
|
+
"matrix.mtx",
|
|
3278
|
+
"dict.da",
|
|
3279
|
+
"dict.vals",
|
|
3280
|
+
"unk.bin",
|
|
3281
|
+
"dict.wordsidx",
|
|
3282
|
+
"dict.words"
|
|
3283
|
+
];
|
|
3284
|
+
async function inflate(bytes) {
|
|
3285
|
+
if (typeof DecompressionStream === "undefined") {
|
|
3286
|
+
throw new Error(
|
|
3287
|
+
"DecompressionStream is not available; serve the dictionary uncompressed"
|
|
3288
|
+
);
|
|
3289
|
+
}
|
|
3290
|
+
const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("gzip"));
|
|
3291
|
+
return new Uint8Array(await new Response(stream).arrayBuffer());
|
|
3292
|
+
}
|
|
3293
|
+
async function loadNaistJdic(baseUrl, options = {}) {
|
|
3294
|
+
const { compressed = true, ...fetchOptions } = options;
|
|
3295
|
+
const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
3296
|
+
const entries = await Promise.all(
|
|
3297
|
+
NAIST_JDIC_FILES.map(async (name) => {
|
|
3298
|
+
const raw = await fetchAssetBytes(
|
|
3299
|
+
`${base}${name}${compressed ? ".gz" : ""}`,
|
|
3300
|
+
fetchOptions
|
|
3301
|
+
);
|
|
3302
|
+
return [name, compressed ? await inflate(raw) : raw];
|
|
3303
|
+
})
|
|
3304
|
+
);
|
|
3305
|
+
return Object.fromEntries(entries);
|
|
3306
|
+
}
|
|
3307
|
+
function initJpreprocessDictionary(module, data) {
|
|
3308
|
+
module.init_dictionary(
|
|
3309
|
+
data["metadata.json"],
|
|
3310
|
+
data["char_def.bin"],
|
|
3311
|
+
data["matrix.mtx"],
|
|
3312
|
+
data["dict.da"],
|
|
3313
|
+
data["dict.vals"],
|
|
3314
|
+
data["unk.bin"],
|
|
3315
|
+
data["dict.wordsidx"],
|
|
3316
|
+
data["dict.words"]
|
|
3317
|
+
);
|
|
3318
|
+
}
|
|
3319
|
+
|
|
3320
|
+
// src/utautts/hts.ts
|
|
3321
|
+
var NUCLEUS = /* @__PURE__ */ new Set([
|
|
3322
|
+
"a",
|
|
3323
|
+
"i",
|
|
3324
|
+
"u",
|
|
3325
|
+
"e",
|
|
3326
|
+
"o",
|
|
3327
|
+
"A",
|
|
3328
|
+
"I",
|
|
3329
|
+
"U",
|
|
3330
|
+
"E",
|
|
3331
|
+
"O",
|
|
3332
|
+
"N",
|
|
3333
|
+
"cl"
|
|
3334
|
+
]);
|
|
3335
|
+
function softLimit(value, knee, max2) {
|
|
3336
|
+
const magnitude = Math.abs(value);
|
|
3337
|
+
const limited = magnitude <= knee ? magnitude : knee + (magnitude - knee) * 0.5;
|
|
3338
|
+
return Math.sign(value) * Math.min(max2, limited);
|
|
3339
|
+
}
|
|
3340
|
+
var DEVOICED = /* @__PURE__ */ new Set(["A", "I", "U", "E", "O"]);
|
|
3341
|
+
function segmentPhonemes(phonemes) {
|
|
3342
|
+
const segments = [];
|
|
3343
|
+
let openStart = null;
|
|
3344
|
+
for (let i = 0; i < phonemes.length; i++) {
|
|
3345
|
+
const { phone, start_ms, duration_ms } = phonemes[i];
|
|
3346
|
+
if (phone === "sil" || phone === "pau") {
|
|
3347
|
+
if (openStart !== null) {
|
|
3348
|
+
segments.push({
|
|
3349
|
+
pause: false,
|
|
3350
|
+
startMs: openStart,
|
|
3351
|
+
durationMs: start_ms - openStart,
|
|
3352
|
+
devoiced: false
|
|
3353
|
+
});
|
|
3354
|
+
openStart = null;
|
|
3355
|
+
}
|
|
3356
|
+
if (i === 0) continue;
|
|
3357
|
+
segments.push({
|
|
3358
|
+
pause: true,
|
|
3359
|
+
startMs: start_ms,
|
|
3360
|
+
durationMs: duration_ms,
|
|
3361
|
+
devoiced: false
|
|
3362
|
+
});
|
|
3363
|
+
continue;
|
|
3364
|
+
}
|
|
3365
|
+
if (openStart === null) openStart = start_ms;
|
|
3366
|
+
if (NUCLEUS.has(phone)) {
|
|
3367
|
+
segments.push({
|
|
3368
|
+
pause: false,
|
|
3369
|
+
startMs: openStart,
|
|
3370
|
+
durationMs: start_ms + duration_ms - openStart,
|
|
3371
|
+
devoiced: DEVOICED.has(phone)
|
|
3372
|
+
});
|
|
3373
|
+
openStart = null;
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
if (openStart !== null) {
|
|
3377
|
+
const last = phonemes[phonemes.length - 1];
|
|
3378
|
+
segments.push({
|
|
3379
|
+
pause: false,
|
|
3380
|
+
startMs: openStart,
|
|
3381
|
+
durationMs: last.start_ms + last.duration_ms - openStart,
|
|
3382
|
+
devoiced: false
|
|
3383
|
+
});
|
|
3384
|
+
}
|
|
3385
|
+
return segments;
|
|
3386
|
+
}
|
|
3387
|
+
function alignHtsProsody(frames, features, options = {}) {
|
|
3388
|
+
const {
|
|
3389
|
+
intonationStrength = 1,
|
|
3390
|
+
frameMs = 10,
|
|
3391
|
+
fallbackPauseMs = 150,
|
|
3392
|
+
kneeCents = 400,
|
|
3393
|
+
maxCents = 700
|
|
3394
|
+
} = options;
|
|
3395
|
+
const segments = segmentPhonemes(frames.phonemes);
|
|
3396
|
+
const durations = new Array(features.length).fill(0);
|
|
3397
|
+
const htsStart = new Array(features.length).fill(null);
|
|
3398
|
+
const devoiced = new Array(features.length).fill(false);
|
|
3399
|
+
let s = 0;
|
|
3400
|
+
for (let f2 = 0; f2 < features.length; f2++) {
|
|
3401
|
+
const feature = features[f2];
|
|
3402
|
+
while (s < segments.length && segments[s].pause && !feature.pause) s++;
|
|
3403
|
+
const segment = segments[s];
|
|
3404
|
+
if (feature.pause) {
|
|
3405
|
+
if (segment?.pause) {
|
|
3406
|
+
durations[f2] = segment.durationMs;
|
|
3407
|
+
htsStart[f2] = segment.startMs;
|
|
3408
|
+
s++;
|
|
3409
|
+
} else {
|
|
3410
|
+
durations[f2] = fallbackPauseMs;
|
|
3411
|
+
}
|
|
3412
|
+
continue;
|
|
3413
|
+
}
|
|
3414
|
+
if (!segment) return null;
|
|
3415
|
+
durations[f2] = segment.durationMs;
|
|
3416
|
+
htsStart[f2] = segment.startMs;
|
|
3417
|
+
devoiced[f2] = segment.devoiced;
|
|
3418
|
+
s++;
|
|
3419
|
+
}
|
|
3420
|
+
if (segments.slice(s).some((segment) => !segment.pause)) return null;
|
|
3421
|
+
const f0 = frames.f0_hz;
|
|
3422
|
+
const logF0 = new Float64Array(f0.length);
|
|
3423
|
+
const voiced = [];
|
|
3424
|
+
for (let i = 0; i < f0.length; i++) if (f0[i] > 0) voiced.push(i);
|
|
3425
|
+
if (voiced.length === 0) return null;
|
|
3426
|
+
const sorted = voiced.map((i) => f0[i]).sort((a, b) => a - b);
|
|
3427
|
+
const medianHz = sorted[Math.floor(sorted.length / 2)];
|
|
3428
|
+
for (let i = 0, v = 0; i < f0.length; i++) {
|
|
3429
|
+
if (f0[i] > 0) {
|
|
3430
|
+
logF0[i] = Math.log2(f0[i]);
|
|
3431
|
+
continue;
|
|
3432
|
+
}
|
|
3433
|
+
while (v < voiced.length && voiced[v] < i) v++;
|
|
3434
|
+
const right = voiced[v];
|
|
3435
|
+
const left = v > 0 ? voiced[v - 1] : void 0;
|
|
3436
|
+
if (left === void 0) logF0[i] = Math.log2(f0[right]);
|
|
3437
|
+
else if (right === void 0) logF0[i] = Math.log2(f0[left]);
|
|
3438
|
+
else {
|
|
3439
|
+
const t = (i - left) / (right - left);
|
|
3440
|
+
logF0[i] = Math.log2(f0[left]) * (1 - t) + Math.log2(f0[right]) * t;
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
const logMedian = Math.log2(medianHz);
|
|
3444
|
+
const htsCentsAt = (tMs) => {
|
|
3445
|
+
const position = Math.max(0, tMs) / frames.frame_ms;
|
|
3446
|
+
const left = Math.min(logF0.length - 1, Math.floor(position));
|
|
3447
|
+
const right = Math.min(logF0.length - 1, left + 1);
|
|
3448
|
+
const t = position - Math.floor(position);
|
|
3449
|
+
return (logF0[left] * (1 - t) + logF0[right] * t - logMedian) * 1200;
|
|
3450
|
+
};
|
|
3451
|
+
const planStart = [];
|
|
3452
|
+
let cursor = 0;
|
|
3453
|
+
for (let f2 = 0; f2 < features.length; f2++) {
|
|
3454
|
+
planStart.push(cursor);
|
|
3455
|
+
cursor += durations[f2];
|
|
3456
|
+
}
|
|
3457
|
+
const durationMs = cursor;
|
|
3458
|
+
const count = Math.max(2, Math.ceil(durationMs / frameMs) + 2);
|
|
3459
|
+
const cents = new Array(count);
|
|
3460
|
+
let f = 0;
|
|
3461
|
+
for (let frame = 0; frame < count; frame++) {
|
|
3462
|
+
const tMs = frame * frameMs;
|
|
3463
|
+
while (f + 1 < features.length && tMs >= planStart[f + 1]) f++;
|
|
3464
|
+
const start = htsStart[f];
|
|
3465
|
+
let htsMs;
|
|
3466
|
+
if (start !== null) htsMs = start + (tMs - planStart[f]);
|
|
3467
|
+
else {
|
|
3468
|
+
const next = htsStart.slice(f + 1).find((value) => value !== null);
|
|
3469
|
+
htsMs = next ?? frames.phonemes.at(-1)?.start_ms ?? 0;
|
|
3470
|
+
}
|
|
3471
|
+
cents[frame] = softLimit(
|
|
3472
|
+
htsCentsAt(htsMs) * intonationStrength,
|
|
3473
|
+
kneeCents,
|
|
3474
|
+
maxCents
|
|
3475
|
+
);
|
|
3476
|
+
}
|
|
3477
|
+
return {
|
|
3478
|
+
moraDurationsMs: durations,
|
|
3479
|
+
pitchCurve: { frame_ms: frameMs, cents },
|
|
3480
|
+
medianHz,
|
|
3481
|
+
durationMs,
|
|
3482
|
+
devoiced
|
|
3483
|
+
};
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
// src/utautts/openjtalk_features.ts
|
|
3487
|
+
var PUNCTUATION = /* @__PURE__ */ new Set(["\u3001", "\u3002", "\uFF1F", "\uFF01", ",", ".", "?", "!"]);
|
|
3488
|
+
var SMALL_KANA = /* @__PURE__ */ new Set(["\u3041", "\u3043", "\u3045", "\u3047", "\u3049", "\u3083", "\u3085", "\u3087", "\u308E", "\u3095", "\u3096"]);
|
|
3489
|
+
function toHiragana2(character) {
|
|
3490
|
+
const code = character.charCodeAt(0);
|
|
3491
|
+
if (12449 <= code && code <= 12534) {
|
|
3492
|
+
return String.fromCharCode(code - 96);
|
|
3493
|
+
}
|
|
3494
|
+
return character;
|
|
3495
|
+
}
|
|
3496
|
+
function splitMorae(reading) {
|
|
3497
|
+
const result = [];
|
|
3498
|
+
const normalized = reading.normalize("NFC").replace(/'/g, "").replace(/’/g, "");
|
|
3499
|
+
for (const character of normalized) {
|
|
3500
|
+
if (/\s/.test(character) || PUNCTUATION.has(character)) {
|
|
3501
|
+
if (result.length > 0 && !result[result.length - 1].pause) {
|
|
3502
|
+
result.push({ mora: "", pause: true });
|
|
3503
|
+
}
|
|
3504
|
+
continue;
|
|
3505
|
+
}
|
|
3506
|
+
const mora = toHiragana2(character);
|
|
3507
|
+
if (SMALL_KANA.has(mora) && result.length > 0 && !result[result.length - 1].pause) {
|
|
3508
|
+
result[result.length - 1].mora += mora;
|
|
3509
|
+
} else {
|
|
3510
|
+
result.push({ mora, pause: false });
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
return result;
|
|
3514
|
+
}
|
|
3515
|
+
function isHigh(position, accent) {
|
|
3516
|
+
if (accent === 1) return position === 1;
|
|
3517
|
+
if (accent > 1) return 2 <= position && position <= accent;
|
|
3518
|
+
return position >= 2;
|
|
3519
|
+
}
|
|
3520
|
+
function analyze2(nodes) {
|
|
3521
|
+
const reading_parts = [];
|
|
3522
|
+
const result = [];
|
|
3523
|
+
let index = 0;
|
|
3524
|
+
while (index < nodes.length) {
|
|
3525
|
+
const node = nodes[index];
|
|
3526
|
+
const pronunciation = (node.pron || "").replace(/'/g, "").replace(/’/g, "");
|
|
3527
|
+
if (node.mora_size === 0 || PUNCTUATION.has(node.string)) {
|
|
3528
|
+
reading_parts.push(node.string || "\u3001");
|
|
3529
|
+
if (result.length > 0 && !result[result.length - 1].pause) {
|
|
3530
|
+
result.push({ mora: "", pause: true });
|
|
3531
|
+
}
|
|
3532
|
+
index++;
|
|
3533
|
+
continue;
|
|
3534
|
+
}
|
|
3535
|
+
const phrase_nodes = [];
|
|
3536
|
+
while (index < nodes.length) {
|
|
3537
|
+
const current = nodes[index];
|
|
3538
|
+
if (current.mora_size === 0 || PUNCTUATION.has(current.string)) {
|
|
3539
|
+
break;
|
|
3540
|
+
}
|
|
3541
|
+
if (phrase_nodes.length > 0 && current.chain_flag !== 1) {
|
|
3542
|
+
break;
|
|
3543
|
+
}
|
|
3544
|
+
const current_pronunciation = (current.pron || "").replace(/'/g, "").replace(/’/g, "");
|
|
3545
|
+
const morae = splitMorae(current_pronunciation).filter((item) => !item.pause);
|
|
3546
|
+
phrase_nodes.push({ current, morae });
|
|
3547
|
+
reading_parts.push(current_pronunciation);
|
|
3548
|
+
index++;
|
|
3549
|
+
}
|
|
3550
|
+
const phrase_length = phrase_nodes.reduce((sum, item) => sum + item.morae.length, 0);
|
|
3551
|
+
const accent = phrase_nodes[0].current.acc || 0;
|
|
3552
|
+
let phrase_position = 0;
|
|
3553
|
+
for (const { current, morae } of phrase_nodes) {
|
|
3554
|
+
for (let word_position = 1; word_position <= morae.length; word_position++) {
|
|
3555
|
+
phrase_position++;
|
|
3556
|
+
result.push({
|
|
3557
|
+
mora: morae[word_position - 1].mora,
|
|
3558
|
+
pause: false,
|
|
3559
|
+
accent_phrase_position: phrase_position,
|
|
3560
|
+
accent_phrase_length: phrase_length,
|
|
3561
|
+
accent_nucleus: accent,
|
|
3562
|
+
accent_high: isHigh(phrase_position, accent),
|
|
3563
|
+
accent_phrase_start: phrase_position === 1,
|
|
3564
|
+
accent_phrase_end: phrase_position === phrase_length,
|
|
3565
|
+
word_start: word_position === 1,
|
|
3566
|
+
word_end: word_position === morae.length,
|
|
3567
|
+
pos: current.pos || "*",
|
|
3568
|
+
pos_group1: current.pos_group1 || "*"
|
|
3569
|
+
});
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
return { reading: reading_parts.join(""), features: result };
|
|
3574
|
+
}
|
|
3575
|
+
function sparse_features(token) {
|
|
3576
|
+
if (token.pause || token.accent_phrase_position === void 0 || token.accent_phrase_length === void 0 || token.accent_nucleus === void 0) {
|
|
3577
|
+
return {};
|
|
3578
|
+
}
|
|
3579
|
+
const phrase_length = Math.max(1, token.accent_phrase_length);
|
|
3580
|
+
const phrase_position = token.accent_phrase_position;
|
|
3581
|
+
const nucleus = token.accent_nucleus;
|
|
3582
|
+
const result = {
|
|
3583
|
+
"accent_position": phrase_position / phrase_length,
|
|
3584
|
+
"accent_from_end": (phrase_length - phrase_position) / phrase_length,
|
|
3585
|
+
"accent_nucleus_position": nucleus / phrase_length,
|
|
3586
|
+
"accent_high": token.accent_high ? 1 : 0,
|
|
3587
|
+
"accent_phrase_start": token.accent_phrase_start ? 1 : 0,
|
|
3588
|
+
"accent_phrase_end": token.accent_phrase_end ? 1 : 0,
|
|
3589
|
+
"word_start": token.word_start ? 1 : 0,
|
|
3590
|
+
"word_end": token.word_end ? 1 : 0
|
|
3591
|
+
};
|
|
3592
|
+
result[`pos=${token.pos || "*"}`] = 1;
|
|
3593
|
+
result[`pos_group1=${token.pos_group1 || "*"}`] = 1;
|
|
3594
|
+
if (nucleus === 0) {
|
|
3595
|
+
result["accent_type=heiban"] = 1;
|
|
3596
|
+
} else if (phrase_position < nucleus) {
|
|
3597
|
+
result["accent_type=before"] = 1;
|
|
3598
|
+
} else if (phrase_position === nucleus) {
|
|
3599
|
+
result["accent_type=nucleus"] = 1;
|
|
3600
|
+
} else {
|
|
3601
|
+
result["accent_type=after"] = 1;
|
|
3602
|
+
}
|
|
3603
|
+
return result;
|
|
3604
|
+
}
|
|
3605
|
+
|
|
3606
|
+
// src/utautts/prosody_rules.ts
|
|
3607
|
+
var TRAILING = /[\s」』))〕】>>"'”’]+$/u;
|
|
3608
|
+
function isQuestion(text) {
|
|
3609
|
+
return /[??]$/u.test(text.replace(TRAILING, ""));
|
|
3610
|
+
}
|
|
3611
|
+
function smoothstep(t) {
|
|
3612
|
+
const x = Math.min(1, Math.max(0, t));
|
|
3613
|
+
return x * x * (3 - 2 * x);
|
|
3614
|
+
}
|
|
3615
|
+
function shapeProsody(prosody, features, options = {}) {
|
|
3616
|
+
const {
|
|
3617
|
+
question = false,
|
|
3618
|
+
questionRiseCents = 350,
|
|
3619
|
+
energyDbPerSemitone = 0.5,
|
|
3620
|
+
devoicedGain = 0.5,
|
|
3621
|
+
minGain = 0.35,
|
|
3622
|
+
maxGain = 1.8
|
|
3623
|
+
} = options;
|
|
3624
|
+
const { frame_ms: frameMs } = prosody.pitchCurve;
|
|
3625
|
+
const cents = prosody.pitchCurve.cents.slice();
|
|
3626
|
+
const count = Math.min(features.length, prosody.moraDurationsMs.length);
|
|
3627
|
+
const startFrame = [];
|
|
3628
|
+
const endFrame = [];
|
|
3629
|
+
let cursor = 0;
|
|
3630
|
+
for (let f = 0; f < count; f++) {
|
|
3631
|
+
startFrame.push(cursor / frameMs);
|
|
3632
|
+
cursor += prosody.moraDurationsMs[f];
|
|
3633
|
+
endFrame.push(cursor / frameMs);
|
|
3634
|
+
}
|
|
3635
|
+
const frameRange = (f) => [
|
|
3636
|
+
Math.max(0, Math.min(cents.length, Math.round(startFrame[f]))),
|
|
3637
|
+
Math.max(0, Math.min(cents.length, Math.round(endFrame[f])))
|
|
3638
|
+
];
|
|
3639
|
+
if (question && questionRiseCents > 0) {
|
|
3640
|
+
let last = -1;
|
|
3641
|
+
for (let f = count - 1; f >= 0; f--) {
|
|
3642
|
+
if (!features[f].pause) {
|
|
3643
|
+
last = f;
|
|
3644
|
+
break;
|
|
3645
|
+
}
|
|
3646
|
+
}
|
|
3647
|
+
if (last >= 0) {
|
|
3648
|
+
const [from, to] = frameRange(last);
|
|
3649
|
+
if (to > from) {
|
|
3650
|
+
const existing = cents[to - 1] - cents[from];
|
|
3651
|
+
const shortfall = questionRiseCents - existing;
|
|
3652
|
+
if (shortfall > 0) {
|
|
3653
|
+
for (let frame = from; frame < cents.length; frame++) {
|
|
3654
|
+
const t = (frame - from) / (to - from);
|
|
3655
|
+
cents[frame] += shortfall * smoothstep((t - 0.25) / 0.75);
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3660
|
+
}
|
|
3661
|
+
const moraGains = new Array(features.length).fill(1);
|
|
3662
|
+
for (let f = 0; f < count; f++) {
|
|
3663
|
+
if (features[f].pause) continue;
|
|
3664
|
+
let gainDb = 0;
|
|
3665
|
+
if (energyDbPerSemitone !== 0) {
|
|
3666
|
+
const [from, to] = frameRange(f);
|
|
3667
|
+
if (to > from) {
|
|
3668
|
+
let sum = 0;
|
|
3669
|
+
for (let frame = from; frame < to; frame++) sum += cents[frame];
|
|
3670
|
+
gainDb += sum / (to - from) / 100 * energyDbPerSemitone;
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
let gain = 10 ** (gainDb / 20);
|
|
3674
|
+
if (prosody.devoiced[f]) gain *= devoicedGain;
|
|
3675
|
+
moraGains[f] = Math.min(maxGain, Math.max(minGain, gain));
|
|
3676
|
+
}
|
|
3677
|
+
return {
|
|
3678
|
+
...prosody,
|
|
3679
|
+
pitchCurve: { frame_ms: frameMs, cents },
|
|
3680
|
+
moraGains
|
|
3681
|
+
};
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3150
3684
|
// src/utautts/UtauTTSAdapter.ts
|
|
3151
|
-
|
|
3152
|
-
|
|
3685
|
+
function readingFromFeatures(features) {
|
|
3686
|
+
return features.map((frame) => frame.pause ? "\u3001" : frame.mora).join("");
|
|
3687
|
+
}
|
|
3688
|
+
var FS = WORLDLINE_SAMPLE_RATE;
|
|
3689
|
+
var msToSamples2 = (ms) => Math.round(ms / 1e3 * FS);
|
|
3690
|
+
function f0At(timeline, tMs) {
|
|
3691
|
+
const curve = timeline.f0_curve;
|
|
3692
|
+
if (curve.length === 0) return timeline.reference_hz || 220;
|
|
3693
|
+
const position = Math.max(0, tMs) / timeline.frame_ms;
|
|
3694
|
+
const left = Math.floor(position);
|
|
3695
|
+
if (left >= curve.length - 1) return curve[curve.length - 1];
|
|
3696
|
+
const progress = position - left;
|
|
3697
|
+
return curve[left] * (1 - progress) + curve[left + 1] * progress;
|
|
3698
|
+
}
|
|
3699
|
+
function applyMoraGains(plan, gains) {
|
|
3700
|
+
const morae = plan.morae ?? [];
|
|
3701
|
+
const timings = plan.mora_timings ?? [];
|
|
3702
|
+
const byIndex = morae.length === gains.length;
|
|
3703
|
+
const moraAt = (unit) => {
|
|
3704
|
+
if (byIndex) return unit.position;
|
|
3705
|
+
let found = -1;
|
|
3706
|
+
for (let i = 0; i < timings.length && i < gains.length; i++) {
|
|
3707
|
+
if (timings[i].StartMS <= unit.note_start_ms + 1e-6) found = i;
|
|
3708
|
+
else break;
|
|
3709
|
+
}
|
|
3710
|
+
return found;
|
|
3711
|
+
};
|
|
3712
|
+
for (const unit of plan.timeline.units) {
|
|
3713
|
+
const index = moraAt(unit);
|
|
3714
|
+
const gain = gains[index];
|
|
3715
|
+
if (index < 0 || gain === void 0 || !Number.isFinite(gain)) continue;
|
|
3716
|
+
unit.volume *= gain;
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
function planChunks(units, firstChunkUnits, chunkUnits) {
|
|
3720
|
+
const ranges = [];
|
|
3721
|
+
if (units.length === 0) return ranges;
|
|
3722
|
+
let start = 0;
|
|
3723
|
+
let coveredEndMs = units[0].position_ms + units[0].length_ms;
|
|
3724
|
+
let headSeam = false;
|
|
3725
|
+
for (let i = 1; i <= units.length; i++) {
|
|
3726
|
+
const limit = ranges.length === 0 ? firstChunkUnits : chunkUnits;
|
|
3727
|
+
const atEnd = i === units.length;
|
|
3728
|
+
const clean = !atEnd && units[i].position_ms >= coveredEndMs - 1e-6;
|
|
3729
|
+
const full = !atEnd && i - start >= limit;
|
|
3730
|
+
if (atEnd || clean || full) {
|
|
3731
|
+
const tailSeam = !atEnd && !clean;
|
|
3732
|
+
ranges.push({ start, end: i, headSeam, tailSeam });
|
|
3733
|
+
start = i;
|
|
3734
|
+
headSeam = tailSeam;
|
|
3735
|
+
if (!atEnd) coveredEndMs = units[i].position_ms + units[i].length_ms;
|
|
3736
|
+
continue;
|
|
3737
|
+
}
|
|
3738
|
+
coveredEndMs = Math.max(
|
|
3739
|
+
coveredEndMs,
|
|
3740
|
+
units[i].position_ms + units[i].length_ms
|
|
3741
|
+
);
|
|
3742
|
+
}
|
|
3743
|
+
return ranges;
|
|
3744
|
+
}
|
|
3745
|
+
function seamTimeMs(next, crossfadeMs) {
|
|
3746
|
+
const earliest = next.position_ms + next.fade_in_ms + crossfadeMs / 2;
|
|
3747
|
+
const latest = next.position_ms + next.length_ms - crossfadeMs / 2;
|
|
3748
|
+
return Math.min(
|
|
3749
|
+
earliest,
|
|
3750
|
+
Math.max(next.position_ms + crossfadeMs / 2, latest)
|
|
3751
|
+
);
|
|
3752
|
+
}
|
|
3753
|
+
function applyFade(pcm, fromSample, samples, fadeIn) {
|
|
3754
|
+
const n = Math.max(1, Math.min(samples, pcm.length - fromSample));
|
|
3755
|
+
for (let k = 0; k < n; k++) {
|
|
3756
|
+
const t = (k + 0.5) / n;
|
|
3757
|
+
const gain = fadeIn ? Math.sin(Math.PI / 2 * t) : Math.cos(Math.PI / 2 * t);
|
|
3758
|
+
pcm[fromSample + k] *= gain;
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
var UtauTTSAdapter = class _UtauTTSAdapter {
|
|
3153
3762
|
constructor(worldline) {
|
|
3154
3763
|
this.worldline = worldline;
|
|
3155
3764
|
}
|
|
3765
|
+
worldline;
|
|
3766
|
+
static modelId = null;
|
|
3767
|
+
bankAliases = /* @__PURE__ */ new WeakMap();
|
|
3768
|
+
currentBank = null;
|
|
3769
|
+
pcmCache = /* @__PURE__ */ new Map();
|
|
3156
3770
|
/**
|
|
3157
|
-
*
|
|
3771
|
+
* Load the UtauTTS Go wasm. `wasm_exec.js` must already be on the page.
|
|
3772
|
+
* Pass `fetch` (e.g. koe's `fetchAsset`) to stream from the Cache API.
|
|
3158
3773
|
*/
|
|
3159
|
-
static async initializeWasm(wasmUrl = "utautts.wasm") {
|
|
3774
|
+
static async initializeWasm(wasmUrl = "utautts.wasm", options = {}) {
|
|
3775
|
+
if (typeof utautts_plan === "function") return;
|
|
3160
3776
|
if (typeof Go === "undefined") {
|
|
3161
|
-
throw new Error(
|
|
3777
|
+
throw new Error(
|
|
3778
|
+
"wasm_exec.js must be loaded before calling initializeWasm."
|
|
3779
|
+
);
|
|
3162
3780
|
}
|
|
3163
3781
|
const go = new Go();
|
|
3164
|
-
const
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3782
|
+
const responsePromise = (options.fetch ?? fetch)(wasmUrl);
|
|
3783
|
+
let instance;
|
|
3784
|
+
try {
|
|
3785
|
+
instance = (await WebAssembly.instantiateStreaming(responsePromise, go.importObject)).instance;
|
|
3786
|
+
} catch {
|
|
3787
|
+
const bytes = await (await (options.fetch ?? fetch)(wasmUrl)).arrayBuffer();
|
|
3788
|
+
instance = (await WebAssembly.instantiate(bytes, go.importObject)).instance;
|
|
3789
|
+
}
|
|
3790
|
+
void go.run(instance);
|
|
3791
|
+
for (let attempt = 0; attempt < 100 && typeof utautts_plan !== "function"; attempt++) {
|
|
3792
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
3793
|
+
}
|
|
3169
3794
|
if (typeof utautts_plan !== "function") {
|
|
3170
3795
|
throw new Error("utautts_plan failed to initialize in global scope.");
|
|
3171
3796
|
}
|
|
3172
3797
|
}
|
|
3798
|
+
static get ready() {
|
|
3799
|
+
return typeof utautts_plan === "function";
|
|
3800
|
+
}
|
|
3801
|
+
/** Parse and cache a prosody model (e.g. `frame-intonation-v8.json`) inside the wasm. */
|
|
3802
|
+
static setModel(modelJSON) {
|
|
3803
|
+
_UtauTTSAdapter.assertReady();
|
|
3804
|
+
const result = utautts_set_model(modelJSON);
|
|
3805
|
+
if (!result.success)
|
|
3806
|
+
throw new Error(`UtauTTS model error: ${result.error}`);
|
|
3807
|
+
_UtauTTSAdapter.modelId = result.id ?? null;
|
|
3808
|
+
return _UtauTTSAdapter.modelId;
|
|
3809
|
+
}
|
|
3810
|
+
static get currentModelId() {
|
|
3811
|
+
return _UtauTTSAdapter.modelId;
|
|
3812
|
+
}
|
|
3813
|
+
static assertReady() {
|
|
3814
|
+
if (typeof utautts_plan !== "function") {
|
|
3815
|
+
throw new Error(
|
|
3816
|
+
"UtauTTS wasm is not initialized; call UtauTTSAdapter.initializeWasm() first."
|
|
3817
|
+
);
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3173
3820
|
/**
|
|
3174
|
-
* koe
|
|
3821
|
+
* Register a koe voice bank with the planner: the manifest becomes a virtual
|
|
3822
|
+
* oto.ini (koe PCM is pre-trimmed, so offset is 0) plus each sample's
|
|
3823
|
+
* recorded pitch. Called automatically by {@link plan}; cheap when unchanged.
|
|
3175
3824
|
*/
|
|
3176
|
-
|
|
3825
|
+
setBank(bank) {
|
|
3826
|
+
_UtauTTSAdapter.assertReady();
|
|
3827
|
+
if (this.currentBank === bank && this.bankAliases.has(bank)) return;
|
|
3177
3828
|
const entries = {};
|
|
3178
|
-
const
|
|
3179
|
-
for (const [alias, phoneme] of Object.entries(manifest.phonemes)) {
|
|
3180
|
-
entries[alias] = [
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3829
|
+
const pitch = {};
|
|
3830
|
+
for (const [alias, phoneme] of Object.entries(bank.manifest.phonemes)) {
|
|
3831
|
+
entries[alias] = [
|
|
3832
|
+
{
|
|
3833
|
+
Filename: `koe:${alias}`,
|
|
3834
|
+
Alias: alias,
|
|
3835
|
+
Offset: 0,
|
|
3836
|
+
Fixed: phoneme.consonant / 48,
|
|
3837
|
+
Blank: 0,
|
|
3838
|
+
Preutterance: phoneme.pre / 48,
|
|
3839
|
+
Overlap: phoneme.overlap / 48,
|
|
3840
|
+
SourceGroup: "koe"
|
|
3841
|
+
}
|
|
3842
|
+
];
|
|
3843
|
+
if (phoneme.pitch > 0) pitch[alias] = phoneme.pitch;
|
|
3844
|
+
}
|
|
3845
|
+
const result = utautts_set_bank(
|
|
3846
|
+
JSON.stringify({
|
|
3847
|
+
name: "koe",
|
|
3848
|
+
oto_entries: entries,
|
|
3849
|
+
source_pitch_hz: pitch
|
|
3850
|
+
})
|
|
3851
|
+
);
|
|
3852
|
+
if (!result.success) throw new Error(`UtauTTS bank error: ${result.error}`);
|
|
3853
|
+
this.bankAliases.set(bank, result.aliases ?? 0);
|
|
3854
|
+
this.currentBank = bank;
|
|
3855
|
+
this.pcmCache.clear();
|
|
3193
3856
|
}
|
|
3194
3857
|
/**
|
|
3195
|
-
*
|
|
3858
|
+
* Plan an utterance: unit selection, timing, pitch contour and worldline
|
|
3859
|
+
* placement. `features` are the mora-level frames from `openjtalkAnalyze`
|
|
3860
|
+
* (pauses included); the kana reading is derived from them.
|
|
3196
3861
|
*/
|
|
3197
|
-
|
|
3198
|
-
|
|
3862
|
+
plan(bank, text, features, options = {}) {
|
|
3863
|
+
this.setBank(bank);
|
|
3199
3864
|
const request = {
|
|
3200
3865
|
text,
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3866
|
+
reading: readingFromFeatures(features),
|
|
3867
|
+
frames: features.map((frame) => sparse_features(frame)),
|
|
3868
|
+
tone: options.tone ?? "C4",
|
|
3869
|
+
mora_duration_ms: options.moraDurationMs ?? 0,
|
|
3870
|
+
pause_duration_ms: options.pauseDurationMs ?? 0,
|
|
3871
|
+
release_ms: options.releaseMs ?? 20,
|
|
3872
|
+
leading_preutterance_ms: options.leadingPreutteranceMs ?? 0,
|
|
3873
|
+
apply_pitch: options.applyPitch ?? true,
|
|
3874
|
+
intonation_strength: options.intonationStrength ?? 1,
|
|
3875
|
+
speech_timing: options.speechTiming ?? false,
|
|
3876
|
+
word_boundary_envelope: options.wordBoundaryEnvelope ?? false,
|
|
3877
|
+
mora_durations_ms: options.prosody?.moraDurationsMs,
|
|
3878
|
+
pitch_curve: options.prosody?.pitchCurve
|
|
3205
3879
|
};
|
|
3206
3880
|
const response = utautts_plan(JSON.stringify(request));
|
|
3207
|
-
if (!response.success) {
|
|
3208
|
-
throw new Error(`UtauTTS
|
|
3881
|
+
if (!response.success || !response.plan) {
|
|
3882
|
+
throw new Error(`UtauTTS error: ${response.error ?? "no plan"}`);
|
|
3209
3883
|
}
|
|
3210
3884
|
const plan = JSON.parse(response.plan);
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3885
|
+
const gains = options.prosody?.moraGains;
|
|
3886
|
+
if (gains) applyMoraGains(plan, gains);
|
|
3887
|
+
return plan;
|
|
3888
|
+
}
|
|
3889
|
+
getPcm(bank, alias) {
|
|
3890
|
+
let cached = this.pcmCache.get(alias);
|
|
3891
|
+
if (!cached) {
|
|
3892
|
+
cached = bank.getPcm(alias);
|
|
3893
|
+
this.pcmCache.set(alias, cached);
|
|
3894
|
+
}
|
|
3895
|
+
return cached;
|
|
3896
|
+
}
|
|
3897
|
+
/**
|
|
3898
|
+
* Render a plan chunk by chunk. Each chunk is independent audio positioned
|
|
3899
|
+
* at `startMs`; schedule them as they arrive (see the demo) or sum them.
|
|
3900
|
+
*
|
|
3901
|
+
* Chunk breaks fall on pauses when possible. Inside a phrase a break renders
|
|
3902
|
+
* one neighbouring unit of context on each side so the unit crossfade stays
|
|
3903
|
+
* WORLD's spectral one, then the two renders are joined with a short
|
|
3904
|
+
* equal-power crossfade in the following vowel.
|
|
3905
|
+
*/
|
|
3906
|
+
async *renderChunks(bank, plan, options = {}) {
|
|
3907
|
+
const {
|
|
3908
|
+
firstChunkUnits = 3,
|
|
3909
|
+
chunkUnits = 6,
|
|
3910
|
+
seamCrossfadeMs = 20,
|
|
3911
|
+
signal
|
|
3912
|
+
} = options;
|
|
3913
|
+
const timeline = plan.timeline;
|
|
3914
|
+
const units = timeline.units.filter((unit) => unit.length_ms > 0);
|
|
3915
|
+
const ranges = planChunks(
|
|
3916
|
+
units,
|
|
3917
|
+
Math.max(1, firstChunkUnits),
|
|
3918
|
+
Math.max(1, chunkUnits)
|
|
3919
|
+
);
|
|
3920
|
+
const crossfadeSamples = Math.max(2, msToSamples2(seamCrossfadeMs));
|
|
3921
|
+
for (let index = 0; index < ranges.length; index++) {
|
|
3922
|
+
if (signal?.aborted) return;
|
|
3923
|
+
const range = ranges[index];
|
|
3924
|
+
const renderStart = range.headSeam ? range.start - 1 : range.start;
|
|
3925
|
+
const renderEnd = range.tailSeam ? range.end + 1 : range.end;
|
|
3926
|
+
const rendered = units.slice(renderStart, renderEnd);
|
|
3927
|
+
const baseMs = Math.min(...rendered.map((unit) => unit.position_ms));
|
|
3928
|
+
const phraseUnits = [];
|
|
3929
|
+
for (const unit of rendered) {
|
|
3930
|
+
const pcm2 = await this.getPcm(bank, unit.alias);
|
|
3931
|
+
if (!pcm2 || pcm2.length < MIN_WORLDLINE_SAMPLES) {
|
|
3932
|
+
console.warn(
|
|
3933
|
+
`[utautts] no usable PCM for alias "${unit.alias}"; skipped`
|
|
3934
|
+
);
|
|
3935
|
+
continue;
|
|
3936
|
+
}
|
|
3937
|
+
phraseUnits.push({
|
|
3938
|
+
pcm: pcm2,
|
|
3939
|
+
posMs: unit.position_ms - baseMs,
|
|
3940
|
+
skipMs: unit.skip_ms,
|
|
3941
|
+
lengthMs: unit.length_ms,
|
|
3942
|
+
fadeInMs: unit.fade_in_ms,
|
|
3943
|
+
fadeOutMs: unit.fade_out_ms,
|
|
3944
|
+
consonantMs: unit.consonant_ms,
|
|
3945
|
+
requiredLengthMs: unit.required_length_ms,
|
|
3946
|
+
volume: unit.volume,
|
|
3947
|
+
tone: unit.tone
|
|
3948
|
+
});
|
|
3219
3949
|
}
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3950
|
+
if (signal?.aborted) return;
|
|
3951
|
+
if (phraseUnits.length === 0) continue;
|
|
3952
|
+
const audio = this.worldline.renderPhrase({
|
|
3953
|
+
units: phraseUnits,
|
|
3954
|
+
pitch: (tMs) => f0At(timeline, baseMs + tMs),
|
|
3955
|
+
gender: options.gender,
|
|
3956
|
+
tension: options.tension,
|
|
3957
|
+
breathiness: options.breathiness,
|
|
3958
|
+
voicing: options.voicing
|
|
3229
3959
|
});
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3960
|
+
if (!audio || audio.length === 0) continue;
|
|
3961
|
+
let fromSample = 0;
|
|
3962
|
+
let toSample = audio.length;
|
|
3963
|
+
let startMs = baseMs;
|
|
3964
|
+
if (range.headSeam) {
|
|
3965
|
+
const seamMs = seamTimeMs(units[range.start], seamCrossfadeMs);
|
|
3966
|
+
fromSample = Math.max(
|
|
3967
|
+
0,
|
|
3968
|
+
msToSamples2(seamMs - baseMs) - crossfadeSamples / 2
|
|
3969
|
+
);
|
|
3970
|
+
startMs = baseMs + fromSample / FS * 1e3;
|
|
3971
|
+
}
|
|
3972
|
+
if (range.tailSeam) {
|
|
3973
|
+
const seamMs = seamTimeMs(units[range.end], seamCrossfadeMs);
|
|
3974
|
+
toSample = Math.min(
|
|
3975
|
+
audio.length,
|
|
3976
|
+
msToSamples2(seamMs - baseMs) + crossfadeSamples / 2
|
|
3977
|
+
);
|
|
3978
|
+
}
|
|
3979
|
+
if (toSample <= fromSample) continue;
|
|
3980
|
+
const pcm = audio.slice(fromSample, toSample);
|
|
3981
|
+
if (range.headSeam) applyFade(pcm, 0, crossfadeSamples, true);
|
|
3982
|
+
if (range.tailSeam)
|
|
3983
|
+
applyFade(
|
|
3984
|
+
pcm,
|
|
3985
|
+
Math.max(0, pcm.length - crossfadeSamples),
|
|
3986
|
+
crossfadeSamples,
|
|
3987
|
+
false
|
|
3988
|
+
);
|
|
3989
|
+
yield { pcm, startMs, index, units: units.slice(range.start, range.end) };
|
|
3990
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
3252
3991
|
}
|
|
3253
|
-
|
|
3992
|
+
}
|
|
3993
|
+
/**
|
|
3994
|
+
* Plan + render an utterance into one buffer (Float32, 48 kHz) covering the
|
|
3995
|
+
* whole timeline. Use {@link plan} + {@link renderChunks} for streaming.
|
|
3996
|
+
*/
|
|
3997
|
+
async synthesizeText(bank, text, features, options = {}) {
|
|
3998
|
+
const plan = this.plan(bank, text, features, options);
|
|
3999
|
+
const total = msToSamples2(plan.timeline.duration_ms) + msToSamples2(200);
|
|
4000
|
+
const out = new Float32Array(total);
|
|
4001
|
+
let any = false;
|
|
4002
|
+
for await (const chunk of this.renderChunks(bank, plan, options)) {
|
|
4003
|
+
any = true;
|
|
4004
|
+
const offset = msToSamples2(chunk.startMs);
|
|
4005
|
+
const n = Math.min(chunk.pcm.length, out.length - offset);
|
|
4006
|
+
for (let k = 0; k < n; k++) out[offset + k] += chunk.pcm[k];
|
|
4007
|
+
}
|
|
4008
|
+
return any ? out : null;
|
|
3254
4009
|
}
|
|
3255
4010
|
};
|
|
3256
4011
|
export {
|
|
3257
4012
|
KoeEngine,
|
|
3258
4013
|
MIN_WORLDLINE_SAMPLES,
|
|
4014
|
+
NAIST_JDIC_FILES,
|
|
3259
4015
|
UtauTTSAdapter,
|
|
3260
4016
|
VoiceBank,
|
|
3261
4017
|
WORLDLINE_SAMPLE_RATE,
|
|
3262
4018
|
Worldline,
|
|
4019
|
+
alignHtsProsody,
|
|
3263
4020
|
analyze,
|
|
3264
4021
|
analyzeWav,
|
|
3265
4022
|
detectF0,
|
|
@@ -3269,15 +4026,22 @@ export {
|
|
|
3269
4026
|
estimateSequence,
|
|
3270
4027
|
estimateSolo,
|
|
3271
4028
|
estimateVowelJoin,
|
|
4029
|
+
fetchAsset,
|
|
4030
|
+
fetchAssetBytes,
|
|
4031
|
+
fetchAssetText,
|
|
3272
4032
|
formatOto,
|
|
3273
4033
|
frqAverageF0InRange,
|
|
3274
4034
|
frqFileName,
|
|
3275
4035
|
generateOto,
|
|
3276
4036
|
generateOtoForFile,
|
|
4037
|
+
initJpreprocessDictionary,
|
|
4038
|
+
isQuestion,
|
|
3277
4039
|
leadInFromEntry,
|
|
4040
|
+
loadNaistJdic,
|
|
3278
4041
|
locateMora,
|
|
3279
4042
|
normalizePcm,
|
|
3280
4043
|
noteNameToHz,
|
|
4044
|
+
analyze2 as openjtalkAnalyze,
|
|
3281
4045
|
otoRegion,
|
|
3282
4046
|
pack,
|
|
3283
4047
|
packKoe,
|
|
@@ -3289,8 +4053,11 @@ export {
|
|
|
3289
4053
|
pcmBase,
|
|
3290
4054
|
pitchFromAliasSuffix,
|
|
3291
4055
|
readWavPcm48k,
|
|
4056
|
+
readingFromFeatures,
|
|
3292
4057
|
resample,
|
|
3293
4058
|
samplesToMs,
|
|
4059
|
+
shapeProsody,
|
|
4060
|
+
sparse_features,
|
|
3294
4061
|
splitKana,
|
|
3295
4062
|
suffixFromFolderName,
|
|
3296
4063
|
summarise,
|