Skip to content
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

コードベースを大規模にリファクタリングし、ライブラリ (Python パッケージ) としてほかの Python コードから利用できるように改善 #92

Merged
merged 69 commits into from
Mar 11, 2024

Conversation

tsukumijima
Copy link

以前から Discord にてお伝えしている通りですが、Style-Bert-VITS2 のコードベースを大規模にリファクタリングし、ライブラリとしてほかの Python コードから利用できるようにしました。

同時に Style-Bert-VITS2 固有のディレクトリ構造に依存しているコードを修正し、ユーザーが style-bert-vits2 ライブラリの動作をコントローラブルに置けるように改良しました。

Note

一例を挙げると、従来のコードでは、モジュールをインポートした瞬間に AutoTokenizer.from_pretrained() が実行されてしまっていました。
従来のコードはただでさえハードコードされたパスに依存している上、インポートした瞬間に BERT モデル/トークナイザーがロードされてしまいます。この状況では、とても「コントローラブル」であるとは言えません。
特にモデルのロードは重たい処理ですから、ユーザーが明示的に、任意のタイミングでモデルをロードできる形が理想的です。

別途 pip install hatchHatch をインストールした上で hatch build を実行すると、dist/style_bert_vits2-(version)-py3-none-any.whl に wheel パッケージ (.whl) が生成されます。
この「ライブラリとしての」style-bert-vits2 を PyPI に公開するかどうかはお任せします。

Note

PyPI に公開せずとも、wheel がリリースに公開されていれば pip に wheel の URL を指定してライブラリをインストールできます。また GitHub の URL を git+https:// のスキームで指定してインストールすることも可能なはずです。

良くなること

  • 魑魅魍魎だったコードベースが大幅にクリーンアップされ、可読性・保守性・堅牢性が大幅に改善される
    • Type Hints が最大限活用されたモダンなコードになり、大幅に読みやすくなっているはず
    • もしかしたら Contribute が増えたりするかもしれない
  • Style-Bert-VITS2 のコア実装をライブラリとして切り出すことで、明確に関心/責務を分離する
    • Web UI や学習用コード、server_editor.py / server_fastapi.py から (ライブラリとして切り出された) コア API を呼び出す形になるため、今までどこがどう引っ付いてるのか分からなかった依存関係が明確化される
    • ライブラリには主に推論 (音声合成) に必要なコードのみが含まれている
    • ライブラリ内のコードでは Style-Bert-VITS2 固有のディレクトリ構造への依存を引き剥がし、ライブラリのユーザーが別のディレクトリに配置した BERT モデルや音声合成モデルで音声合成を行えるようにした
  • Style-Bert-VITS2 をライブラリ (Python パッケージ) として他の Python 製ソフトウェアから利用できるようになり、この優れた音声合成技術を多くのユースケースで活用できるようになる
    • たとえば LLM と組み合わせたアプリケーションなどが考えられる
    • 今までは server_fastapi.py などを別途起動の上で Python コードから HTTP API を叩く必要がありかなり面倒だったが、これからは (別途手元に音声合成モデルを用意した上で) 直接音声合成処理を呼び出せるようになり、敷居が大幅に下がる
    • ライブラリとしての style-bert-vits2 を組み込んだ CLI 音声合成ツールを作ったりも可能になる

悪くなること

  • 基本ない
    • 従来実装されていたすべての機能が引き続き動作するように慎重にリファクタリングした
      • よって既存のユーザーがこのリファクタリングによって不利益を被ることはないはず
    • もし何か動作しなくなっている場合は、私の意図しないバグ (リグレッション) が発生している
      • 少なくとも音声合成ができることは確認したが、時間の関係で学習が動作するかは試せていない
    • 強いて言えば、Type Hints の関係で明確に動作環境に Python 3.9 以上が前提になったこと
      • ただ Python 3.8 以下は流石に古すぎるし、メンテナンスの観点からも古い Python をいつまでもサポートし続けるのは非常に大変なので、私は問題ないと思う

リファクタリング方針

以下の方針でリファクタリングを行いました。

  • Style-Bert-VITS2 の推論機能のみをライブラリとして切り出す
    • Python パッケージ (ライブラリ) 化するには (pyproject.toml が配置されているディレクトリ)/(パッケージ名)/ 以下に当該パッケージのソースコードが配置されている必要がある
    • 従来のコードベースでは重要なコードの大半がリポジトリのルートディレクトリに雑多に置かれており、ライブラリ化するには最低でもコードをサブディレクトリ以下に移動する必要があった
    • Style-Bert-VITS2 には学習やデータセット作成、Web UI など他にも様々な機能があるが、ライブラリとしては音声合成 (推論) 機能以外は不要なため、ライブラリには含めない
  • リファクタリング範囲はライブラリとして style_bert_vits2/ 以下に切り出したコードのみとし、それ以外のコードは import の変更など最低限の変更にとどめる
    • 全コードを対象にすると、単純に規模が大きくなりすぎて収拾つかなくなるため
    • server_editor.py / server_fastapi.py などもリファクタリングの対象外
    • train_ms.py のようなクリティカルな影響を与える学習用コードはできるだけそのままにしておきたく…
    • とはいえ、リファクタリング後も全てのコードが正常動作するように配慮している (もし何かが動作しなくなっている場合は意図しない不具合)
  • リファクタリングは関数単位とし、内部の処理や全体の設計自体は変更しない
    • 私は Python にはそれなりに精通しているつもりだが機械学習に関する知見は全くないため、特にモデル定義周りのコードに関してはなぜこのような処理になっているのが全く理解できていない
    • 内部ロジックを全く理解できていないコードを新たなバグを生み出すことなくリファクタリングできるとは思えないため、極力ロジック自体は変更しない方針でリファクタした
    • 関数単位でモジュールや記述位置等々を移動したり並び替えて依存を分離するだけでも、十分可読性の高いコードにできると考えた (実際そうなった)
  • ライブラリとして利用する際に、ライブラリのユーザーが意図しない副作用が起こらないようにする
    • たとえば style_bert_vits2 パッケージをインポートしただけで意図せず BERT モデルがロードされてしまう、など
    • ライブラリである以上、その挙動はライブラリのユーザーがすべてコントロール可能な状態であるべき
    • Style-Bert-VITS2 固有のディレクトリ構造 (モデルの保存先パスなど) への依存が残っているとライブラリ化できないため、ライブラリ単体で動作する (BERT モデルなどの依存は外部から注入できる) ようにした

