-
Notifications
You must be signed in to change notification settings - Fork 206
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
windowsでエンジンの多重起動を可能にする(openjtalkによるエラーを出なくする) #1347
Comments
@takana-v @sabonerune ちょっとメンションすみません!! |
FastAPIにteardown的なフックってないんですかね?それで消すとかどうでしょう。(AIVoiceVoxだとctrl-cのフックで後処理をしていてちゃんと動いているので、多分FastAPIのteardownも呼ばれるはず) |
https://fastapi.tiangolo.com/advanced/events/#lifespan voicevox_engine/voicevox_engine/app/application.py Lines 54 to 57 in 81a9360
lifespanで終了時のクリーンアップができます( しかしエディタは 起動時にコンパイル済みの辞書を書き込むディレクトリを空にするという手もあると思います。 #620 (comment)
|
@sevenc-nanashi それプラスtmpディレクトリに保存する形であればまあ残っちゃっても良いかって感じなんですが、次は環境によってはtmpディレクトリへのファイルmvができずatomic操作ができないという問題が・・・・・・・・ あれ、もしかしてatomic操作いらなくなった????
自分もこれ考えたんですが、わざと2つ起動する人とかもいる気がするんですよね〜・・・。 |
今のとこまとめると、こう? 今のmainの実装 OSErrorを握りつぶす場合 コンパイル済み辞書の名前をN個用意し、1つずつ使えるか試していく コンパイル済み辞書の名前をランダムにし、teardownで消す コンパイル済み辞書の名前をランダムにし、エンジン起動時に全ファイル消す コンパイル済み辞書の名前をランダムにし、tmpディレクトリに保存して、teardownで消す |
Linuxの場合開いているファイルを削除しても問題ないのではと思います。 |
@sabonerune まあ大丈夫かもなのですが、あんまりOSやライブラリやファイルシステムの(隠れ)仕様に頼った実装にしない方が良いかなぁと。 とはいえエディタ側にsignal実装もちょっと大変そうではあるんですよねぇ。。 うーーーーーーーーーん。ラウンドロビンもどきとかぁ・・・? |
これ本当です?AIVoiceVoxのteardown(ctrl-cハンドル)はちゃんと動いてるので動くと思いますが
tree-killってシグナル指定できませんでしたっけ?一回SIGINT送って10秒後にSIGKILLみたいなことできそうな気が。 |
@sevenc-nanashi とりあえずコード追っかけてみました。 エディタ側では なのでwindowsなら即強制終了・・・という理解で合ってるはずなんですが、まだ未知ななにかがあるかもですねぇ。 |
なるほどー。 となるとこれを再Openしてもいいかも? |
@sevenc-nanashi VOICEVOX/voicevox#1540 は・・・うーん、1つのWebAPIとしては強力すぎると思うんですよね・・・。 |
とりあえず候補が決まってない状況なのが一番もったいない気がしました! まず現状、エディタはどうやらSIGTERMをエンジンに送っているっぽいことがわかっているためです。 あとこの方法であれば、最悪問題がほぼ無いと思います。 設計としてはこうでしょうか?
|
#1248 (comment) POSIXの方はTreeKillのシグナルを |
@sabonerune ここでSIGINTもTERMもBREAKも同じ用に
実際linux環境でSIGTERM(たぶんあたい何もなくkillすればよいだけ)してuvicornがgraceful shutdownしてそうか確かめると良さそう? |
@Hiroshiba ただ、 |
以前の話だとなぜか呼ばれたという感じだった記憶がありますが、まあ、呼ばれない前提で考えるのが良さそう。 そうなると #1347 (comment) の方法ではtmpに辞書が残り続けてしまう前提で良さそう。流石にそれは微妙そうに感じます。 とりあえずこのissueの解決策としては、ワークアラウンドですが↓の方法が良さそうかなと思いました。
候補のファイルを5つくらい持っておいて、順に開いていって開けなかったら次のを試す、という感じをイメージしています。 別件で、正常終了させる方法はほんとに難しいですね。。。 |
本 Issue は直近 180 日間で活動がありません。今後の方針について VOICEVOX チームによる再検討がおこなわれる予定です。 |
VST版で多重起動したい需要があるので優先度を上げたいです。 |
普通にエディタ開発するときとかも不便なので、優先度上げましょうか!!! これ今更ながら最良の方法を思いつきました。 ダメそうな提案「辞書でハッシュを取ってファイル名に使用し、辞書内容が変わるタイミングで前の辞書のハッシュと同じファイル名があれば消す」で良い気がしました。 流れ:
|
数ヶ月経って今の感想ですが、やっぱり
のワークアラウンド手法を使うのが良さそうな気がしました。 ということで実装者募集中です!! そんなに難しくないと思うのでぜひぜひ!! |
Pythonで作成した voicevox_engine/voicevox_engine/user_dict/user_dict_manager.py Lines 160 to 164 in 1db9ede
- # コンパイル済み辞書の置き換え・読み込み
- pyopenjtalk.unset_user_dict()
- tmp_compiled_path.replace(compiled_dict_path)
- if compiled_dict_path.is_file():
- pyopenjtalk.set_user_dict(str(compiled_dict_path.resolve(strict=True)))
+ # コンパイル済み辞書の内容を一時ファイルに移し替える
+ with NamedTemporaryFile() as t:
+ t.write(tmp_compiled_path.read_bytes())
+ t.flush()
+
+ # コンパイル済み辞書の置き換え・読み込み
+ pyopenjtalk.unset_user_dict()
+ pyopenjtalk.set_user_dict(t.name) この方法をWindowsで動作させるためにはOpenJTalkにも変更が必要です。 - hFile = ::CreateFileW(WPATH(filename), mode1, FILE_SHARE_READ, 0,
+ hFile = ::CreateFileW(WPATH(filename), mode1, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0, 一応動作確認ができたので案の一つとして報告です。 |
@sabonerune なるほどです!! ちょっと考えたのですが、もしその方法適用するのであればopenjtalk側のいじった場所は問題が発生しないと自信が得られるまで掘ったほうが良さそうだと思いました! というのも、例えばそのopenjtalkのファイルを書き換えているところですが、hFileはdeleteされても大丈夫なのでしょうか・・・? ということで、Pythonの自信が持てる範囲でワークアラウンドを適用ほうが良さそうかなと思いました!! |
新しくいくつか手を考えてみました。
|
pyopenjtalkの Mmapクラスは ただし、with文を抜けた後にOpenJtalkが内部的に |
おおなるほどです!この場合、エンジンAとBが同じ辞書を見るとして、 (「Aがclose → Bがclose」の間にBがhMapでファイルを読み込みできないかも、と思ってます) |
そもそもエンジンのプロセス1つにつきコンパイル済みユーザー辞書が1つ作られるという感じなのでそもそも起こらないです。 仮にプロセス間で同じユーザー辞書を共有する場合、Windowsの場合はファイルを開いているハンドルがある限りファイルが残り続けるので動作します。 OpenJTalkのユーザー辞書を開くコード周りをある程度確認しましたがMeCabのModelが破棄されるまでは辞書は開いたままになるはずです。 |
なるほど、確かに複数のエンジンプロセスが同じコンパイル済み辞書のパスを見ることはなさそう! まだまだ考えることがいっぱいあると思うので、問題が起こらなさそうか考えてみていただければ! 正直ドキュメントが書かれていない時点で、ソースコードいじるのはかなり不安です。 メンテナとしては問題はないこと把握する必要があり、そのためには思いつく限り全ての不安要素を調査しておきたい感じです! |
一時ファイルの削除を行うのはWindows側の役割なのでエンジンが強制終了しても削除されます。
POSIXではファイルが削除されるとディスクエントリが削除されてファイルの実態はファイル記述子やmmap等の参照が無くなるまで残ります。
そのため既に開いているfdやmmapはwith文を抜けてファイルが削除されても引き続きアクセスが可能です。 |
なるほどです!! 僕が現状思いつく範囲の問題点はなさそうに思いました! windowsでの挙動の確認ですが、NamedTemporaryFileにある以下の説明の2番めを満たしてるのでmecab側から開けて、
あと
これは結構ありえなくはなさそうなので、一応一通り追いかけて確認したほうが良い気がしました。 |
そのファイルを参照する記述子が全て閉じられた時にWindowsによって削除されるようになっています。
最初はなくても構わないと思ったのですが
https://github.com/VOICEVOX/open_jtalk/blob/e8a99109e84cb1299726f2a9567d3400d70d973d/src/mecab/src/tokenizer.cpp#L119 |
なるほどです!!
とりあえずopenjtalkのソースコード全体で |
自分もあまり理解できていないです。 最終的に削除をする代わりに |
なるほどです!! cdllで直接叩くのありだと感じました! 代案として、python側もcdllでFILE_SHARE_DELETEを指定して開いてwriteしてpyopenjtalkにsetしてdeleteする、というのはいけそうですかね…? |
一応できますがPOSIXのコードと乖離が大きくなるためかえって分かりにくくなりそうな気がします。 |
おっとなるほどです! ファイル削除関数を分けるか、ファイル作成関数を分けるかの違いだと思うので、どちらも複雑性は違わない気が・・・? (意図がわからず、これ気づくのに15分くらいかかりました。。 😇
ちょっと勘違いかもなのですが、実は
C++内でCreateFile使って |
自分が考えていた voicevox_engine/voicevox_engine/user_dict/user_dict_manager.py Lines 170 to 175 in 1db9ede
finally:
# 後処理
if tmp_csv_path.exists():
tmp_csv_path.unlink()
if tmp_compiled_path.exists():
- tmp_compiled_path.unlink()
+ if sys.platform == "win32":
+ _unlink_file_on_close(str(tmp_compiled_path)) # この関数で`FILE_FLAG_DELETE_ON_CLOSE`で開いてすぐ閉じる
+ else:
+ tmp_compiled_path.unlink()
色々試した結果 OpenJTalkでは |
なるほど!?!?!?!? うーーーーーーーん これちょっと詰んだ気がしますね!! というのも、 かなり調べたり調べてくださったりしたのに行き着いた結論がここ(アンドキュメント)なのは結構悔しいですが、どういう挙動になるかわからない以上フォールバックを用意する必要があるように感じました。 であればもうopenjtalkはいじらず、そのフォールバックだけで良いかもとも思いました・・・。
このロジックであれば、finallyでファイル削除している部分をちょっと変更するだけで行けるかもです。 finally:
# 後処理
if tmp_csv_path.exists():
tmp_csv_path.unlink()
# コンパイル済み辞書をすべて削除
_unlink_without_error(directory.glob("*特定のpotfix.txt")) python版jpreprocessとかができたらこの辺り抜本的に解決ができるかもですし、一旦ここはフォールバックを実装するのはどうでしょうか。。 あ、もちろん |
すいません、そもそも自分は「Windows上では開かれているファイルを削除することはできない」と勘違いしていたせいで
の
|
あーーーたしかに!!!!!!
まあバージョンによって挙動が違うこととかはあり得なくはないですが、Windows10/11だったら流石に揃ってると信じたい。 よし!! 実装面ですが、たぶん |
内容
windows環境で、エンジンを2つ起動するとopenjtalk周りでエラーが出ます。
pyopenjtalk.set_user_dict`したものはopenjtalk側でずっとファイルハンドラが必要で、同じファイルに書き込めないためです。
voicevox_engine/voicevox_engine/user_dict/user_dict.py
Line 159 in 1269fab
この辺りを、ユーザーにバグだと思われることなく解決したいです。
Pros 良くなる点
エディタ側でエンジンがなぜか消えずに残っていることが結構あり、エディターを起動した時にエンジンがエラーになるのを防げる。
あと開発段階でエンジンを複数起動できるようになる。
Cons 悪くなる点
完璧な実装方法が思いついてない
実現方法
コンパイルしたユーザー辞書を
set_user_dict
してるので、そのコンパイル済みの辞書のパスをランダムにすれば一応問題は解決されるはず。ただそうすると辞書を更新するたびに、あるいはエンジンが起動するたびに新しいファイルが生まれて残り続けてしまいます。
迂回作としては、ちょっと雑なアイデアだけど、
compiled-0.dic
~compiled-4.dic
まで空いてるパスを探すとか・・・?追記:↑この方法が今のところ一番良さそう!(空いてる=開くことができる)
その他
#1332 (comment) で整理したメモをこちらにも転機します:
update_dict
、つまり保存した辞書などをopenjtalkに読み込ませる部分で、多重起動している場合にエラーになるpyopenjtalk.set_user_dict
したものはopenjtalk側でずっとファイルハンドラが必要で、同じファイルに書き込めないからThe text was updated successfully, but these errors were encountered: