convert method
Converts text with the injected frontend.
As in the pinned pyopenjtalk-style mode, G2pResult.phonemes contains the rendered phoneme text followed immediately by its equal-scalar-length pitch trace.
Implementation
@override
G2pResult convert(String text) {
final words = _analyze(text);
final tokens = <_WorkingJapaneseToken>[];
var lastAccentState = 0;
int? phraseAccent;
var phraseMoraCount = 0;
for (var wordIndex = 0; wordIndex < words.length; wordIndex++) {
final word = words[wordIndex];
if (word.moraSize < 0) {
throw _invalidWord(
wordIndex,
'moraSize must be non-negative, got ${word.moraSize}.',
);
}
if (word.accent < 0) {
throw _invalidWord(
wordIndex,
'accent must be non-negative, got ${word.accent}.',
);
}
var moras = const <String>[];
if (word.moraSize > 0) {
moras = pronunciationToMoras(word.pronunciation);
final leadingLongVowelAdjustment =
moras.isNotEmpty && moras.first == 'ー' ? 1 : 0;
if (moras.length != word.moraSize &&
moras.length + leadingLongVowelAdjustment != word.moraSize) {
throw _invalidWord(
wordIndex,
'pronunciation `${word.pronunciation}` produced ${moras.length} '
'moras, but moraSize is ${word.moraSize}.',
);
}
}
final chainFlag =
word.moraSize > 0 &&
tokens.isNotEmpty &&
tokens.last.moraSize > 0 &&
(word.chainFlag || moras.first == 'ー');
if (!chainFlag) {
phraseAccent = null;
phraseMoraCount = 0;
}
phraseAccent ??= word.accent;
final accents = <int>[];
for (var moraIndex = 0; moraIndex < moras.length; moraIndex++) {
phraseMoraCount++;
final accentState = switch (phraseAccent) {
0 => phraseMoraCount == 1 ? 0 : (lastAccentState == 0 ? 1 : 2),
final int accent when accent == phraseMoraCount => 3,
final int accent
when 1 < phraseMoraCount && phraseMoraCount < accent =>
lastAccentState == 0 ? 1 : 2,
_ => 0,
};
accents.add(accentState);
lastAccentState = accentState;
}
final surface = _punctuationMap[word.surface] ?? word.surface;
var whitespace = '';
String? phonemes;
String? pitch;
if (moras.isNotEmpty) {
final phonemeBuffer = StringBuffer();
final pitchBuffer = StringBuffer();
for (var moraIndex = 0; moraIndex < moras.length; moraIndex++) {
final phoneme = _moraToPhoneme[moras[moraIndex]];
if (phoneme == null) {
throw MalformedDataException(
'Japanese mora table has no phoneme for `${moras[moraIndex]}`.',
);
}
phonemeBuffer.write(phoneme);
final pitchSymbol = switch (accents[moraIndex]) {
0 => '_',
3 => '^',
_ => '-',
};
_writeRepeated(pitchBuffer, pitchSymbol, _scalarLength(phoneme));
}
phonemes = phonemeBuffer.toString();
pitch = pitchBuffer.toString();
} else if (_isPunctuation(surface)) {
phonemes = surface;
final finalCharacter = _lastScalar(surface);
if (_punctuationStops.contains(finalCharacter)) {
whitespace = ' ';
if (tokens.isNotEmpty) {
tokens.last.whitespace = '';
}
} else if (_punctuationStarts.contains(finalCharacter) &&
tokens.isNotEmpty &&
tokens.last.whitespace.isEmpty) {
tokens.last.whitespace = ' ';
}
}
final mergesIntoPrevious =
(tokens.isNotEmpty && phonemes == null && surface == '・') ||
_isNonEmptyWhitespace(surface);
if (mergesIntoPrevious) {
if (tokens.isEmpty) {
throw _invalidWord(
wordIndex,
'leading whitespace cannot attach to a preceding token.',
);
}
tokens.last.whitespace = ' ';
continue;
}
tokens.add(
_WorkingJapaneseToken(
text: surface,
tag: word.partOfSpeech,
whitespace: whitespace,
phonemes: phonemes,
pronunciation: word.pronunciation,
accent: word.accent,
moraSize: word.moraSize,
chainFlag: chainFlag,
moras: moras,
accents: accents,
pitch: pitch,
),
);
}
return _render(tokens);
}