変更点

変更点が非常に多いため、一部書き出せていないものがあるかもしれません。

  • 肥大化していたモジュール (.py) 内の関数を別々のモジュールに切り分け、モジュール名を整理
    • 特に自然言語処理周りは重要な割にかなり雑多になっていたため、style_bert_vits2/nlp/ 以下にコードを集約し、さらに各言語固有の実装を style_bert_vits2/nlp/(chinese|english|japanese) に移動した
    • 音声合成モデルの構造や関連ユーティリティは style_bert_vits2/models/ 以下に集約した
    • モジュール名も一部よりわかりやすい名前に変更している
  • モジュール内の関数の記述を並び替え、よりコードリーディングがしやすいように改善
    • そのモジュール内でパブリックに公開され、使用頻度が高いと思われる関数ほど上に、そのモジュール内でしか使われない関数ほど下に記述されるようにし、ファイルを開いた際に重要なコードがどこかを把握しやすくした
  • 現在はどのコードからも呼び出されていない多数のデッドコードを削除した
    • これにはもはやメンテナンスされておらず動作しない、Bert-VITS2 本家の webui.py も含まれる
      • app.py の機能が充実しており、webui.py を Style-Bert-VITS2 で使えるように復活させる必要性はないと判断
    • これだけでも大幅にコードが削減され (特にユーティリティ関数) 、コードベースがスリム化された
  • 一部のクラス・メソッド・関数に Docstring (Google Style) を付与した
    • docstring があるのとないのではコードの読みやすさや、ライブラリ利用時の使い方の把握しやすさが大きく変わってくる
    • style_bert_vits2/models/ 以下にあるモデル構造が定義されているコードは、私が機械学習に全く精通しておらずコードの内容を全く理解できていないこと、非常に専門的な上に通常ライブラリユーザーは触らないことを鑑み、Docstring の付与を省略した
    • 一部 GPT-4 に docstring を書かせた箇所もある
      • 基本正しい内容に思えるが、前述の通り私がコードの内容を理解できているわけではないため、必ずしも正しいとは限らない
      • 当該コードの一番上には「GPT-4 で生成したコメントなのであくまで参考程度に」と記載してある
      • 不要であれば削除していただいて構わない
    • Docstring は個人的な好みで Google Style に統一した
      • もし他のスタイルの方が好みであれば全然修正していただいて構わない
    • 万全は期しているつもりだが、もし Docstring の内容が誤っている場合も気にせず修正していただければ
  • VSCode + Pylance による型チェックを導入の上で style_bert_vits2/ 以下のすべてのコードに型ヒントを追加し、型エラーを修正
    • Python の Type Hints は ML 界隈では残念ながらあまり活用されていないが、明確に関数の引数・戻り値に型を定義することで、潜在的なバグの多くを事前に検知できる
    • より堅牢でバグの少ないコードにし、さらに VSCode 上で補完が効くコードにするために、ライブラリとして切り出したすべてのコードに型ヒントを追加した
      • 型ヒントは Python 3.9 以上で動作する記述としている (このため | は使わず、敢えて Union, Optional を利用している)
    • 併せて追加した VSCode のワークスペース設定には Pylance (インテリジェントな Python コーディング支援プラグイン) の設定が含まれているため、VSCode (あるいは派生エディタ) であれば多分に型の恩恵を得られるはず
    • 発生していた型エラーのうち、恐らく利用ライブラリ側の型が誤っているもの、処理自体を変更しないとエラーを解消できないものに関しては、安全側に倒すために # type: ignore を付与して型エラーを抑制した
      • このため # type: ignore が付与されている行のコードの型安全性は保証されない
  • import 文の下と上下の関数・メソッドとの間に必ず2行空けるように変更
    • 従来のコードでは空行の行数がまちまちだったが、これを統一した
    • 2行分空けることで、より可読性が高まるはず
  • import 文の記述ルールを明確化し、概ねアルファベット順に並び替えた
    • まず import 文を「標準ライブラリ」「PyPI からインストールした外部ライブラリ」「内部ライブラリ」ごとにグループ化し、グループごとに1行空行を入れ、さらに上からアルファベット順で並び替えた
    • 具体例を下記に示す:
      import math
      from typing import Any, Optional
      
      import torch
      from torch import nn
      from torch.nn import Conv1d, Conv2d, ConvTranspose1d
      from torch.nn import functional as F
      from torch.nn.utils import remove_weight_norm, spectral_norm, weight_norm
      
      from style_bert_vits2.models import attentions
      from style_bert_vits2.models import commons
      from style_bert_vits2.models import modules
      from style_bert_vits2.models import monotonic_alignment
      from style_bert_vits2.nlp.symbols import NUM_LANGUAGES, NUM_TONES, SYMBOLS
      
  • モジュールやクラス内部でのみ利用される関数・メソッド・変数をプライベート化
    • 外部から利用されない関数・メソッド・変数はカプセル化され、外から呼び出し/変更できない状態であるべき
      • こうすることで特にライブラリ利用時、どの関数を使えば良いかが明確になる
    • Python では関数・メソッド・変数に __ の prefix をつけるとプライベートになる (参考)
  • 一見して分かりにくいと感じた命名のモジュール・関数・引数名をより分かりやすい名前に変更
    • 顕著な例では get_bert()extract_bert_feature() に変更したりなど
    • 私は比較的短く、実装されている機能を明確に示す命名を好む
    • この辺りは好みの差が大きいので、お気に召さないようであれば変更いただいて構わない
  • Hatch を導入し、ライブラリとして style_bert_vits2/ 以下に切り出した部分を Python パッケージ (wheel) にビルドできるようにした
    • Hatch の設定は pyproject.toml に記載されている
    • hatch run test:test を実行すると自動的に専用の仮想環境が構築され、Python 3.9 〜 3.11 すべてでライブラリが動作するかをテストできる
      • 別途 OS に Python 3.9 〜 3.11 がインストールされている必要がある
      • テスト実行時に生成された音声データは tests/wavs/ に保存される
  • config.json からロードしたハイパーパラメータの管理に Pydantic を導入し、型の恩恵を得られるように改善
    • Pydantic は FastAPI のバリデーションで使われているが、FastAPI 以外でも利用できる
    • 今までは本当にこのプロパティが存在しているか分からない中コードを書かなければならなかったが、utils.HParams を Pydantic モデル (HyperParameters) に移行したことで、特定の型のプロパティが存在することが保証されるようになった
    • これにより型の恩恵を受けられるし、補完でプロパティが出てくるためより安心して変更できるようになる
    • utils.HParams とデータ構造はほぼ変わらないため、基本 drop-in で置き換えできた
  • 推論時に必要となる各言語ごとの BERT モデルを、ライブラリのユーザーが明示的にロード/アンロードできるように改善
    • Style-Bert-VITS2 内部から呼び出す際はデフォルト値として bert/ 以下に保存されているモデルが利用される
    • ライブラリとして外部からロードする際は、別途用意した BERT モデルのディレクトリのパスか、Hugging Face のリポジトリ名を明示的に指定してロードする必要がある
      • 元々 (Style-)Bert-VITS2 が bert/ フォルダに BERT のモデルを保存する構成なのも、中国だと Hugging Face へのアクセスが制限されており openi という中国国内のミラーからダウンロードする必要があったからのはず
      • 基本的には利便性から Hugging Face のリポジトリ名を直接指定することになると思う (この場合 Hugging Face から DL されたモデルは transformers ライブラリの既定のキャッシュディレクトリにキャッシュされる)
    • 一度ロードしたモデルはアンロードするまでメモリ上に保持される設計 (Store パターン) のため、事前にロードしておけば、以降の処理でロードに時間がかかることなく、スムーズに推論を行える
  • ライブラリ利用時、明示的に呼び出さない限り pyopenjtalk_worker を起動せずに通常の pyopenjtalk を利用するように変更
    • pyopenjtalk_worker は Style-Bert-VITS2 内部で単一の辞書を複数プロセスで共有するために実装されたものだが、ライブラリ利用時は複数プロセスで辞書にアクセスできる必要はないかもしれない
      • たとえばライブラリを利用して独自の FastAPI + Uvicorn サーバーを建てたいだけなら、シングルプロセスなので辞書の競合状態は発生しない
    • むしろ PyInstaller でライブラリとしての style-bert-vits2 を含めて exe 化する場合、pyopenjtalk_worker は『Python インタプリタ上で実行されていることを前提に』subprocess.Popen() + python -m で別プロセスで pyopenjtalk サーバーを起動する設計のため、正常動作しないことが容易に予想される
    • これらの事情から、pyopenjtalk_worker.initialize()pyopenjtalk_worker.initialize_worker() にリネームした上で、明示的に pyopenjtalk_worker.initialize() を実行しなかった場合は通常の pyopenjtalk にフォールバックするように変更した
      • app.py (webui/inference.py) / server_editor.py / server_fastapi.pyには pyopenjtalk_worker.initialize_worker() を記述してあるため、今まで通り複数プロセスで辞書を共有できる
      • この変更の兼ね合いで、モジュールインポート時に自動で pyopenjtalk サーバーが起動するようになっていた部分を削除した
        • 前述の通り、副作用のある処理は暗黙的に実行されるべきではない
  • コードベース内の言語表現を文字列 (JP, EN, ZH) ではなく Languages の StrEnum で管理するように変更
    • 元々 server_editor.py ではこの Enum が使われていたが、大半のコードでは未だ文字列ベースでの言語判定になっていた
    • 文字列だと不正な値が入力される可能性もあるし、Enum の方がより安全で堅牢になる
      • ライブラリ利用時にどの値を指定すれば良いかが明確になるメリットもある
    • ただし Web UI などで一部文字列指定の箇所が残っているため、StrEnum にして旧来との互換性を維持した
  • VSCode のワークスペース設定ファイル (.vscode/settings.json)を追加
    • Pylance は型エラーをどこまでエラーにするか、警告にするかをワークスペースごとに変更できる
    • 今回のリファクタリングはこの .vscode/settings.json に記載の Pylance 設定にて行っているため、今後コードを修正していく際も Pylance の型チェックの恩恵が得られるようにする
    • また、VSCode のワークスペースごとの推奨拡張機能 (vscode/extensions.json) に Python と Pylance を追加した
  • app.py に --host / --port オプションを追加
    • ライブラリ化とは関係ないが、メイン PC と異なる PC で動かしている場合に、ローカルネットワーク上の他デバイスから Web UI にアクセスできるようにするため
    • --host 0.0.0.0 を指定するとすべてのネットワークインターフェイスで HTTP サーバーがリッスンされる

