-
Notifications
You must be signed in to change notification settings - Fork 120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
TextAnalyzer
インターフェイスの導入
#730
Comments
どちらかというと賛成寄り、かな、という印象です! 実装コストの割にできることがほぼ増えないので、もし自分が実装するならかなり後回しにするかもです。 |
外部から呼ばれるAPIをいきなり変更すると結構な部分に波及してしまうと思うので,一旦Rust側にTextAnalyzer traitを実装してみました. |
この話ですが、ソングの存在によってTTS機能自体が必ずしも必要ではなくなる、つまりkanaのパーサーすら必要無い場合がありえるようになりました。つまり #694 で話した議論に戻ることになります。 |
https://discord.com/channels/879570910208733277/893889888208977960/1241243358131912704
|
色々考えましたが、やはり"Synthesizer"という名前に機能が集約されていた方がわかりやすいし便利なのではないかと思いました。 synth: TalkableSynthesizer[OpenJtalk] = Synthesizer().with_text_analyzer(ojt)
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^ ^^^
# class TalkableSynthesizer[_] class Synthesizer interface TextAnalyzer # `TextAnalyzer`が不要な操作はここに集約
class HasSynthesizer(ABC):
@property
@abstractmethod
def _synthesizer(self) -> "Synthesizer": ...
def load_voice_model(self, model: VoiceModel) -> None:
# 実装
...
def synthesis(self, query: AudioQuery) -> bytes:
# 実装
...
def with_text_analyzer[T: TextAnalyzer](self, text_analyzer: T) -> "TalkableSynthesizer[T]":
return TalkableSynthesizer(self._synthesizer, text_analyzer)
...
class Synthesizer(HasSynthesizer):
def __init__(self) -> None:
# 実装
...
@property
def _synthesizer(self) -> "Synthesizer":
return self
# `TextAnalyzer`が必要になってくる操作はここに
class TalkableSynthesizer[T: TextAnalyzer](HasSynthesizer):
def __init__(self, synthesizer: Synthesizer, text_analyzer: T) -> None:
self.synthesizer = synthesizer
self.text_analyzer = text_analyzer
@property
def _synthesizer(self) -> Synthesizer:
return self.synthesizer
def tts(self, text: str) -> bytes:
# 実装
...
... |
どっちにしろ 次の二つが同じですよいう流れでドキュメントも書きやすくなる。 wav = await synth.tts("こんにちは", style_id) phrases = synth.text_analyzer.analyze("こんにちは")
phrases = list(map(TextualAccentPhrase.with_zeros, phrases))
phrases = await synth.replace_phoneme_length(phrases, style_id)
phrases = await synth.replace_mora_pitch(phrases, style_id)
query = AudioQuery.from_accent_phrases(phrases)
wav = await synth.synthesis(query, style_id) |
↑からまた考えたのですが、
みたいな感じでよいことに気づきました。別にPythonで言う # Pythonだと`text_analyzer: T = Varnothing()`のような形式の引数指定が可能
synth: Synthesizer[Varnothing] = Synthesizer() synth: Synthesizer[OpenJtalk] = Synthesizer(ojt) 名前は
|
すみません、遅くなりました! 個人的には実行時エラーで良い気がしました!
よくわかってないのですが、そもそもgenerics的な感じで型を用意する必要がないかも、とちょっと思いました。
で、あとは
みたいな。まあ実行時エラーならこれでも・・・? |
今は無いですけどこういう感じでgetterを用意することを考えてました。これがあればユーザーが持ち回るのは synth.text_analyzer.use_user_dict(UserDict.load("./userdic.csv"))
# ^^^^^^^^^^^^^^^^^
# class OpenJtalk (getterを用意する場合、型をeraseしてしまうとユーザー側でダウンキャストするということになりむしろわかりにくくなります。また 追記
まあ無理は生じない…かも? むしろ悪くない気がしてきた。
|
一つ困る点が発生しうるとしたら、「 |
すみません遅くなりました!! 考えてたんですが、Synthesizerに2つ以上のAnalyzerを入れたくなるかもです!! うーーーーーーーーーーーーーん。。。どうしたもんか。。。 |
複数の class EngineTextAnalyzer(TextAnalyzer):
def __init__(self, ojt: Openjtalk) -> None:
self._ojt = ojt
def analyze(self, text: str) -> list[AccentPhrase]:
if text.startswith("japanese:"):
return self._ojt.analyze(text.removeprefix("japanese:"))
if text.startswith("kana:"):
return Kana().analyze(text.removeprefix("kana:"))
raise ValueError('expected "japanese:…" or "kana:…"')
text_analyzer = EngineTextAnalyzer(ojt)
phrases = text_analyzer.analyze("japanese:こんにちは")
phrases = text_analyzer.analyze("kana:コンニチワ") "prefix"じゃなくてJSONとかにしてもよいし、あるいはライブラリ側で合成用APIを用意してもよいと思います。 class TextAnalyzer(ABC):
@staticmethod
def composite(text_analyzers: dict[str, "TextAnalyzer"]) -> "TextAnalyzer":
return _rust.composite_text_analyzers(text_analyzers)
@abstractmethod
def analyze(self, text: str) -> list[AccentPhrase]:
... text_analyzer = TextAnalyzer.composite({"japanese": ojt, "kana": Kana()})
phrases = text_analyzer.analyze(json.dumps({"type": "japanese", "value": "こんにちは"}))
phrases = text_analyzer.analyze(json.dumps({"type": "kana", "value": "コンニチワ"})) |
なるほどです!! textは文字列だからとということで、jsonなりなんなりを入れる設計はかなり危ない気がちょっとしてます・・・! 色々考えたのですが、複数のTextAnalyzerを使う場合、今のSynthesizer相当のものを複数作ってもらうか、Synthesizer内で複数のTextAnalyzerを扱えるようにするかの二択になる気がしています。 どっちが良いかまだユースケースが出てきてなくてわからないので、一旦今の用途から考えると、OpenjtalkParaserなしでKanaParserを使うアプリがないのと、KanaParser側はオプショナルなので、SynthesizerはOpenjtalkParaserを受け取るか受け取らないかの2択だけで良さそうに思いました! ただ、
というのが実現できないですが・・・。 あと多分なのですが、Opnjtalk→JPreprocessの乗り換えは完全に互換性があると思っていて、スイッチングする必要がない(片方だけで良い)と思ってたりします。 |
内容
インターフェイス
TextAnalyzer
を導入し、PythonやJavaのパブリックAPIでSynthesizer<T extends TextAnalyzer>
のような形にします。TextAnalyzer
が取り得る型は次の通りです。Cなどの型引数の表現が難しい言語では、パブリックAPIとしては消去(erase)して単なる
Synthesizer
として扱います。Pros 良くなる点
_from_kana
系のAPIを統合でき、OpenJtalk
をSynthesizer<OpenJtalk> | Synthesizer<()>
として持つ #694 で話したような設計も悩まずにすむCons 悪くなる点
(text: string) => AccentPhrase[]
という処理にあてはまらない手法を導入するときに困る?実現方法
VOICEVOXのバージョン
N/A
OSの種類/ディストリ/バージョン
その他
The text was updated successfully, but these errors were encountered: