from enum import Enum from re import findall, fullmatch from typing import Dict, List, Optional from pydantic import BaseModel, Field, conint, validator from .metas.Metas import Speaker, SpeakerInfo class Mora(BaseModel): """ モーラ(子音+母音)ごとの情報 """ text: str = Field(title="文字") consonant: Optional[str] = Field(title="子音の音素") consonant_length: Optional[float] = Field(title="子音の音長") vowel: str = Field(title="母音の音素") vowel_length: float = Field(title="母音の音長") pitch: float = Field(title="音高") # デフォルト値をつけるとts側のOpenAPIで生成されたコードの型がOptionalになる def __hash__(self): items = [ (k, tuple(v)) if isinstance(v, List) else (k, v) for k, v in self.__dict__.items() ] return hash(tuple(sorted(items))) class AccentPhrase(BaseModel): """ アクセント句ごとの情報 """ moras: List[Mora] = Field(title="モーラのリスト") accent: int = Field(title="アクセント箇所") pause_mora: Optional[Mora] = Field(title="後ろに無音を付けるかどうか") is_interrogative: bool = Field(default=False, title="疑問系かどうか") def __hash__(self): items = [ (k, tuple(v)) if isinstance(v, List) else (k, v) for k, v in self.__dict__.items() ] return hash(tuple(sorted(items))) class AudioQuery(BaseModel): """ 音声合成用のクエリ """ accent_phrases: List[AccentPhrase] = Field(title="アクセント句のリスト") speedScale: float = Field(title="全体の話速") pitchScale: float = Field(title="全体の音高") intonationScale: float = Field(title="全体の抑揚") volumeScale: float = Field(title="全体の音量") prePhonemeLength: float = Field(title="音声の前の無音時間") postPhonemeLength: float = Field(title="音声の後の無音時間") outputSamplingRate: int = Field(title="音声データの出力サンプリングレート") outputStereo: bool = Field(title="音声データをステレオ出力するか否か") kana: Optional[str] = Field(title="[読み取り専用]AquesTalkライクな読み仮名。音声合成クエリとしては無視される") def __hash__(self): items = [ (k, tuple(v)) if isinstance(v, List) else (k, v) for k, v in self.__dict__.items() ] return hash(tuple(sorted(items))) class ParseKanaErrorCode(Enum): UNKNOWN_TEXT = "判別できない読み仮名があります: {text}" ACCENT_TOP = "句頭にアクセントは置けません: {text}" ACCENT_TWICE = "1つのアクセント句に二つ以上のアクセントは置けません: {text}" ACCENT_NOTFOUND = "アクセントを指定していないアクセント句があります: {text}" EMPTY_PHRASE = "{position}番目のアクセント句が空白です" INTERROGATION_MARK_NOT_AT_END = "アクセント句末以外に「?」は置けません: {text}" INFINITE_LOOP = "処理時に無限ループになってしまいました...バグ報告をお願いします。" class ParseKanaError(Exception): def __init__(self, errcode: ParseKanaErrorCode, **kwargs): self.errcode = errcode self.errname = errcode.name self.kwargs: Dict[str, str] = kwargs err_fmt: str = errcode.value self.text = err_fmt.format(**kwargs) class ParseKanaBadRequest(BaseModel): text: str = Field(title="エラーメッセージ") error_name: str = Field( title="エラー名", description="|name|description|\n|---|---|\n" + "\n".join( [ "| {} | {} |".format(err.name, err.value) for err in list(ParseKanaErrorCode) ] ), ) error_args: Dict[str, str] = Field(title="エラーを起こした箇所") def __init__(self, err: ParseKanaError): super().__init__(text=err.text, error_name=err.errname, error_args=err.kwargs) class MorphableTargetInfo(BaseModel): is_morphable: bool = Field(title="指定した話者に対してモーフィングの可否") # FIXME: add reason property # reason: Optional[str] = Field(title="is_morphableがfalseである場合、その理由") class SpeakerNotFoundError(LookupError): def __init__(self, speaker: int, *args: object, **kywrds: object) -> None: self.speaker = speaker super().__init__(f"speaker {speaker} is not found.", *args, **kywrds) class LibrarySpeaker(BaseModel): """ 音声ライブラリに含まれる話者の情報 """ speaker: Speaker = Field(title="話者情報") speaker_info: SpeakerInfo = Field(title="話者の追加情報") class DownloadableLibrary(BaseModel): """ ダウンロード可能な音声ライブラリの情報 """ name: str = Field(title="音声ライブラリの名前") uuid: str = Field(title="音声ライブラリのUUID") version: str = Field(title="音声ライブラリのバージョン") download_url: str = Field(title="音声ライブラリのダウンロードURL") bytes: int = Field(title="音声ライブラリのバイト数") speakers: List[LibrarySpeaker] = Field(title="音声ライブラリに含まれる話者のリスト") USER_DICT_MIN_PRIORITY = 0 USER_DICT_MAX_PRIORITY = 10 class UserDictWord(BaseModel): """ 辞書のコンパイルに使われる情報 """ surface: str = Field(title="表層形") priority: conint(ge=USER_DICT_MIN_PRIORITY, le=USER_DICT_MAX_PRIORITY) = Field( title="優先度" ) context_id: int = Field(title="文脈ID", default=1348) part_of_speech: str = Field(title="品詞") part_of_speech_detail_1: str = Field(title="品詞細分類1") part_of_speech_detail_2: str = Field(title="品詞細分類2") part_of_speech_detail_3: str = Field(title="品詞細分類3") inflectional_type: str = Field(title="活用型") inflectional_form: str = Field(title="活用形") stem: str = Field(title="原形") yomi: str = Field(title="読み") pronunciation: str = Field(title="発音") accent_type: int = Field(title="アクセント型") mora_count: Optional[int] = Field(title="モーラ数") accent_associative_rule: str = Field(title="アクセント結合規則") class Config: validate_assignment = True @validator("surface") def convert_to_zenkaku(cls, surface): return surface.translate( str.maketrans( "".join(chr(0x21 + i) for i in range(94)), "".join(chr(0xFF01 + i) for i in range(94)), ) ) @validator("pronunciation", pre=True) def check_is_katakana(cls, pronunciation): if not fullmatch(r"[ァ-ヴー]+", pronunciation): raise ValueError("発音は有効なカタカナでなくてはいけません。") sutegana = ["ァ", "ィ", "ゥ", "ェ", "ォ", "ャ", "ュ", "ョ", "ヮ", "ッ"] for i in range(len(pronunciation)): if pronunciation[i] in sutegana: # 「キャット」のように、捨て仮名が連続する可能性が考えられるので、 # 「ッ」に関しては「ッ」そのものが連続している場合と、「ッ」の後にほかの捨て仮名が連続する場合のみ無効とする if i < len(pronunciation) - 1 and ( pronunciation[i + 1] in sutegana[:-1] or ( pronunciation[i] == sutegana[-1] and pronunciation[i + 1] == sutegana[-1] ) ): raise ValueError("無効な発音です。(捨て仮名の連続)") if pronunciation[i] == "ヮ": if i != 0 and pronunciation[i - 1] not in ["ク", "グ"]: raise ValueError("無効な発音です。(「くゎ」「ぐゎ」以外の「ゎ」の使用)") return pronunciation @validator("mora_count", pre=True, always=True) def check_mora_count_and_accent_type(cls, mora_count, values): if "pronunciation" not in values or "accent_type" not in values: # 適切な場所でエラーを出すようにする return mora_count if mora_count is None: rule_others = "[イ][ェ]|[ヴ][ャュョ]|[トド][ゥ]|[テデ][ィャュョ]|[デ][ェ]|[クグ][ヮ]" rule_line_i = "[キシチニヒミリギジビピ][ェャュョ]" rule_line_u = "[ツフヴ][ァ]|[ウスツフヴズ][ィ]|[ウツフヴ][ェォ]" rule_one_mora = "[ァ-ヴー]" mora_count = len( findall( f"(?:{rule_others}|{rule_line_i}|{rule_line_u}|{rule_one_mora})", values["pronunciation"], ) ) if not 0 <= values["accent_type"] <= mora_count: raise ValueError( "誤ったアクセント型です({})。 expect: 0 <= accent_type <= {}".format( values["accent_type"], mora_count ) ) return mora_count class PartOfSpeechDetail(BaseModel): """ 品詞ごとの情報 """ part_of_speech: str = Field(title="品詞") part_of_speech_detail_1: str = Field(title="品詞細分類1") part_of_speech_detail_2: str = Field(title="品詞細分類2") part_of_speech_detail_3: str = Field(title="品詞細分類3") # context_idは辞書の左・右文脈IDのこと # https://github.com/VOICEVOX/open_jtalk/blob/427cfd761b78efb6094bea3c5bb8c968f0d711ab/src/mecab-naist-jdic/_left-id.def # noqa context_id: int = Field(title="文脈ID") cost_candidates: List[int] = Field(title="コストのパーセンタイル") accent_associative_rules: List[str] = Field(title="アクセント結合規則の一覧") class WordTypes(str, Enum): """ fastapiでword_type引数を検証する時に使用するクラス """ PROPER_NOUN = "PROPER_NOUN" COMMON_NOUN = "COMMON_NOUN" VERB = "VERB" ADJECTIVE = "ADJECTIVE" SUFFIX = "SUFFIX" class SupportedDevicesInfo(BaseModel): """ 対応しているデバイスの情報 """ cpu: bool = Field(title="CPUに対応しているか") cuda: bool = Field(title="CUDA(Nvidia GPU)に対応しているか") dml: bool = Field(title="DirectML(Nvidia GPU/Radeon GPU等)に対応しているか") class SupportedFeaturesInfo(BaseModel): """ エンジンの機能の情報 """ support_adjusting_mora: bool = Field(title="モーラが調整可能かどうか") support_adjusting_speed_scale: bool = Field(title="話速が調整可能かどうか") support_adjusting_pitch_scale: bool = Field(title="音高が調整可能かどうか") support_adjusting_intonation_scale: bool = Field(title="抑揚が調整可能かどうか") support_adjusting_volume_scale: bool = Field(title="音量が調整可能かどうか") support_adjusting_silence_scale: bool = Field(title="前後の無音時間が調節可能かどうか") support_interrogative_upspeak: bool = Field(title="疑似疑問文に対応しているかどうか") support_switching_device: bool = Field(title="CPU/GPUの切り替えが可能かどうか")