変更が非常に大規模なこともあり、さすがにこのままフォークを維持していくのは正直かなりつらいです…。
コーディングスタイルが気に入らない箇所などあれば後で遠慮せず修正していただいて構いませんので、まずはマージしていただけると非常に助かります。

今回の変更で何か不明な点があれば、コメントいただければ回答いたします。
お手数おかけしますが、何卒よろしくお願いいたします。

…from server_editor.py

The logic has not been changed, only renaming, splitting and moving modules on a per-function basis.
Existing code will be left in place for the time being to avoid breaking the training code, which is not subject to refactoring this time.
…OX to style_bert_vits2/text_processing/japanese/user_dict/
…tyle_bert_vits2/models/

The code has not yet been cleaned up, just moved.
… loaded BERT models/tokenizer and replace all from_pretrained() to load_model/load_tokenizer
…each language to style_bert_vits2/text_processing/(language)/bert_feature.py
… style_bert_vits2/text_processing/__init__.py

This was often used in 3 function sets and felt like a wasteful division with few lines.
…VITS2

Since app.py and server_editor.py already exist as alternative Web UI, there is no need to revive webui.py in the future.
I have determined that this is excessive for this project at this time.
"text_processing" is clearer, but the import statement is longer.
"nlp" is shorter and makes it clear that it is natural language processing.
pyopenjtalk_worker.initialize() has the side effect of starting another process and should not be executed automatically on import.
…ontinue processing without a worker

