Spaces:
Build error
Build error
| import re | |
| from dataclasses import dataclass | |
| from itertools import chain | |
| from typing import Dict, List, Optional | |
| import pyopenjtalk | |
| class Phoneme: | |
| """ | |
| 音素(母音・子音)クラス、音素の元となるcontextを保持する | |
| 音素には、母音や子音以外にも無音(silent/pause)も含まれる | |
| Attributes | |
| ---------- | |
| contexts: Dict[str, str] | |
| 音素の元 | |
| """ | |
| contexts: Dict[str, str] | |
| def from_label(cls, label: str): | |
| """ | |
| pyopenjtalk.extract_fullcontextで得られる音素の元(ラベル)から、Phonemeクラスを作成する | |
| Parameters | |
| ---------- | |
| label : str | |
| pyopenjtalk.extract_fullcontextで得られるラベルを渡す | |
| Returns | |
| ------- | |
| phoneme: Phoneme | |
| Phonemeクラスを返す | |
| """ | |
| # フルコンテキストラベルの仕様は、 | |
| # http://hts.sp.nitech.ac.jp/?Download の HTS-2.3のJapanese tar.bz2 (126 MB)をダウンロードして、data/lab_format.pdfを見るとリストが見つかります。 # noqa | |
| contexts = re.search( | |
| r"^(?P<p1>.+?)\^(?P<p2>.+?)\-(?P<p3>.+?)\+(?P<p4>.+?)\=(?P<p5>.+?)" | |
| r"/A\:(?P<a1>.+?)\+(?P<a2>.+?)\+(?P<a3>.+?)" | |
| r"/B\:(?P<b1>.+?)\-(?P<b2>.+?)\_(?P<b3>.+?)" | |
| r"/C\:(?P<c1>.+?)\_(?P<c2>.+?)\+(?P<c3>.+?)" | |
| r"/D\:(?P<d1>.+?)\+(?P<d2>.+?)\_(?P<d3>.+?)" | |
| r"/E\:(?P<e1>.+?)\_(?P<e2>.+?)\!(?P<e3>.+?)\_(?P<e4>.+?)\-(?P<e5>.+?)" | |
| r"/F\:(?P<f1>.+?)\_(?P<f2>.+?)\#(?P<f3>.+?)\_(?P<f4>.+?)\@(?P<f5>.+?)\_(?P<f6>.+?)\|(?P<f7>.+?)\_(?P<f8>.+?)" # noqa | |
| r"/G\:(?P<g1>.+?)\_(?P<g2>.+?)\%(?P<g3>.+?)\_(?P<g4>.+?)\_(?P<g5>.+?)" | |
| r"/H\:(?P<h1>.+?)\_(?P<h2>.+?)" | |
| r"/I\:(?P<i1>.+?)\-(?P<i2>.+?)\@(?P<i3>.+?)\+(?P<i4>.+?)\&(?P<i5>.+?)\-(?P<i6>.+?)\|(?P<i7>.+?)\+(?P<i8>.+?)" # noqa | |
| r"/J\:(?P<j1>.+?)\_(?P<j2>.+?)" | |
| r"/K\:(?P<k1>.+?)\+(?P<k2>.+?)\-(?P<k3>.+?)$", | |
| label, | |
| ).groupdict() | |
| return cls(contexts=contexts) | |
| def label(self): | |
| """ | |
| pyopenjtalk.extract_fullcontextで得られるラベルと等しい | |
| Returns | |
| ------- | |
| lebel: str | |
| ラベルを返す | |
| """ | |
| return ( | |
| "{p1}^{p2}-{p3}+{p4}={p5}" | |
| "/A:{a1}+{a2}+{a3}" | |
| "/B:{b1}-{b2}_{b3}" | |
| "/C:{c1}_{c2}+{c3}" | |
| "/D:{d1}+{d2}_{d3}" | |
| "/E:{e1}_{e2}!{e3}_{e4}-{e5}" | |
| "/F:{f1}_{f2}#{f3}_{f4}@{f5}_{f6}|{f7}_{f8}" | |
| "/G:{g1}_{g2}%{g3}_{g4}_{g5}" | |
| "/H:{h1}_{h2}" | |
| "/I:{i1}-{i2}@{i3}+{i4}&{i5}-{i6}|{i7}+{i8}" | |
| "/J:{j1}_{j2}" | |
| "/K:{k1}+{k2}-{k3}" | |
| ).format(**self.contexts) | |
| def phoneme(self): | |
| """ | |
| 音素クラスの中で、発声に必要な要素を返す | |
| Returns | |
| ------- | |
| phoneme : str | |
| 発声に必要な要素を返す | |
| """ | |
| return self.contexts["p3"] | |
| def is_pause(self): | |
| """ | |
| 音素がポーズ(無音、silent/pause)であるかを返す | |
| Returns | |
| ------- | |
| is_pose : bool | |
| 音素がポーズ(無音、silent/pause)であるか(True)否か(False) | |
| """ | |
| return self.contexts["f1"] == "xx" | |
| def __repr__(self): | |
| return f"<Phoneme phoneme='{self.phoneme}'>" | |
| class Mora: | |
| """ | |
| モーラクラス | |
| モーラは1音素(母音や促音「っ」、撥音「ん」など)か、2音素(母音と子音の組み合わせ)で成り立つ | |
| Attributes | |
| ---------- | |
| consonant : Optional[Phoneme] | |
| 子音 | |
| vowel : Phoneme | |
| 母音 | |
| """ | |
| consonant: Optional[Phoneme] | |
| vowel: Phoneme | |
| def set_context(self, key: str, value: str): | |
| """ | |
| Moraクラス内に含まれるPhonemeのcontextのうち、指定されたキーの値を変更する | |
| consonantが存在する場合は、vowelと同じようにcontextを変更する | |
| Parameters | |
| ---------- | |
| key : str | |
| 変更したいcontextのキー | |
| value : str | |
| 変更したいcontextの値 | |
| """ | |
| self.vowel.contexts[key] = value | |
| if self.consonant is not None: | |
| self.consonant.contexts[key] = value | |
| def phonemes(self): | |
| """ | |
| 音素群を返す | |
| Returns | |
| ------- | |
| phonemes : List[Phoneme] | |
| 母音しかない場合は母音のみ、子音もある場合は子音、母音の順番でPhonemeのリストを返す | |
| """ | |
| if self.consonant is not None: | |
| return [self.consonant, self.vowel] | |
| else: | |
| return [self.vowel] | |
| def labels(self): | |
| """ | |
| ラベル群を返す | |
| Returns | |
| ------- | |
| labels : List[str] | |
| Moraに含まれるすべてのラベルを返す | |
| """ | |
| return [p.label for p in self.phonemes] | |
| class AccentPhrase: | |
| """ | |
| アクセント句クラス | |
| 同じアクセントのMoraを複数保持する | |
| Attributes | |
| ---------- | |
| moras : List[Mora] | |
| 音韻のリスト | |
| accent : int | |
| アクセント | |
| """ | |
| moras: List[Mora] | |
| accent: int | |
| is_interrogative: bool | |
| def from_phonemes(cls, phonemes: List[Phoneme]): | |
| """ | |
| PhonemeのリストからAccentPhraseクラスを作成する | |
| Parameters | |
| ---------- | |
| phonemes : List[Phoneme] | |
| phonemeのリストを渡す | |
| Returns | |
| ------- | |
| accent_phrase : AccentPhrase | |
| AccentPhraseクラスを返す | |
| """ | |
| moras: List[Mora] = [] | |
| mora_phonemes: List[Phoneme] = [] | |
| for phoneme, next_phoneme in zip(phonemes, phonemes[1:] + [None]): | |
| # workaround for Hihosiba/voicevox_engine#57 | |
| # (py)openjtalk によるアクセント句内のモーラへの附番は 49 番目まで | |
| # 49 番目のモーラについて、続く音素のモーラ番号を単一モーラの特定に使えない | |
| if int(phoneme.contexts["a2"]) == 49: | |
| break | |
| mora_phonemes.append(phoneme) | |
| if ( | |
| next_phoneme is None | |
| or phoneme.contexts["a2"] != next_phoneme.contexts["a2"] | |
| ): | |
| if len(mora_phonemes) == 1: | |
| consonant, vowel = None, mora_phonemes[0] | |
| elif len(mora_phonemes) == 2: | |
| consonant, vowel = mora_phonemes[0], mora_phonemes[1] | |
| else: | |
| raise ValueError(mora_phonemes) | |
| mora = Mora(consonant=consonant, vowel=vowel) | |
| moras.append(mora) | |
| mora_phonemes = [] | |
| accent = int(moras[0].vowel.contexts["f2"]) | |
| # workaround for Hihosiba/voicevox_engine#55 | |
| # アクセント位置とするキー f2 の値がアクセント句内のモーラ数を超える場合がある | |
| accent = accent if accent <= len(moras) else len(moras) | |
| is_interrogative = moras[-1].vowel.contexts["f3"] == "1" | |
| return cls(moras=moras, accent=accent, is_interrogative=is_interrogative) | |
| def set_context(self, key: str, value: str): | |
| """ | |
| AccentPhraseに間接的に含まれる全てのPhonemeのcontextの、指定されたキーの値を変更する | |
| Parameters | |
| ---------- | |
| key : str | |
| 変更したいcontextのキー | |
| value : str | |
| 変更したいcontextの値 | |
| """ | |
| for mora in self.moras: | |
| mora.set_context(key, value) | |
| def phonemes(self): | |
| """ | |
| 音素群を返す | |
| Returns | |
| ------- | |
| phonemes : List[Phoneme] | |
| AccentPhraseに間接的に含まれる全てのPhonemeを返す | |
| """ | |
| return list(chain.from_iterable(m.phonemes for m in self.moras)) | |
| def labels(self): | |
| """ | |
| ラベル群を返す | |
| Returns | |
| ------- | |
| labels : List[str] | |
| AccentPhraseに間接的に含まれる全てのラベルを返す | |
| """ | |
| return [p.label for p in self.phonemes] | |
| def merge(self, accent_phrase: "AccentPhrase"): | |
| """ | |
| AccentPhraseを合成する | |
| (このクラスが保持するmorasの後ろに、引数として渡されたAccentPhraseのmorasを合成する) | |
| Parameters | |
| ---------- | |
| accent_phrase : AccentPhrase | |
| 合成したいAccentPhraseを渡す | |
| Returns | |
| ------- | |
| accent_phrase : AccentPhrase | |
| 合成されたAccentPhraseを返す | |
| """ | |
| return AccentPhrase( | |
| moras=self.moras + accent_phrase.moras, | |
| accent=self.accent, | |
| is_interrogative=accent_phrase.is_interrogative, | |
| ) | |
| class BreathGroup: | |
| """ | |
| 発声の区切りクラス | |
| アクセントの異なるアクセント句を複数保持する | |
| Attributes | |
| ---------- | |
| accent_phrases : List[AccentPhrase] | |
| アクセント句のリスト | |
| """ | |
| accent_phrases: List[AccentPhrase] | |
| def from_phonemes(cls, phonemes: List[Phoneme]): | |
| """ | |
| PhonemeのリストからBreathGroupクラスを作成する | |
| Parameters | |
| ---------- | |
| phonemes : List[Phoneme] | |
| phonemeのリストを渡す | |
| Returns | |
| ------- | |
| breath_group : BreathGroup | |
| BreathGroupクラスを返す | |
| """ | |
| accent_phrases: List[AccentPhrase] = [] | |
| accent_phonemes: List[Phoneme] = [] | |
| for phoneme, next_phoneme in zip(phonemes, phonemes[1:] + [None]): | |
| accent_phonemes.append(phoneme) | |
| if ( | |
| next_phoneme is None | |
| or phoneme.contexts["i3"] != next_phoneme.contexts["i3"] | |
| or phoneme.contexts["f5"] != next_phoneme.contexts["f5"] | |
| ): | |
| accent_phrase = AccentPhrase.from_phonemes(accent_phonemes) | |
| accent_phrases.append(accent_phrase) | |
| accent_phonemes = [] | |
| return cls(accent_phrases=accent_phrases) | |
| def set_context(self, key: str, value: str): | |
| """ | |
| BreathGroupに間接的に含まれる全てのPhonemeのcontextの、指定されたキーの値を変更する | |
| Parameters | |
| ---------- | |
| key : str | |
| 変更したいcontextのキー | |
| value : str | |
| 変更したいcontextの値 | |
| """ | |
| for accent_phrase in self.accent_phrases: | |
| accent_phrase.set_context(key, value) | |
| def phonemes(self): | |
| """ | |
| 音素群を返す | |
| Returns | |
| ------- | |
| phonemes : List[Phoneme] | |
| BreathGroupに間接的に含まれる全てのPhonemeを返す | |
| """ | |
| return list( | |
| chain.from_iterable( | |
| accent_phrase.phonemes for accent_phrase in self.accent_phrases | |
| ) | |
| ) | |
| def labels(self): | |
| """ | |
| ラベル群を返す | |
| Returns | |
| ------- | |
| labels : List[str] | |
| BreathGroupに間接的に含まれる全てのラベルを返す | |
| """ | |
| return [p.label for p in self.phonemes] | |
| class Utterance: | |
| """ | |
| 発声クラス | |
| 発声の区切りと無音を複数保持する | |
| Attributes | |
| ---------- | |
| breath_groups : List[BreathGroup] | |
| 発声の区切りのリスト | |
| pauses : List[Phoneme] | |
| 無音のリスト | |
| """ | |
| breath_groups: List[BreathGroup] | |
| pauses: List[Phoneme] | |
| def from_phonemes(cls, phonemes: List[Phoneme]): | |
| """ | |
| Phonemeの完全なリストからUtteranceクラスを作成する | |
| Parameters | |
| ---------- | |
| phonemes : List[Phoneme] | |
| phonemeのリストを渡す | |
| Returns | |
| ------- | |
| utterance : Utterance | |
| Utteranceクラスを返す | |
| """ | |
| pauses: List[Phoneme] = [] | |
| breath_groups: List[BreathGroup] = [] | |
| group_phonemes: List[Phoneme] = [] | |
| for phoneme in phonemes: | |
| if not phoneme.is_pause(): | |
| group_phonemes.append(phoneme) | |
| else: | |
| pauses.append(phoneme) | |
| if len(group_phonemes) > 0: | |
| breath_group = BreathGroup.from_phonemes(group_phonemes) | |
| breath_groups.append(breath_group) | |
| group_phonemes = [] | |
| return cls(breath_groups=breath_groups, pauses=pauses) | |
| def set_context(self, key: str, value: str): | |
| """ | |
| Utteranceに間接的に含まれる全てのPhonemeのcontextの、指定されたキーの値を変更する | |
| Parameters | |
| ---------- | |
| key : str | |
| 変更したいcontextのキー | |
| value : str | |
| 変更したいcontextの値 | |
| """ | |
| for breath_group in self.breath_groups: | |
| breath_group.set_context(key, value) | |
| def phonemes(self): | |
| """ | |
| 音素群を返す | |
| Returns | |
| ------- | |
| phonemes : List[Phoneme] | |
| Utteranceクラスに直接的・間接的に含まれる、全てのPhonemeを返す | |
| """ | |
| accent_phrases = list( | |
| chain.from_iterable( | |
| breath_group.accent_phrases for breath_group in self.breath_groups | |
| ) | |
| ) | |
| for prev, cent, post in zip( | |
| [None] + accent_phrases[:-1], | |
| accent_phrases, | |
| accent_phrases[1:] + [None], | |
| ): | |
| mora_num = len(cent.moras) | |
| accent = cent.accent | |
| if prev is not None: | |
| prev.set_context("g1", str(mora_num)) | |
| prev.set_context("g2", str(accent)) | |
| if post is not None: | |
| post.set_context("e1", str(mora_num)) | |
| post.set_context("e2", str(accent)) | |
| cent.set_context("f1", str(mora_num)) | |
| cent.set_context("f2", str(accent)) | |
| for i_mora, mora in enumerate(cent.moras): | |
| mora.set_context("a1", str(i_mora - accent + 1)) | |
| mora.set_context("a2", str(i_mora + 1)) | |
| mora.set_context("a3", str(mora_num - i_mora)) | |
| for prev, cent, post in zip( | |
| [None] + self.breath_groups[:-1], | |
| self.breath_groups, | |
| self.breath_groups[1:] + [None], | |
| ): | |
| accent_phrase_num = len(cent.accent_phrases) | |
| if prev is not None: | |
| prev.set_context("j1", str(accent_phrase_num)) | |
| if post is not None: | |
| post.set_context("h1", str(accent_phrase_num)) | |
| cent.set_context("i1", str(accent_phrase_num)) | |
| cent.set_context( | |
| "i5", str(accent_phrases.index(cent.accent_phrases[0]) + 1) | |
| ) | |
| cent.set_context( | |
| "i6", | |
| str(len(accent_phrases) - accent_phrases.index(cent.accent_phrases[0])), | |
| ) | |
| self.set_context( | |
| "k2", | |
| str( | |
| sum( | |
| [ | |
| len(breath_group.accent_phrases) | |
| for breath_group in self.breath_groups | |
| ] | |
| ) | |
| ), | |
| ) | |
| phonemes: List[Phoneme] = [] | |
| for i in range(len(self.pauses)): | |
| if self.pauses[i] is not None: | |
| phonemes += [self.pauses[i]] | |
| if i < len(self.pauses) - 1: | |
| phonemes += self.breath_groups[i].phonemes | |
| return phonemes | |
| def labels(self): | |
| """ | |
| ラベル群を返す | |
| Returns | |
| ------- | |
| labels : List[str] | |
| Utteranceクラスに直接的・間接的に含まれる全てのラベルを返す | |
| """ | |
| return [p.label for p in self.phonemes] | |
| def extract_full_context_label(text: str): | |
| labels = pyopenjtalk.extract_fullcontext(text) | |
| phonemes = [Phoneme.from_label(label=label) for label in labels] | |
| utterance = Utterance.from_phonemes(phonemes) | |
| return utterance | |