@onjmin/koe 1.0.10 → 1.0.11
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 +446 -440
- package/dist/index.d.ts +189 -6
- package/dist/index.js +447 -36
- package/dist/index.js.map +1 -1
- package/dist/utautts/hts/tohoku-f01-angry.htsvoice +0 -0
- package/dist/utautts/hts/tohoku-f01-happy.htsvoice +0 -0
- package/dist/utautts/hts/tohoku-f01-sad.htsvoice +0 -0
- package/dist/utautts/utautts.wasm +0 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2100,7 +2100,7 @@ function analyze(wav) {
|
|
|
2100
2100
|
const mono = resample(toMono(wav), RATE);
|
|
2101
2101
|
const x = mono.samples;
|
|
2102
2102
|
const n = Math.max(1, Math.floor((x.length - FFT_SIZE) / HOP) + 1);
|
|
2103
|
-
const
|
|
2103
|
+
const rmsDb2 = new Float32Array(n);
|
|
2104
2104
|
const highRatio = new Float32Array(n);
|
|
2105
2105
|
const highDb = new Float32Array(n);
|
|
2106
2106
|
const flux = new Float32Array(n);
|
|
@@ -2123,7 +2123,7 @@ function analyze(wav) {
|
|
|
2123
2123
|
re[i] = s * window[i];
|
|
2124
2124
|
im[i] = 0;
|
|
2125
2125
|
}
|
|
2126
|
-
|
|
2126
|
+
rmsDb2[t] = 10 * Math.log10(energy / FFT_SIZE + 1e-12);
|
|
2127
2127
|
fft(re, im);
|
|
2128
2128
|
let lowSum = 0;
|
|
2129
2129
|
let highSum = 0;
|
|
@@ -2141,7 +2141,7 @@ function analyze(wav) {
|
|
|
2141
2141
|
highDb[t] = 20 * Math.log10(highSum / BINS + 1e-9);
|
|
2142
2142
|
flux[t] = t === 0 ? 0 : fluxSum;
|
|
2143
2143
|
}
|
|
2144
|
-
const smoothDb = smooth(
|
|
2144
|
+
const smoothDb = smooth(rmsDb2, Math.round(30 / HOP_MS));
|
|
2145
2145
|
const smoothHighDb = smooth(highDb, Math.round(20 / HOP_MS));
|
|
2146
2146
|
const highFloorDb = quietestWindow(smoothHighDb);
|
|
2147
2147
|
const floorDb = percentile(smoothDb, 0.1);
|
|
@@ -2197,7 +2197,7 @@ function analyze(wav) {
|
|
|
2197
2197
|
return {
|
|
2198
2198
|
n,
|
|
2199
2199
|
durationMs: x.length / RATE * 1e3,
|
|
2200
|
-
rmsDb,
|
|
2200
|
+
rmsDb: rmsDb2,
|
|
2201
2201
|
smoothDb,
|
|
2202
2202
|
voiced,
|
|
2203
2203
|
highRatio,
|
|
@@ -3338,9 +3338,25 @@ function softLimit(value, knee, max2) {
|
|
|
3338
3338
|
return Math.sign(value) * Math.min(max2, limited);
|
|
3339
3339
|
}
|
|
3340
3340
|
var DEVOICED = /* @__PURE__ */ new Set(["A", "I", "U", "E", "O"]);
|
|
3341
|
+
var UNVOICED_ONSET = /* @__PURE__ */ new Set([
|
|
3342
|
+
"k",
|
|
3343
|
+
"ky",
|
|
3344
|
+
"s",
|
|
3345
|
+
"sh",
|
|
3346
|
+
"t",
|
|
3347
|
+
"ty",
|
|
3348
|
+
"ch",
|
|
3349
|
+
"ts",
|
|
3350
|
+
"h",
|
|
3351
|
+
"hy",
|
|
3352
|
+
"f",
|
|
3353
|
+
"p",
|
|
3354
|
+
"py"
|
|
3355
|
+
]);
|
|
3341
3356
|
function segmentPhonemes(phonemes) {
|
|
3342
3357
|
const segments = [];
|
|
3343
3358
|
let openStart = null;
|
|
3359
|
+
let openOnset = "";
|
|
3344
3360
|
for (let i = 0; i < phonemes.length; i++) {
|
|
3345
3361
|
const { phone, start_ms, duration_ms } = phonemes[i];
|
|
3346
3362
|
if (phone === "sil" || phone === "pau") {
|
|
@@ -3348,8 +3364,10 @@ function segmentPhonemes(phonemes) {
|
|
|
3348
3364
|
segments.push({
|
|
3349
3365
|
pause: false,
|
|
3350
3366
|
startMs: openStart,
|
|
3351
|
-
|
|
3352
|
-
|
|
3367
|
+
nucleusMs: openStart,
|
|
3368
|
+
endMs: start_ms,
|
|
3369
|
+
devoiced: false,
|
|
3370
|
+
onset: openOnset
|
|
3353
3371
|
});
|
|
3354
3372
|
openStart = null;
|
|
3355
3373
|
}
|
|
@@ -3357,18 +3375,25 @@ function segmentPhonemes(phonemes) {
|
|
|
3357
3375
|
segments.push({
|
|
3358
3376
|
pause: true,
|
|
3359
3377
|
startMs: start_ms,
|
|
3360
|
-
|
|
3361
|
-
|
|
3378
|
+
nucleusMs: start_ms,
|
|
3379
|
+
endMs: start_ms + duration_ms,
|
|
3380
|
+
devoiced: false,
|
|
3381
|
+
onset: ""
|
|
3362
3382
|
});
|
|
3363
3383
|
continue;
|
|
3364
3384
|
}
|
|
3365
|
-
if (openStart === null)
|
|
3385
|
+
if (openStart === null) {
|
|
3386
|
+
openStart = start_ms;
|
|
3387
|
+
openOnset = NUCLEUS.has(phone) ? "" : phone;
|
|
3388
|
+
}
|
|
3366
3389
|
if (NUCLEUS.has(phone)) {
|
|
3367
3390
|
segments.push({
|
|
3368
3391
|
pause: false,
|
|
3369
3392
|
startMs: openStart,
|
|
3370
|
-
|
|
3371
|
-
|
|
3393
|
+
nucleusMs: start_ms,
|
|
3394
|
+
endMs: start_ms + duration_ms,
|
|
3395
|
+
devoiced: DEVOICED.has(phone),
|
|
3396
|
+
onset: openOnset
|
|
3372
3397
|
});
|
|
3373
3398
|
openStart = null;
|
|
3374
3399
|
}
|
|
@@ -3378,47 +3403,79 @@ function segmentPhonemes(phonemes) {
|
|
|
3378
3403
|
segments.push({
|
|
3379
3404
|
pause: false,
|
|
3380
3405
|
startMs: openStart,
|
|
3381
|
-
|
|
3406
|
+
nucleusMs: openStart,
|
|
3407
|
+
endMs: last.start_ms + last.duration_ms,
|
|
3408
|
+
onset: openOnset,
|
|
3382
3409
|
devoiced: false
|
|
3383
3410
|
});
|
|
3384
3411
|
}
|
|
3385
3412
|
return segments;
|
|
3386
3413
|
}
|
|
3414
|
+
function anchorMs(segment) {
|
|
3415
|
+
return segment.pause ? segment.startMs : segment.nucleusMs;
|
|
3416
|
+
}
|
|
3387
3417
|
function alignHtsProsody(frames, features, options = {}) {
|
|
3388
3418
|
const {
|
|
3389
3419
|
intonationStrength = 1,
|
|
3390
3420
|
frameMs = 10,
|
|
3391
3421
|
fallbackPauseMs = 150,
|
|
3392
3422
|
kneeCents = 400,
|
|
3393
|
-
maxCents = 700
|
|
3423
|
+
maxCents = 700,
|
|
3424
|
+
unvoicedBridgeMs = 120
|
|
3394
3425
|
} = options;
|
|
3395
3426
|
const segments = segmentPhonemes(frames.phonemes);
|
|
3396
|
-
const
|
|
3397
|
-
const htsStart = new Array(features.length).fill(null);
|
|
3427
|
+
const matched = new Array(features.length).fill(null);
|
|
3398
3428
|
const devoiced = new Array(features.length).fill(false);
|
|
3429
|
+
const consonantMs = new Array(features.length).fill(0);
|
|
3430
|
+
const unvoicedOnset = new Array(features.length).fill(false);
|
|
3399
3431
|
let s = 0;
|
|
3400
3432
|
for (let f2 = 0; f2 < features.length; f2++) {
|
|
3401
3433
|
const feature = features[f2];
|
|
3402
3434
|
while (s < segments.length && segments[s].pause && !feature.pause) s++;
|
|
3403
3435
|
const segment = segments[s];
|
|
3404
3436
|
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
|
-
}
|
|
3437
|
+
if (segment?.pause) matched[f2] = s++;
|
|
3412
3438
|
continue;
|
|
3413
3439
|
}
|
|
3414
3440
|
if (!segment) return null;
|
|
3415
|
-
|
|
3416
|
-
htsStart[f2] = segment.startMs;
|
|
3441
|
+
matched[f2] = s++;
|
|
3417
3442
|
devoiced[f2] = segment.devoiced;
|
|
3418
|
-
|
|
3443
|
+
consonantMs[f2] = Math.max(0, segment.nucleusMs - segment.startMs);
|
|
3444
|
+
unvoicedOnset[f2] = UNVOICED_ONSET.has(segment.onset);
|
|
3419
3445
|
}
|
|
3420
3446
|
if (segments.slice(s).some((segment) => !segment.pause)) return null;
|
|
3421
|
-
const
|
|
3447
|
+
const durations = new Array(features.length).fill(0);
|
|
3448
|
+
const htsStart = new Array(features.length).fill(null);
|
|
3449
|
+
for (let f2 = 0; f2 < features.length; f2++) {
|
|
3450
|
+
const index = matched[f2];
|
|
3451
|
+
if (index === null) {
|
|
3452
|
+
durations[f2] = fallbackPauseMs;
|
|
3453
|
+
continue;
|
|
3454
|
+
}
|
|
3455
|
+
const here = anchorMs(segments[index]);
|
|
3456
|
+
let next = f2 + 1;
|
|
3457
|
+
while (next < features.length && matched[next] === null) next++;
|
|
3458
|
+
let end = segments[index].endMs;
|
|
3459
|
+
if (next < features.length) {
|
|
3460
|
+
const nextIndex = matched[next];
|
|
3461
|
+
end = anchorMs(segments[nextIndex]);
|
|
3462
|
+
for (let k = index + 1; k < nextIndex; k++) {
|
|
3463
|
+
if (segments[k].pause) end -= segments[k].endMs - segments[k].startMs;
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
durations[f2] = Math.max(0, end - here);
|
|
3467
|
+
htsStart[f2] = here;
|
|
3468
|
+
}
|
|
3469
|
+
const f0 = frames.f0_hz.slice();
|
|
3470
|
+
for (const phoneme of frames.phonemes) {
|
|
3471
|
+
if (!UNVOICED_ONSET.has(phoneme.phone) && phoneme.phone !== "cl") continue;
|
|
3472
|
+
const from = Math.max(0, Math.round(phoneme.start_ms / frames.frame_ms));
|
|
3473
|
+
const to = Math.min(
|
|
3474
|
+
f0.length,
|
|
3475
|
+
Math.round((phoneme.start_ms + phoneme.duration_ms) / frames.frame_ms)
|
|
3476
|
+
);
|
|
3477
|
+
for (let i = from; i < to; i++) f0[i] = 0;
|
|
3478
|
+
}
|
|
3422
3479
|
const logF0 = new Float64Array(f0.length);
|
|
3423
3480
|
const voiced = [];
|
|
3424
3481
|
for (let i = 0; i < f0.length; i++) if (f0[i] > 0) voiced.push(i);
|
|
@@ -3436,7 +3493,18 @@ function alignHtsProsody(frames, features, options = {}) {
|
|
|
3436
3493
|
if (left === void 0) logF0[i] = Math.log2(f0[right]);
|
|
3437
3494
|
else if (right === void 0) logF0[i] = Math.log2(f0[left]);
|
|
3438
3495
|
else {
|
|
3439
|
-
|
|
3496
|
+
if ((right - left) * frames.frame_ms > unvoicedBridgeMs) {
|
|
3497
|
+
logF0[i] = Math.log2(
|
|
3498
|
+
i - left <= (right - left) / 2 ? f0[left] : f0[right]
|
|
3499
|
+
);
|
|
3500
|
+
continue;
|
|
3501
|
+
}
|
|
3502
|
+
const rampStart = Math.max(left, right - Math.ceil(20 / frames.frame_ms));
|
|
3503
|
+
if (i <= rampStart) {
|
|
3504
|
+
logF0[i] = Math.log2(f0[left]);
|
|
3505
|
+
continue;
|
|
3506
|
+
}
|
|
3507
|
+
const t = (i - rampStart) / (right - rampStart);
|
|
3440
3508
|
logF0[i] = Math.log2(f0[left]) * (1 - t) + Math.log2(f0[right]) * t;
|
|
3441
3509
|
}
|
|
3442
3510
|
}
|
|
@@ -3479,7 +3547,9 @@ function alignHtsProsody(frames, features, options = {}) {
|
|
|
3479
3547
|
pitchCurve: { frame_ms: frameMs, cents },
|
|
3480
3548
|
medianHz,
|
|
3481
3549
|
durationMs,
|
|
3482
|
-
devoiced
|
|
3550
|
+
devoiced,
|
|
3551
|
+
consonantMs,
|
|
3552
|
+
unvoicedOnset
|
|
3483
3553
|
};
|
|
3484
3554
|
}
|
|
3485
3555
|
|
|
@@ -3517,6 +3587,25 @@ function isHigh(position, accent) {
|
|
|
3517
3587
|
if (accent > 1) return 2 <= position && position <= accent;
|
|
3518
3588
|
return position >= 2;
|
|
3519
3589
|
}
|
|
3590
|
+
var SENTENCE_END = /[。.!?!?.]/u;
|
|
3591
|
+
var CLAUSE = /[、,,]/u;
|
|
3592
|
+
function pauseKind(punctuation) {
|
|
3593
|
+
if (punctuation === void 0) return "clause";
|
|
3594
|
+
if (SENTENCE_END.test(punctuation)) return "sentence";
|
|
3595
|
+
if (CLAUSE.test(punctuation)) return "clause";
|
|
3596
|
+
return "space";
|
|
3597
|
+
}
|
|
3598
|
+
function pauseRank(punctuation) {
|
|
3599
|
+
if (punctuation === void 0) return 0;
|
|
3600
|
+
switch (pauseKind(punctuation)) {
|
|
3601
|
+
case "sentence":
|
|
3602
|
+
return 3;
|
|
3603
|
+
case "clause":
|
|
3604
|
+
return 2;
|
|
3605
|
+
default:
|
|
3606
|
+
return 1;
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3520
3609
|
function analyze2(nodes) {
|
|
3521
3610
|
const reading_parts = [];
|
|
3522
3611
|
const result = [];
|
|
@@ -3526,8 +3615,11 @@ function analyze2(nodes) {
|
|
|
3526
3615
|
const pronunciation = (node.pron || "").replace(/'/g, "").replace(/’/g, "");
|
|
3527
3616
|
if (node.mora_size === 0 || PUNCTUATION.has(node.string)) {
|
|
3528
3617
|
reading_parts.push(node.string || "\u3001");
|
|
3529
|
-
|
|
3530
|
-
|
|
3618
|
+
const last = result[result.length - 1];
|
|
3619
|
+
if (result.length > 0 && !last.pause) {
|
|
3620
|
+
result.push({ mora: "", pause: true, punctuation: node.string || "\u3001" });
|
|
3621
|
+
} else if (last?.pause && pauseRank(node.string) > pauseRank(last.punctuation)) {
|
|
3622
|
+
last.punctuation = node.string;
|
|
3531
3623
|
}
|
|
3532
3624
|
index++;
|
|
3533
3625
|
continue;
|
|
@@ -3612,26 +3704,113 @@ function smoothstep(t) {
|
|
|
3612
3704
|
const x = Math.min(1, Math.max(0, t));
|
|
3613
3705
|
return x * x * (3 - 2 * x);
|
|
3614
3706
|
}
|
|
3707
|
+
var DEFAULT_PAUSE_MS = {
|
|
3708
|
+
sentence: 650,
|
|
3709
|
+
clause: 380,
|
|
3710
|
+
space: 500
|
|
3711
|
+
};
|
|
3712
|
+
function shapeDurations(durations, features, options = {}) {
|
|
3713
|
+
const {
|
|
3714
|
+
pauseMs = DEFAULT_PAUSE_MS,
|
|
3715
|
+
durationContrast = 1.3,
|
|
3716
|
+
minMoraMs = 60,
|
|
3717
|
+
minShrink = 0.85
|
|
3718
|
+
} = options;
|
|
3719
|
+
const count = Math.min(features.length, durations.length);
|
|
3720
|
+
let sum = 0;
|
|
3721
|
+
let spoken = 0;
|
|
3722
|
+
for (let f = 0; f < count; f++) {
|
|
3723
|
+
if (features[f].pause) continue;
|
|
3724
|
+
sum += durations[f];
|
|
3725
|
+
spoken++;
|
|
3726
|
+
}
|
|
3727
|
+
const mean = spoken > 0 ? sum / spoken : 0;
|
|
3728
|
+
const result = durations.slice();
|
|
3729
|
+
let changed = false;
|
|
3730
|
+
for (let f = 0; f < count; f++) {
|
|
3731
|
+
const feature = features[f];
|
|
3732
|
+
let next = durations[f];
|
|
3733
|
+
if (feature.pause) {
|
|
3734
|
+
if (pauseMs && f < count - 1) {
|
|
3735
|
+
const kind = pauseKind(feature.punctuation);
|
|
3736
|
+
next = Math.max(next, pauseMs[kind] ?? DEFAULT_PAUSE_MS[kind]);
|
|
3737
|
+
}
|
|
3738
|
+
} else if (durationContrast !== 1 && next > 0) {
|
|
3739
|
+
next = Math.max(
|
|
3740
|
+
Math.min(next, minMoraMs),
|
|
3741
|
+
next * minShrink,
|
|
3742
|
+
mean + (next - mean) * durationContrast
|
|
3743
|
+
);
|
|
3744
|
+
}
|
|
3745
|
+
if (next !== durations[f]) {
|
|
3746
|
+
result[f] = next;
|
|
3747
|
+
changed = true;
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
return changed ? result : durations;
|
|
3751
|
+
}
|
|
3752
|
+
function warpPitchCurve(cents, frameMs, from, to) {
|
|
3753
|
+
const count = Math.min(from.length, to.length);
|
|
3754
|
+
const fromStart = [];
|
|
3755
|
+
const toStart = [];
|
|
3756
|
+
let a = 0;
|
|
3757
|
+
let b = 0;
|
|
3758
|
+
for (let f2 = 0; f2 < count; f2++) {
|
|
3759
|
+
fromStart.push(a);
|
|
3760
|
+
toStart.push(b);
|
|
3761
|
+
a += from[f2];
|
|
3762
|
+
b += to[f2];
|
|
3763
|
+
}
|
|
3764
|
+
const frames = Math.max(2, Math.ceil(b / frameMs) + 2);
|
|
3765
|
+
const result = new Array(frames);
|
|
3766
|
+
const last = cents.length - 1;
|
|
3767
|
+
if (last < 0) return result.fill(0);
|
|
3768
|
+
let f = 0;
|
|
3769
|
+
for (let frame = 0; frame < frames; frame++) {
|
|
3770
|
+
const tMs = frame * frameMs;
|
|
3771
|
+
while (f + 1 < count && tMs >= toStart[f + 1]) f++;
|
|
3772
|
+
const progress = to[f] > 0 ? (tMs - toStart[f]) / to[f] : 0;
|
|
3773
|
+
const source = (fromStart[f] + progress * from[f]) / frameMs;
|
|
3774
|
+
const left = Math.min(last, Math.max(0, Math.floor(source)));
|
|
3775
|
+
const right = Math.min(last, left + 1);
|
|
3776
|
+
const t = Math.min(1, Math.max(0, source - Math.floor(source)));
|
|
3777
|
+
result[frame] = cents[left] * (1 - t) + cents[right] * t;
|
|
3778
|
+
}
|
|
3779
|
+
return result;
|
|
3780
|
+
}
|
|
3615
3781
|
function shapeProsody(prosody, features, options = {}) {
|
|
3616
3782
|
const {
|
|
3617
3783
|
question = false,
|
|
3618
3784
|
questionRiseCents = 350,
|
|
3619
|
-
energyDbPerSemitone = 0
|
|
3620
|
-
devoicedGain =
|
|
3785
|
+
energyDbPerSemitone = 0,
|
|
3786
|
+
devoicedGain = 1,
|
|
3621
3787
|
minGain = 0.35,
|
|
3622
|
-
maxGain = 1.8
|
|
3788
|
+
maxGain = 1.8,
|
|
3789
|
+
pitchShiftCents = 0
|
|
3623
3790
|
} = options;
|
|
3624
3791
|
const { frame_ms: frameMs } = prosody.pitchCurve;
|
|
3625
|
-
const
|
|
3626
|
-
const
|
|
3792
|
+
const durations = shapeDurations(prosody.moraDurationsMs, features, options);
|
|
3793
|
+
const cents = durations === prosody.moraDurationsMs ? prosody.pitchCurve.cents.slice() : warpPitchCurve(
|
|
3794
|
+
prosody.pitchCurve.cents,
|
|
3795
|
+
frameMs,
|
|
3796
|
+
prosody.moraDurationsMs,
|
|
3797
|
+
durations
|
|
3798
|
+
);
|
|
3799
|
+
const count = Math.min(features.length, durations.length);
|
|
3627
3800
|
const startFrame = [];
|
|
3628
3801
|
const endFrame = [];
|
|
3629
3802
|
let cursor = 0;
|
|
3630
3803
|
for (let f = 0; f < count; f++) {
|
|
3631
3804
|
startFrame.push(cursor / frameMs);
|
|
3632
|
-
cursor +=
|
|
3805
|
+
cursor += durations[f];
|
|
3633
3806
|
endFrame.push(cursor / frameMs);
|
|
3634
3807
|
}
|
|
3808
|
+
for (let f = count; f < durations.length; f++) cursor += durations[f];
|
|
3809
|
+
const durationMs = cursor;
|
|
3810
|
+
if (pitchShiftCents !== 0) {
|
|
3811
|
+
for (let frame = 0; frame < cents.length; frame++)
|
|
3812
|
+
cents[frame] += pitchShiftCents;
|
|
3813
|
+
}
|
|
3635
3814
|
const frameRange = (f) => [
|
|
3636
3815
|
Math.max(0, Math.min(cents.length, Math.round(startFrame[f]))),
|
|
3637
3816
|
Math.max(0, Math.min(cents.length, Math.round(endFrame[f])))
|
|
@@ -3676,17 +3855,100 @@ function shapeProsody(prosody, features, options = {}) {
|
|
|
3676
3855
|
}
|
|
3677
3856
|
return {
|
|
3678
3857
|
...prosody,
|
|
3858
|
+
moraDurationsMs: durations,
|
|
3859
|
+
durationMs,
|
|
3679
3860
|
pitchCurve: { frame_ms: frameMs, cents },
|
|
3680
3861
|
moraGains
|
|
3681
3862
|
};
|
|
3682
3863
|
}
|
|
3683
3864
|
|
|
3865
|
+
// src/utautts/style.ts
|
|
3866
|
+
var SPEAKING_STYLES = {
|
|
3867
|
+
neutral: {
|
|
3868
|
+
speed: 1,
|
|
3869
|
+
intonation: 1,
|
|
3870
|
+
pitchShiftCents: 0,
|
|
3871
|
+
pauseScale: 1,
|
|
3872
|
+
durationContrast: 1.3,
|
|
3873
|
+
energyDbPerSemitone: 0.8
|
|
3874
|
+
},
|
|
3875
|
+
calm: {
|
|
3876
|
+
speed: 0.9,
|
|
3877
|
+
intonation: 0.9,
|
|
3878
|
+
pitchShiftCents: -100,
|
|
3879
|
+
pauseScale: 1.4,
|
|
3880
|
+
durationContrast: 1.2,
|
|
3881
|
+
energyDbPerSemitone: 0.6
|
|
3882
|
+
},
|
|
3883
|
+
lively: {
|
|
3884
|
+
speed: 1.08,
|
|
3885
|
+
intonation: 1.3,
|
|
3886
|
+
pitchShiftCents: 100,
|
|
3887
|
+
pauseScale: 0.85,
|
|
3888
|
+
durationContrast: 1.4,
|
|
3889
|
+
energyDbPerSemitone: 1
|
|
3890
|
+
}
|
|
3891
|
+
};
|
|
3892
|
+
var BASE_PAUSE_MS = { sentence: 650, clause: 380, space: 500 };
|
|
3893
|
+
function resolveSpeakingStyle(input = "neutral") {
|
|
3894
|
+
if (typeof input === "string") return { ...SPEAKING_STYLES[input] };
|
|
3895
|
+
const { preset = "neutral", ...overrides } = input;
|
|
3896
|
+
return { ...SPEAKING_STYLES[preset], ...overrides };
|
|
3897
|
+
}
|
|
3898
|
+
function styleAlignOptions(style) {
|
|
3899
|
+
return { intonationStrength: style.intonation };
|
|
3900
|
+
}
|
|
3901
|
+
function styleShapeOptions(style) {
|
|
3902
|
+
return {
|
|
3903
|
+
pauseMs: {
|
|
3904
|
+
sentence: BASE_PAUSE_MS.sentence * style.pauseScale,
|
|
3905
|
+
clause: BASE_PAUSE_MS.clause * style.pauseScale,
|
|
3906
|
+
space: BASE_PAUSE_MS.space * style.pauseScale
|
|
3907
|
+
},
|
|
3908
|
+
durationContrast: style.durationContrast,
|
|
3909
|
+
pitchShiftCents: style.pitchShiftCents
|
|
3910
|
+
};
|
|
3911
|
+
}
|
|
3912
|
+
function styleRenderOptions(style) {
|
|
3913
|
+
return { energyDbPerSemitone: style.energyDbPerSemitone };
|
|
3914
|
+
}
|
|
3915
|
+
|
|
3684
3916
|
// src/utautts/UtauTTSAdapter.ts
|
|
3685
3917
|
function readingFromFeatures(features) {
|
|
3686
3918
|
return features.map((frame) => frame.pause ? "\u3001" : frame.mora).join("");
|
|
3687
3919
|
}
|
|
3688
3920
|
var FS = WORLDLINE_SAMPLE_RATE;
|
|
3689
3921
|
var msToSamples2 = (ms) => Math.round(ms / 1e3 * FS);
|
|
3922
|
+
function rmsDb(pcm, from, to) {
|
|
3923
|
+
const a = Math.max(0, from);
|
|
3924
|
+
const b = Math.min(pcm.length, to);
|
|
3925
|
+
if (b <= a) return Number.NaN;
|
|
3926
|
+
let sum = 0;
|
|
3927
|
+
for (let k = a; k < b; k++) sum += pcm[k] * pcm[k];
|
|
3928
|
+
return 10 * Math.log10(sum / (b - a) + 1e-12);
|
|
3929
|
+
}
|
|
3930
|
+
function applyGainFrames(pcm, framesDb, frameSamples) {
|
|
3931
|
+
const last = framesDb.length - 1;
|
|
3932
|
+
if (last < 0) return;
|
|
3933
|
+
for (let k = 0; k < pcm.length; k++) {
|
|
3934
|
+
const position = k / frameSamples - 0.5;
|
|
3935
|
+
const left = Math.min(last, Math.max(0, Math.floor(position)));
|
|
3936
|
+
const right = Math.min(last, left + 1);
|
|
3937
|
+
const t = Math.min(1, Math.max(0, position - left));
|
|
3938
|
+
pcm[k] *= 10 ** ((framesDb[left] * (1 - t) + framesDb[right] * t) / 20);
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
function softLimit2(pcm, threshold) {
|
|
3942
|
+
const range = 1 - threshold;
|
|
3943
|
+
if (range <= 0) return;
|
|
3944
|
+
for (let k = 0; k < pcm.length; k++) {
|
|
3945
|
+
const x = pcm[k];
|
|
3946
|
+
const a = Math.abs(x);
|
|
3947
|
+
if (a <= threshold) continue;
|
|
3948
|
+
const y = threshold + range * Math.tanh((a - threshold) / range);
|
|
3949
|
+
pcm[k] = x < 0 ? -y : y;
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3690
3952
|
function f0At(timeline, tMs) {
|
|
3691
3953
|
const curve = timeline.f0_curve;
|
|
3692
3954
|
if (curve.length === 0) return timeline.reference_hz || 220;
|
|
@@ -3716,6 +3978,30 @@ function applyMoraGains(plan, gains) {
|
|
|
3716
3978
|
unit.volume *= gain;
|
|
3717
3979
|
}
|
|
3718
3980
|
}
|
|
3981
|
+
function applyConsonantGaps(plan, prosody) {
|
|
3982
|
+
const { consonantMs, unvoicedOnset } = prosody;
|
|
3983
|
+
if (!consonantMs || !unvoicedOnset) return;
|
|
3984
|
+
if (consonantMs.length !== (plan.morae?.length ?? -1)) return;
|
|
3985
|
+
const leading = plan.timeline.leading_ms;
|
|
3986
|
+
const units = plan.timeline.units;
|
|
3987
|
+
for (let i = 0; i + 1 < units.length; i++) {
|
|
3988
|
+
const unit = units[i];
|
|
3989
|
+
const next = units[i + 1];
|
|
3990
|
+
if (next.position !== unit.position + 1) continue;
|
|
3991
|
+
if (!unvoicedOnset[next.position]) continue;
|
|
3992
|
+
const consonant = consonantMs[next.position];
|
|
3993
|
+
if (!(consonant > 0)) continue;
|
|
3994
|
+
const gapStart = next.note_start_ms + leading - consonant;
|
|
3995
|
+
const currentEnd = unit.position_ms + unit.length_ms;
|
|
3996
|
+
if (gapStart >= currentEnd) continue;
|
|
3997
|
+
const minimumLength = Math.max(
|
|
3998
|
+
40,
|
|
3999
|
+
unit.fade_in_ms + unit.fade_out_ms + 10,
|
|
4000
|
+
unit.note_start_ms + leading - unit.position_ms + 30
|
|
4001
|
+
);
|
|
4002
|
+
unit.length_ms = Math.max(minimumLength, gapStart - unit.position_ms);
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
3719
4005
|
function planChunks(units, firstChunkUnits, chunkUnits) {
|
|
3720
4006
|
const ranges = [];
|
|
3721
4007
|
if (units.length === 0) return ranges;
|
|
@@ -3758,6 +4044,93 @@ function applyFade(pcm, fromSample, samples, fadeIn) {
|
|
|
3758
4044
|
pcm[fromSample + k] *= gain;
|
|
3759
4045
|
}
|
|
3760
4046
|
}
|
|
4047
|
+
function median(values) {
|
|
4048
|
+
const sorted = values.slice().sort((a, b) => a - b);
|
|
4049
|
+
return sorted[Math.floor(sorted.length / 2)];
|
|
4050
|
+
}
|
|
4051
|
+
function shapeLoudness(audio, units, timeline, baseMs, options) {
|
|
4052
|
+
const frameMs = timeline.frame_ms > 0 ? timeline.frame_ms : 10;
|
|
4053
|
+
const frameSamples = Math.max(1, msToSamples2(frameMs));
|
|
4054
|
+
const frames = Math.ceil(audio.length / frameSamples) + 1;
|
|
4055
|
+
const curve = new Float64Array(frames);
|
|
4056
|
+
if (options.equalize) {
|
|
4057
|
+
const anchors = [];
|
|
4058
|
+
const measured = [];
|
|
4059
|
+
for (const unit of units) {
|
|
4060
|
+
if (!(unit.volume > 0) || unit.duration_ms < 30) continue;
|
|
4061
|
+
const start = unit.note_start_ms + timeline.leading_ms - baseMs;
|
|
4062
|
+
const soundEnd = unit.position_ms - baseMs + unit.length_ms - unit.fade_out_ms;
|
|
4063
|
+
const from = msToSamples2(start + unit.duration_ms * 0.35);
|
|
4064
|
+
const to = msToSamples2(
|
|
4065
|
+
Math.min(start + unit.duration_ms * 0.9, soundEnd)
|
|
4066
|
+
);
|
|
4067
|
+
if (to - from < msToSamples2(20)) continue;
|
|
4068
|
+
const db = rmsDb(audio, from, to);
|
|
4069
|
+
if (!Number.isFinite(db) || db < -70) continue;
|
|
4070
|
+
measured.push({
|
|
4071
|
+
frame: (start + unit.duration_ms * 0.6) / frameMs,
|
|
4072
|
+
intrinsicDb: db - 20 * Math.log10(unit.volume / 100)
|
|
4073
|
+
});
|
|
4074
|
+
}
|
|
4075
|
+
if (measured.length > 0) {
|
|
4076
|
+
const reference = options.targetDb;
|
|
4077
|
+
for (const m of measured) {
|
|
4078
|
+
anchors.push({
|
|
4079
|
+
frame: m.frame,
|
|
4080
|
+
db: Math.max(
|
|
4081
|
+
-options.maxDb,
|
|
4082
|
+
Math.min(options.maxDb, reference - m.intrinsicDb)
|
|
4083
|
+
)
|
|
4084
|
+
});
|
|
4085
|
+
}
|
|
4086
|
+
let a = 0;
|
|
4087
|
+
for (let frame = 0; frame < frames; frame++) {
|
|
4088
|
+
while (a + 1 < anchors.length && frame >= anchors[a + 1].frame) a++;
|
|
4089
|
+
const left = anchors[a];
|
|
4090
|
+
const right = anchors[Math.min(anchors.length - 1, a + 1)];
|
|
4091
|
+
if (frame <= left.frame || right === left) curve[frame] = left.db;
|
|
4092
|
+
else if (frame >= right.frame) curve[frame] = right.db;
|
|
4093
|
+
else {
|
|
4094
|
+
const t = (frame - left.frame) / (right.frame - left.frame);
|
|
4095
|
+
curve[frame] = left.db * (1 - t) + right.db * t;
|
|
4096
|
+
}
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
if (options.energyDbPerSemitone !== 0 && options.referenceHz > 0) {
|
|
4101
|
+
for (let frame = 0; frame < frames; frame++) {
|
|
4102
|
+
const f0 = f0At(timeline, baseMs + frame * frameMs);
|
|
4103
|
+
if (!(f0 > 0)) continue;
|
|
4104
|
+
const semitones = 12 * Math.log2(f0 / options.referenceHz);
|
|
4105
|
+
curve[frame] += Math.max(
|
|
4106
|
+
-options.energyMaxDb,
|
|
4107
|
+
Math.min(options.energyMaxDb, semitones * options.energyDbPerSemitone)
|
|
4108
|
+
);
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
4111
|
+
if (options.devoicedDb < 0) {
|
|
4112
|
+
const rampFrames = Math.max(1, Math.round(20 / frameMs));
|
|
4113
|
+
for (let i = 0; i < units.length; i++) {
|
|
4114
|
+
const unit = units[i];
|
|
4115
|
+
if (!unit.devoiced) continue;
|
|
4116
|
+
const start = unit.note_start_ms + timeline.leading_ms - baseMs;
|
|
4117
|
+
let endMs = start + unit.duration_ms;
|
|
4118
|
+
const next = units[i + 1];
|
|
4119
|
+
if (next) endMs = Math.min(endMs, next.position_ms - baseMs);
|
|
4120
|
+
const from = start / frameMs;
|
|
4121
|
+
const to = endMs / frameMs;
|
|
4122
|
+
for (let frame = Math.max(0, Math.floor(from)); frame < frames && frame < to + rampFrames; frame++) {
|
|
4123
|
+
const fadeIn = Math.min(1, Math.max(0, (frame - from) / rampFrames));
|
|
4124
|
+
const fadeOut = Math.min(
|
|
4125
|
+
1,
|
|
4126
|
+
Math.max(0, (to + rampFrames - frame) / rampFrames)
|
|
4127
|
+
);
|
|
4128
|
+
curve[frame] += options.devoicedDb * Math.min(fadeIn, fadeOut);
|
|
4129
|
+
}
|
|
4130
|
+
}
|
|
4131
|
+
}
|
|
4132
|
+
applyGainFrames(audio, curve, frameSamples);
|
|
4133
|
+
}
|
|
3761
4134
|
var UtauTTSAdapter = class _UtauTTSAdapter {
|
|
3762
4135
|
constructor(worldline) {
|
|
3763
4136
|
this.worldline = worldline;
|
|
@@ -3884,6 +4257,13 @@ var UtauTTSAdapter = class _UtauTTSAdapter {
|
|
|
3884
4257
|
const plan = JSON.parse(response.plan);
|
|
3885
4258
|
const gains = options.prosody?.moraGains;
|
|
3886
4259
|
if (gains) applyMoraGains(plan, gains);
|
|
4260
|
+
const devoiced = options.prosody?.devoiced;
|
|
4261
|
+
if (devoiced && devoiced.length === (plan.morae?.length ?? -1)) {
|
|
4262
|
+
for (const unit of plan.timeline.units) {
|
|
4263
|
+
if (devoiced[unit.position]) unit.devoiced = true;
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
if (options.prosody) applyConsonantGaps(plan, options.prosody);
|
|
3887
4267
|
return plan;
|
|
3888
4268
|
}
|
|
3889
4269
|
getPcm(bank, alias) {
|
|
@@ -3908,10 +4288,19 @@ var UtauTTSAdapter = class _UtauTTSAdapter {
|
|
|
3908
4288
|
firstChunkUnits = 3,
|
|
3909
4289
|
chunkUnits = 6,
|
|
3910
4290
|
seamCrossfadeMs = 20,
|
|
4291
|
+
normalizeUnitLoudness = true,
|
|
4292
|
+
unitLoudnessDb = -16,
|
|
4293
|
+
unitLoudnessMaxDb = 6,
|
|
4294
|
+
energyDbPerSemitone = 0.8,
|
|
4295
|
+
energyMaxDb = 6,
|
|
4296
|
+
devoicedDb = -9,
|
|
4297
|
+
outputLimit = 0.8,
|
|
3911
4298
|
signal
|
|
3912
4299
|
} = options;
|
|
3913
4300
|
const timeline = plan.timeline;
|
|
3914
4301
|
const units = timeline.units.filter((unit) => unit.length_ms > 0);
|
|
4302
|
+
const voicedF0 = timeline.f0_curve.filter((hz) => hz > 0);
|
|
4303
|
+
const referenceHz = voicedF0.length > 0 ? median(voicedF0) : 0;
|
|
3915
4304
|
const ranges = planChunks(
|
|
3916
4305
|
units,
|
|
3917
4306
|
Math.max(1, firstChunkUnits),
|
|
@@ -3958,6 +4347,17 @@ var UtauTTSAdapter = class _UtauTTSAdapter {
|
|
|
3958
4347
|
voicing: options.voicing
|
|
3959
4348
|
});
|
|
3960
4349
|
if (!audio || audio.length === 0) continue;
|
|
4350
|
+
if (normalizeUnitLoudness || energyDbPerSemitone !== 0 || devoicedDb < 0) {
|
|
4351
|
+
shapeLoudness(audio, rendered, timeline, baseMs, {
|
|
4352
|
+
equalize: normalizeUnitLoudness,
|
|
4353
|
+
targetDb: unitLoudnessDb,
|
|
4354
|
+
maxDb: unitLoudnessMaxDb,
|
|
4355
|
+
energyDbPerSemitone,
|
|
4356
|
+
energyMaxDb,
|
|
4357
|
+
devoicedDb,
|
|
4358
|
+
referenceHz
|
|
4359
|
+
});
|
|
4360
|
+
}
|
|
3961
4361
|
let fromSample = 0;
|
|
3962
4362
|
let toSample = audio.length;
|
|
3963
4363
|
let startMs = baseMs;
|
|
@@ -3978,6 +4378,7 @@ var UtauTTSAdapter = class _UtauTTSAdapter {
|
|
|
3978
4378
|
}
|
|
3979
4379
|
if (toSample <= fromSample) continue;
|
|
3980
4380
|
const pcm = audio.slice(fromSample, toSample);
|
|
4381
|
+
if (outputLimit > 0) softLimit2(pcm, outputLimit);
|
|
3981
4382
|
if (range.headSeam) applyFade(pcm, 0, crossfadeSamples, true);
|
|
3982
4383
|
if (range.tailSeam)
|
|
3983
4384
|
applyFade(
|
|
@@ -4005,6 +4406,8 @@ var UtauTTSAdapter = class _UtauTTSAdapter {
|
|
|
4005
4406
|
const n = Math.min(chunk.pcm.length, out.length - offset);
|
|
4006
4407
|
for (let k = 0; k < n; k++) out[offset + k] += chunk.pcm[k];
|
|
4007
4408
|
}
|
|
4409
|
+
const limit = options.outputLimit ?? 0.8;
|
|
4410
|
+
if (any && limit > 0) softLimit2(out, limit);
|
|
4008
4411
|
return any ? out : null;
|
|
4009
4412
|
}
|
|
4010
4413
|
};
|
|
@@ -4012,6 +4415,7 @@ export {
|
|
|
4012
4415
|
KoeEngine,
|
|
4013
4416
|
MIN_WORLDLINE_SAMPLES,
|
|
4014
4417
|
NAIST_JDIC_FILES,
|
|
4418
|
+
SPEAKING_STYLES,
|
|
4015
4419
|
UtauTTSAdapter,
|
|
4016
4420
|
VoiceBank,
|
|
4017
4421
|
WORLDLINE_SAMPLE_RATE,
|
|
@@ -4050,15 +4454,21 @@ export {
|
|
|
4050
4454
|
parseKoeHeader,
|
|
4051
4455
|
parseOto,
|
|
4052
4456
|
parseWav,
|
|
4457
|
+
pauseKind,
|
|
4053
4458
|
pcmBase,
|
|
4054
4459
|
pitchFromAliasSuffix,
|
|
4055
4460
|
readWavPcm48k,
|
|
4056
4461
|
readingFromFeatures,
|
|
4057
4462
|
resample,
|
|
4463
|
+
resolveSpeakingStyle,
|
|
4058
4464
|
samplesToMs,
|
|
4465
|
+
shapeDurations,
|
|
4059
4466
|
shapeProsody,
|
|
4060
4467
|
sparse_features,
|
|
4061
4468
|
splitKana,
|
|
4469
|
+
styleAlignOptions,
|
|
4470
|
+
styleRenderOptions,
|
|
4471
|
+
styleShapeOptions,
|
|
4062
4472
|
suffixFromFolderName,
|
|
4063
4473
|
summarise,
|
|
4064
4474
|
toHiragana,
|
|
@@ -4067,6 +4477,7 @@ export {
|
|
|
4067
4477
|
transcribe,
|
|
4068
4478
|
trimToOto,
|
|
4069
4479
|
unzipToFileMap,
|
|
4480
|
+
warpPitchCurve,
|
|
4070
4481
|
zipFiles
|
|
4071
4482
|
};
|
|
4072
4483
|
//# sourceMappingURL=index.js.map
|