When using style-bert-vits2 as a library, the requirement to be able to launch it in multiple processes may not be necessary. Also, if the library is embedded and exe-ed using PyInstaller or similar, it is difficult to make pyopenjtalk_worker run in a separate process.
Therefore, we changed it so that the worker is used only when it is explicitly initialized.
… clarification and add comments to each method
By executing "hatch run test:test", you can check whether the test passes in all Python 3.9 to 3.12 environments.
Style-Bert-VITS2 has been reported to not work with some PyTorch 2.2 series, but Python 3.12 is only supported in Torch >= 2.2, so Python 3.12 support is not provided for the time being
Enabling type checking with Pylance.
BERT models and tokenizers are already stored and managed in the bert_models module and should not be stored here.
In addition, since there may be situations where the user would like to use cpu instead of mps for inference when using it as a library, the automatic switching process to mps was removed.
The Pydantic models in the library are written for Pydantic v2 and will not work with Pydantic v1.
Prebuilt wheels for almost every OS/architecture (except musl) are now available on PyPI, eliminating the need for a build environment.
ref: https://pypi.org/project/pyworld-prebuilt/
Pydantic models are more robust and properties can be accessed by dots.
@litagin02 litagin02 changed the base branch from dev to dev-refactor March 11, 2024 00:09
@litagin02 litagin02 merged commit 03d4b4c into litagin02:dev-refactor Mar 11, 2024
@litagin02
Copy link
Owner

大規模なリファクタリングありがとうございます。
一旦dev-refactorブランチにマージします。
これから確認しますが、疑問点等があったらここでお聞きすると思いますのでよろしくおねがいします。

@litagin02 litagin02 mentioned this pull request Mar 11, 2024
@litagin02
Copy link
Owner

litagin02 commented Mar 11, 2024

#93 にて修正等をしていきますが、とりあえずこのリファクタリングに直結する修正等はこちらで逐次報告していくことにします(認識の違いがあると後々また面倒になりそうなので)(その他の不具合修正等はこちらで行います)。
何かあれば意見をください。

フォーマットやスタイルについて。いろいろ考えるのが面倒なので、black formatterのデフォルト設定にすべて合わせることにします(特に文字列のクォーテーションがダブルなことやデフォルト引数時の空白無しや最大文字数や空行等)

importスタイルをisort --profile black --gitignore --lai 2 .で機械的に修正

@kale4eat
Copy link

kale4eat commented Mar 11, 2024

@tsukumijima
ありがとうございます。お疲れ様です。

僭越ながら、私が携わったopenjtalkの別プロセス化について
気になったところだけ、コメントさせていただきます。

