-
Notifications
You must be signed in to change notification settings - Fork 313
/
Copy pathtalkModelMock.ts
251 lines (217 loc) · 7.57 KB
/
talkModelMock.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
/**
* トーク系の構造体を作るモック。
*/
import { builder, IpadicFeatures, Tokenizer } from "kuromoji";
import { moraToPhonemes } from "./phonemeMock";
import { parseKana } from "./aquestalkLikeMock";
import { moraPattern } from "@/domain/japanese";
import { AccentPhrase, Mora } from "@/openapi";
import { isNode } from "@/helpers/platform";
let _tokenizer: Tokenizer<IpadicFeatures> | undefined;
/** kuromoji用の辞書のパスを取得する */
function getDicPath() {
if (isNode) {
return "node_modules/kuromoji/dict";
} else {
return "https://cdn.jsdelivr.net/npm/[email protected]/dict";
}
}
/** テキストをトークン列に変換するトークナイザーを取得する */
async function createOrGetTokenizer() {
if (_tokenizer != undefined) {
return _tokenizer;
}
return new Promise<Tokenizer<IpadicFeatures>>((resolve, reject) => {
builder({
dicPath: getDicPath(),
nodeOrBrowser: isNode ? "node" : "browser",
}).build((err: Error, tokenizer: Tokenizer<IpadicFeatures>) => {
if (err) {
reject(err);
} else {
_tokenizer = tokenizer;
resolve(tokenizer);
}
});
});
}
/** アルファベット文字列を適当な0~1の適当な数値に変換する */
function alphabetsToNumber(text: string): number {
const codes = text.split("").map((c) => c.charCodeAt(0));
const sum = codes.reduce((a, b) => a + b);
return (sum % 256) / 256;
}
/** 0.01~0.25になるように適当な長さを決める */
function phonemeToLengthMock(phoneme: string): number {
return alphabetsToNumber(phoneme) * 0.24 + 0.01;
}
/** 3~5になるように適当なピッチを決める */
function phonemeToPitchMock(phoneme: string): number {
return (1 - alphabetsToNumber(phoneme)) * 2 + 3;
}
/** カタカナテキストをモーラに変換する */
function textToMoraMock(text: string): Mora {
const phonemes = moraToPhonemes[text];
if (phonemes == undefined) throw new Error(`モーラに変換できません: ${text}`);
return {
text,
consonant: phonemes[0],
consonantLength: phonemes[0] == undefined ? undefined : 0,
vowel: phonemes[1],
vowelLength: 0,
pitch: 0,
};
}
/**
* カタカナテキストを適当なアクセント句に変換する。
* アクセント位置は適当に決める。
*/
function textToAccentPhraseMock(text: string): AccentPhrase {
const moras: Mora[] = [...text.matchAll(moraPattern)].map((m) =>
textToMoraMock(m[0]),
);
const alphabets = moras.map((m) => (m.consonant ?? "") + m.vowel).join("");
const accent =
1 + Math.round(alphabetsToNumber(alphabets) * (moras.length - 1));
return { moras, accent };
}
/**
* アクセント句内のモーラの長さを適当に代入する。
* 最後のモーラだけ長くする。
*/
export function replaceLengthMock(
accentPhrases: AccentPhrase[],
styleId: number,
) {
for (const accentPhrase of accentPhrases) {
for (let i = 0; i < accentPhrase.moras.length; i++) {
const mora = accentPhrase.moras[i];
// 最後のモーラだけ長く
const offset = i == accentPhrase.moras.length - 1 ? 0.05 : 0;
if (mora.consonant != undefined)
mora.consonantLength =
(phonemeToLengthMock(mora.consonant) + offset) / 5;
mora.vowelLength = phonemeToLengthMock(mora.vowel) + offset;
}
}
// 別のアクセント句や話者で同じにならないように適当に値をずらす
for (let i = 0; i < accentPhrases.length; i++) {
const diff = i * 0.01 + styleId * 0.03;
const accentPhrase = accentPhrases[i];
for (const mora of accentPhrase.moras) {
if (mora.consonantLength != undefined) mora.consonantLength += diff;
mora.vowelLength += diff;
}
if (accentPhrase.pauseMora != undefined) {
accentPhrase.pauseMora.vowelLength += diff;
}
}
}
/**
* アクセント句内のモーラのピッチを適当に代入する。
* アクセント位置のモーラだけ高くする。
*/
export function replacePitchMock(
accentPhrases: AccentPhrase[],
styleId: number,
) {
for (const accentPhrase of accentPhrases) {
for (let i = 0; i < accentPhrase.moras.length; i++) {
const mora = accentPhrase.moras[i];
// 無声化している場合はピッチを0にする
if (mora.vowel == "U") {
mora.pitch = 0;
continue;
}
// アクセント位置のモーラだけ高く
const offset = i == accentPhrase.accent ? 0.3 : 0;
const phoneme = (mora.consonant ?? "") + mora.vowel[1];
mora.pitch = phonemeToPitchMock(phoneme) + offset;
}
}
// 別のアクセント句や話者で同じにならないように適当に値をずらす
for (let i = 0; i < accentPhrases.length; i++) {
const diff = i * 0.01 + styleId * 0.03;
const accentPhrase = accentPhrases[i];
for (const mora of accentPhrase.moras) {
if (mora.pitch > 0) mora.pitch += diff;
}
}
}
/**
* テキストを適当なアクセント句に分割する。
* 助詞ごとに区切る。記号ごとに無音を入れる。
* 無音で終わるアクセント句の最後のモーラが「す」「つ」の場合は無声化する。
*/
export async function textToActtentPhrasesMock(text: string, styleId: number) {
const accentPhrases: AccentPhrase[] = [];
// トークンに分割
const tokenizer = await createOrGetTokenizer();
const tokens = tokenizer.tokenize(text);
let textPhrase = "";
for (const token of tokens) {
// 記号の場合は無音を入れて区切る
if (token.pos == "記号") {
const pauseMora = {
text: "、",
vowel: "pau",
vowelLength: 1 - 1 / (accentPhrases.length + 2),
pitch: 0,
};
// テキストが空の場合は前のアクセント句に無音を追加、空でない場合は新しいアクセント句を追加
let accentPhrase: AccentPhrase;
if (textPhrase.length === 0) {
accentPhrase = accentPhrases[accentPhrases.length - 1];
} else {
accentPhrase = textToAccentPhraseMock(textPhrase);
accentPhrases.push(accentPhrase);
}
accentPhrase.pauseMora = pauseMora;
textPhrase = "";
continue;
}
// 記号以外は連結
if (token.reading == undefined)
throw new Error(`発音がないトークン: ${token.surface_form}`);
textPhrase += token.reading;
// 助詞の場合は区切る
if (token.pos == "助詞") {
accentPhrases.push(textToAccentPhraseMock(textPhrase));
textPhrase = "";
}
}
if (textPhrase != "") {
accentPhrases.push(textToAccentPhraseMock(textPhrase));
}
// 最後のアクセント句の無音をなくす
if (accentPhrases.length > 0) {
const lastPhrase = accentPhrases[accentPhrases.length - 1];
lastPhrase.pauseMora = undefined;
}
// 無音のあるアクセント句を無声化
for (const phrase of accentPhrases) {
if (phrase.pauseMora == undefined) continue;
const lastMora = phrase.moras[phrase.moras.length - 1];
if (lastMora.text == "ス" || lastMora.text == "ツ") {
lastMora.vowel = "U";
lastMora.pitch = 0;
}
}
// 長さとピッチを代入
replaceLengthMock(accentPhrases, styleId);
replacePitchMock(accentPhrases, styleId);
return accentPhrases;
}
/**
* AquesTalk風記法をアクセント句に変換する。
*/
export async function aquestalkLikeToAccentPhrasesMock(
text: string,
styleId: number,
) {
const accentPhrases: AccentPhrase[] = parseKana(text);
// 長さとピッチを代入
replaceLengthMock(accentPhrases, styleId);
replacePitchMock(accentPhrases, styleId);
return accentPhrases;
}