pyopenjtalk_worker は Style-Bert-VITS2 内部で単一の辞書を複数プロセスで共有するために実装されたものだが、ライブラリ利用時は複数プロセスで辞書にアクセスできる必要はないかもしれない
たとえばライブラリを利用して独自の FastAPI + Uvicorn サーバーを建てたいだけなら、シングルプロセスなので辞書の競合状態は発生しない

むしろ PyInstaller でライブラリとしての style-bert-vits2 を含めて exe 化する場合、pyopenjtalk_worker は『Python インタプリタ上で実行されていることを前提に』subprocess.Popen() + python -m で別プロセスで pyopenjtalk サーバーを起動する設計のため、正常動作しないことが容易に予想される

おっしゃっている通り、pyopenjtalk_worker はあくまで
Style-Bert-VITS2というリポジトリを使用する際の問題を解決するために書いたコードです。
そのため、とりしまさんの望まれる、
「ほかの Python コードから利用できる」とは相性が悪いのではないかと
気になっていました。
これは今後の修正や機能追加の方針にも関わってくるかと思います。
このリポジトリ特有の問題を、このライブラリの利用性を下げずに修正できるかとうことです。
コード的にもこのことが複雑さを持ち込んでしまっているように感じます。
PyInstaller exe 化のようなケースまで考慮すると、ますます難しいように思えます。

このあたり、うまく分離できると良さそうという直観だけあります。
何かアイデアはありませんか?
イメージとしてはリポジトリ特有の問題に関するところは差し替え、オーバーライド、フォークといった方向性です。
あるいはpyopenjtalk_workerを別リポジトリとして
私がブラッシュアップして、使っていただいた方がいいのかも......?

いずれにしても、とりしまさんが期待するライブラリとして
どこまでサポートするかを明確にしていただく必要はあるかもしれません。

補足

この変更の兼ね合いで、モジュールインポート時に自動で pyopenjtalk サーバーが起動するようになっていた部分を削除した
前述の通り、副作用のある処理は暗黙的に実行されるべきではない

旧 user_dictの__init__.pyやjapanese.pyでpyopenjtalk_workerのinitializeを呼んでいたこと、
そして今回のstyle-bert-vits2ライブラリをimportする視点で
述べられているのだと捉えています。
一般的にも副作用を避けるのは正しいと思います。
しかし、大規模なコード変更を避けるために
やむを得なくというのもありますので
そこだけよろしくお願いいたします。

私自身、こういったオープンソース開発はまだ不慣れなので
仕様をどこまで考えて、レビューもどこまでなされるものなのか、
いまいち感覚がつかめておりません。

せっかく大変なコード変更をしていただいても、
受け入れられるか、ご負担になってしまわないかは心配なところです。

@litagin02
Copy link
Owner

@tsukumijima

hatch run test:test を実行すると自動的に専用の仮想環境が構築され、Python 3.9 〜 3.11 すべてでライブラリが動作するかをテストできる

すみません、自分が初めてhatchを知って使い方が分かっていないのはあると思いますが、自分のWindowsのCUDA環境では、環境が作られる際にtorchがおそらくCPU版が入ってしまい、

FAILED tests/test_main.py::test_synthesize_cuda - AssertionError: Torch not compiled with CUDA enabled

となり、test_synthesize_cuda()でのグラボ使用での合成テストが通りません。これは意図した挙動でしょうか、もしくはpyproject.tomlを適切に変える必要があるのでしょうか。

@kale4eat
Copy link

kale4eat commented Mar 11, 2024

#92 (comment) に関して続き

ご判断されるのはlitaginさんであり、私は通りすがりではありますが
誰もが楽になる、より良い道があれば模索したいと考えております。

  • リファクタリング
  • ライブラリとしての使用

を同時に行うのは根本的に難しいというのを
前提した方がいいかもしれません。

例えば

  1. ライブラリとして別リポジトリにまとめる
  2. これをサブモジュールとして使う
  3. 適宜、このリポジトリの問題解決のため修正

またあるいは、

  • Style-Bert-VITS2リポジトリとしてのコード
  • ほかのPythonコードから利用できるライブラリとしてのコード

はブランチで区別というのもあるかと思います。
必ずしも1つのコードで両対応する必要はないと気づきました。

@litagin02
Copy link
Owner

@tsukumijima
現在はユーザー辞書の適応update_dict()server_editor.pyのみにされていますが、前処理時やapp.py等の、日本語処理が行われるすべての箇所で辞書を適応する必要があるため、style_bert_vits2/nlp/japanese/g2p.pyのトップレベルにupdate_dict()を書こうと思いますが、この方針は問題ないでしょうか。

@litagin02
Copy link
Owner

litagin02 commented Mar 11, 2024

以下のようなエラーで、JP-Extraでない通常版の学習時にエラーが出る模様です。原因を調査中です(hpsあたりがJP-Extraを想定して書かれているのが怪しそうだけどまだ分からない)

03-11 17:41:52 |  INFO  | train_ms.py:446 | Start training.
  0%|                                                                                                                  | 0/750 [00:00<?, ?it/s]Traceback (most recent call last):
  File "C:\Users\username\Documents\github\Bert-VITS2-litagin\train_ms.py", line 968, in <module>
    run()
  File "C:\Users\username\Documents\github\Bert-VITS2-litagin\train_ms.py", line 463, in run
    train_and_evaluate(
  File "C:\Users\username\Documents\github\Bert-VITS2-litagin\train_ms.py", line 588, in train_and_evaluate
    for batch_idx, (
  File "C:\Users\username\Documents\github\Bert-VITS2-litagin\venv\lib\site-packages\torch\utils\data\dataloader.py", line 630, in __next__        
    data = self._next_data()
  File "C:\Users\username\Documents\github\Bert-VITS2-litagin\venv\lib\site-packages\torch\utils\data\dataloader.py", line 1345, in _next_data     
    return self._process_data(data)
  File "C:\Users\username\Documents\github\Bert-VITS2-litagin\venv\lib\site-packages\torch\utils\data\dataloader.py", line 1371, in _process_data  
    data.reraise()
  File "C:\Users\username\Documents\github\Bert-VITS2-litagin\venv\lib\site-packages\torch\_utils.py", line 694, in reraise
    raise exception
IndexError: Caught IndexError in DataLoader worker process 0.
Original Traceback (most recent call last):
  File "C:\Users\username\Documents\github\Bert-VITS2-litagin\venv\lib\site-packages\torch\utils\data\_utils\worker.py", line 308, in _worker_loop 
    data = fetcher.fetch(index)
  File "C:\Users\username\Documents\github\Bert-VITS2-litagin\venv\lib\site-packages\torch\utils\data\_utils\fetch.py", line 54, in fetch
    return self.collate_fn(data)
  File "C:\Users\username\Documents\github\Bert-VITS2-litagin\data_utils.py", line 286, in __call__
    ja_bert_padded[i, :, : ja_bert.size(1)] = ja_bert
IndexError: Dimension out of range (expected to be in range of [-1, 0], but got 1)

デフォルトのconfigs/config.jsondata.use_jp_extraの記載がなく、デフォルト値に指定されていたTrueが代入されていたことが原因でしたので、jsonファイルを修正して解決しました。
b3cc705

@tsukumijima
Copy link
Author

@litagin02

すみません、自分が初めてhatchを知って使い方が分かっていないのはあると思いますが、自分のWindowsのCUDA環境では、環境が作られる際にtorchがおそらくCPU版が入ってしまい、test_synthesize_cuda()でのグラボ使用での合成テストが通りません。これは意図した挙動でしょうか、もしくはpyproject.tomlを適切に変える必要があるのでしょうか。

私は Linux でしか試していないのですが盲点でした…。
意図しない挙動ですが、これを修正するのは難しそうです。

image

hatch は仮想環境作成時、pyproject.toml に記載された依存関係を自動的にインストールします。
しかし PyTorch には CUDA 対応版と CPU 版など複数のバリエーションがあり、非常にややこしくなっています…。

私も完全には理解していないのですが、https://pytorch.org/get-started/locally/ を見る限り上記のパターンとなっているようです。

…長くなりましたが、要は Linux ではデフォルトで CUDA 対応版 torch がインストールされるが、Windows ではデフォルトで CPU 対応版 torch がインストールされてしまうことが原因と思われます。

しかも、Poetry などと異なり、pyproject.toml には現状プラットフォームや環境ごとに個別に依存関係を細かく指定することができません (参考) 。
よって、現状 Windows 環境で CUDA での音声合成テストを動かすのは厳しいと言わざるを得ません。

Windows 環境メインで開発されているとのことなので、支障するようであれば一旦コメントアウトしておくのが一番手っ取り早いかと思います。

@tsukumijima
Copy link
Author

@litagin02

フォーマットやスタイルについて。いろいろ考えるのが面倒なので、black formatterのデフォルト設定にすべて合わせることにします(特に文字列のクォーテーションがダブルなことやデフォルト引数時の空白無しや最大文字数や空行等)

一旦個人的な好みに合わせて書いてしまいましたが、フォーマットについて考えるのが面倒であればデファクトである black に任せてしまうのも十分アリだと思います。
Ruff を使うと linter と black 完全互換の formatter を両方一発で動かせるので、余裕があれば入れてみるといいかもしれません (Ruff のプロジェクトごとの設定値を pyproject.toml にまとめて記述できます) 。

@kale4eat

このあたり、うまく分離できると良さそうという直観だけあります。
何かアイデアはありませんか?
イメージとしてはリポジトリ特有の問題に関するところは差し替え、オーバーライド、フォークといった方向性です。
あるいはpyopenjtalk_workerを別リポジトリとして
私がブラッシュアップして、使っていただいた方がいいのかも......?

image

この点私もどうするか悩みましたが、現状のコードでは pyopenjtalk_worker.initialize_worker() を呼ばなければ、ワーカープロセスを起動せずに通常通り pyopenjtalk が同一プロセスで呼ばれるようになっています (このとき、pyopenjtalk_worker は単なる pyopenjtalk のラッパーになる) 。
ref: https://github.com/litagin02/Style-Bert-VITS2/blob/dev-refactor/style_bert_vits2/nlp/japanese/pyopenjtalk_worker/__init__.py#L20-L71

現時点で app.py や server_editor.py などの Style-Bert-VITS2 内部のサーバーでは起動前に pyopenjtalk_worker.initialize_worker() を呼び、ライブラリ利用時はあえて呼ばないことで、pyopenjtalk_worker を起動するかを切り替えられる状況です。
ですのでライブラリ化する上で特段支障するとは考えていませんが、どうお考えになりますか?

旧 user_dictの__init__.pyやjapanese.pyでpyopenjtalk_workerのinitializeを呼んでいたこと、
そして今回のstyle-bert-vits2ライブラリをimportする視点で
述べられているのだと捉えています。
一般的にも副作用を避けるのは正しいと思います。
しかし、大規模なコード変更を避けるために
やむを得なくというのもありますので
そこだけよろしくお願いいたします。

私も大規模な変更を避けるためにあえてこの実装にしたのだろうという理解でした。納得です。
ただライブラリ化する上ではやはり副作用があると利用に支障するため、できるだけ暗黙的な副作用を避け、明示的に副作用がある処理をコントロールできるようにすべくリファクタリングを行った次第です。

例えば

  1. ライブラリとして別リポジトリにまとめる
  2. これをサブモジュールとして使う
  3. 適宜、このリポジトリの問題解決のため修正

またあるいは、

  • Style-Bert-VITS2リポジトリとしてのコード
  • ほかのPythonコードから利用できるライブラリとしてのコード

はブランチで区別というのもあるかと思います。
必ずしも1つのコードで両対応する必要はないと気づきました。

実際私もライブラリ用のコードを切り出して別リポジトリで公開しようかとも考えました。ただやはり二重管理になりますし、変更への追従に多大な手間がかかる課題があります(そして面倒になって放棄されうる)。

今回の pyopenjtalk_worker の件に関して言えば、上記方法でライブラリとしての style-bert-vits2 と単独ソフトとしての Style-Bert-VITS2 は共存可能ですし、1つのコードに統合するメリットの方がはるかに大きいと考えます。

@tsukumijima
Copy link
Author

@tsukumijima

現在はユーザー辞書の適応 update_dict() が server_editor.py のみにされていますが、前処理時や app.py 等の、日本語処理が行われるすべての箇所で辞書を適応する必要があるため、style_bert_vits2/nlp/japanese/g2p.py のトップレベルに update_dict() を書こうと思いますが、この方針は問題ないでしょうか。

あーそこも盲点でした…。
あまり pyopenjtalk の仕様を理解できていないのですが、同一プロセス中で今後呼び出される pyopenjtalk API のレスポンス結果にローカルの辞書データを適用/反映するには、事前に必ず update_dict() を実行しておく必要があるという理解です。

ただ、g2p.py への update_dict() のハードコードはやめてほしいです。
というのも update_dict() は引数指定を省略した場合にデフォルトの辞書ファイルのパス (dict_data/ 以下) から辞書データをロードしますが、そのデフォルトのパスは Style-Bert-VITS2 のディレクトリ構造固有です。
このため、update_dict() をハードコードしてしまうと、ライブラリ利用時に当該モジュールを import しただけでクラッシュしてしまいます。

すべてのエントリーポイントとなるスクリプトに update_dict() を記載するのが冗長だと感じるのであれば、ライブラリ「外」に下記のような nlp_wrapper.py を作成し、

from style_bert_vits2.nlp.japanese import pyopenjtalk_worker
from style_bert_vits2.nlp.japanese.user_dict import update_dict

# ライブラリとしての style-bert-vits2 のうち、自然言語処理関連のモジュールをすべてインポート
from style_bert_vits2.nlp import *  # type: ignore


# pyopenjtalk_worker を起動
pyopenjtalk_worker.initialize_worker()

# dict_data/ 以下の辞書データを pyopenjtalk に適用
update_dict()

… server_editor.py などで from style_bert_vits2.nlp import ...from nlp_wrapper import ... に書き換えるといった手もあります。

このようにすれば、Style-Bert-VITS2 内部から自然言語処理関連モジュールを import すると自動的に pyopenjtalk_worker が起動され、デフォルト辞書データパスから辞書データが適用されます。
一方ライブラリ利用時は辞書データをどのタイミングでロードするか、pyopenjtalk_worker を利用するかをライブラリのユーザーが選択できるようになるはずです。

@kale4eat
Copy link

@tsukumijima

大変、ご丁寧なご回答、ありがとうございます。
問題ございません。
以下、補足と謝罪です。

ですのでライブラリ化する上で特段支障するとは考えていませんが、どうお考えになりますか?

実際私もライブラリ用のコードを切り出して別リポジトリで公開しようかとも考えました。ただやはり二重管理になりますし、変更への追従に多大な手間がかかる課題があります(そして面倒になって放棄されうる)。

今回の pyopenjtalk_worker の件に関して言えば、上記方法でライブラリとしての style-bert-vits2 と単独ソフトとしての Style-Bert-VITS2 は共存可能ですし、1つのコードに統合するメリットの方がはるかに大きいと考えます。

大変なリファクタリングをしてまでライブラリ化するにあたっては、
一体どこまでのユースケースに耐えうるようにされたいのか、
つかめないところがありました。

initialize_workerの呼び出しによるスイッチの許容や
コード管理の手間などのバランスを考慮して
当のとりしまさんがジャッジされたのであれば大丈夫です。
(pyopenjtalk_worker は出来るなら上手く分離したいという気持ちが先行していました。)

現状のコードからも明らかなように
副作用や再利用性については、
これまで携わった人は誰も想定していない部分があるかと思います。

このリポジトリ特有の問題を、このライブラリの利用性を下げずに修正できるかとうことです。

と言及しましたように、似たようなことが起こらないかだけ不安ですが、
リファクタリングによって修正やテストが容易になると期待します。

しかし、大規模なコード変更を避けるために
やむを得なくというのもありますので

こちらは、言い訳がましくてすみませんでした。
大規模なコード変更やリファクタリングがしたくても認められなかった
過去の経験から来る悔しさが出てしまったかもしれません。
何卒ご容赦ください。

おそらくオープンソース開発はケースバイケースな
フットワークが求められるため、
固定観念を捨てる必要があるようですね。

@litagin02
Copy link
Owner

@tsukumijima
ありがとうございます、勉強になります。

Hatch を導入し、ライブラリとして style_bert_vits2/ 以下に切り出した部分を Python パッケージ (wheel) にビルドできるようにした

ここの箇所で、ライブラリという概念や通常の使用方法について曖昧にしか知らないのでお聞きしたいのですが、今の状態でhatch buildをすると、ルートディレクトリにある全てのコード(や大容量モデルファイル等含めて)がsdistに入ります。
おそらくBERTモデルや事前学習済みモデルはsdistから除外する設定をすればいいのでしょうが、他の学習用コード等のstyle_bert_vits2/に無いコードについての扱いをどうするのかお聞きしたいです。

また具体的にライブラリとして公開した場合は、どのような使われ方・インストール方法・ユースケースを想定しているのでしょうか?
実際に音声合成を使用するにはBERTモデル等のダウンロードが必要になるかと思われますが、現在はそれをルートのinitialize.pyで行っているため、対応するコードをstyle_bert_vits2/内に何らかの形でおいでおくのがよいのでしょうか?

@tsukumijima
Copy link
Author

tsukumijima commented Mar 12, 2024

@litagin02

ここの箇所で、ライブラリという概念や通常の使用方法について曖昧にしか知らないのでお聞きしたいのですが、今の状態でhatch buildをすると、ルートディレクトリにある全てのコード(や大容量モデルファイル等含めて)がsdistに入ります。
おそらくBERTモデルや事前学習済みモデルはsdistから除外する設定をすればいいのでしょうが、

すみません、ここも盲点でした… (確かに手元でビルドした sdist は 3.1GB くらいになっちゃってました) 。
私も Hatch を使い込んでいるわけではないのであれですが、公式ドキュメント に一通りビルド時の除外ファイルの設定が載っていますので、参考にしていただければと思います。

他の学習用コード等のstyle_bert_vits2/に無いコードについての扱いをどうするのかお聞きしたいです。

ライブラリとしての SBV2 は style_bert_vits2 ディレクトリ外のファイルには依存していないので、 基本 .gitignore・LICENSE・README.md・pyproject.toml・style_bert_vits2/ 以下だけ含めるように設定すれば良さそうです。

また具体的にライブラリとして公開した場合は、どのような使われ方・インストール方法・ユースケースを想定しているのでしょうか?

以前のコメントで書いた通りですが、使われ方/ユースケースは LLM と組み合わせりした GUI アプリや、独自に実装した音声合成 API サーバーなどが考えられます。
(実際個人的にそういう系のソフトを開発中でして、ライブラリ化を試みた次第です。)

インストール方法は pip や Poetry になります。
litagin さんがライブラリとしての SBV2 を PyPI に公開されるのであればそこからインストールしますし、PyPI に公開されないのであれば Git や Releases に公開されている wheel の URL を指定してインストールすることになると思います。

実際に音声合成を使用するにはBERTモデル等のダウンロードが必要になるかと思われますが、現在はそれをルートのinitialize.pyで行っているため、対応するコードをstyle_bert_vits2/内に何らかの形でおいでおくのがよいのでしょうか?

bert_models.py の load_model/load_tokenizer() をライブラリユーザーが事前に呼び出す形を想定しています。
load_model/load_tokenizer() には BERT モデルの保存先パスか、Hugging Face のリポジトリ名を指定できます (重要) 。

このため、ライブラリとしての SBV2 側で新たに配慮いただく必要はありません。

Note

bert_models.py の内部で使われている transformers ライブラリの from_pretrained() は、むしろ Hugging Face のリポジトリ名を指定する使い方のほうが一般的で、(Style-)Bert-VITS2 の実装手法はむしろかなりイレギュラーだと思います。
この記事公式ドキュメント の通り、from_pretrained() でダウンロードされたモデルはキャッシュされます。

Note

Bert-VITS2 でモデルをわざわざローカルに保存している (ついでにハイパーパラメータなどをリポジトリ内に含めている) のも、中国国内からだと Hugging Face へのアクセスが非常に遅い or 金盾で規制されている事が理由だったはずです。

なので日本国内で使う分には、bert/ フォルダや slm/ フォルダを削除して from_pretrained() にローカルのパスではなく Hugging Face のリポジトリ名を渡す形にしてしまっても全く問題ないと思います。(キャッシュディレクトリを統一したければ from_pretrained() に cache_dir を指定すれば良い)


image

Important

ここまで書いて、bert_models.load_model/load_tokenizer() に、内部で呼び出す from_pretrained() に渡す用の cache_dirrevision 引数を実装し忘れていることに気づきました。
cache_dir は from_pretrained() でのモデル自動ダウンロード時のキャッシュディレクトリの変更に、revision はモデルの Git コミットハッシュを固定するためにそれぞれ必要です。
お手数ですが実装していただけないでしょうか…? (ご面倒であればプルリク出します)


PS: 上記問題を修正したプルリクエストを出させていただきました。マージしていただければ幸いです。
#95

litagin02 added a commit that referenced this pull request Mar 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants