Skip to content

Commit bb240ee

Browse files
committed
Fix: 保持している音声合成モデルの情報が正常にソートされていない状態で返される問題を修正
1 parent fcbb012 commit bb240ee

File tree

1 file changed

+86
-33
lines changed

1 file changed

+86
-33
lines changed

voicevox_engine/aivm_infos_repository.py

Lines changed: 86 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import aivmlib
1414
from aivmlib.schemas.aivm_manifest import AivmMetadata, ModelArchitecture
15-
from pydantic import TypeAdapter
15+
from pydantic import BaseModel, TypeAdapter
1616
from semver.version import Version
1717

1818
from voicevox_engine.library.model import LibrarySpeaker
@@ -31,7 +31,12 @@
3131

3232
__all__ = ["AivmInfosRepository"]
3333

34-
AivmInfosCache = TypeAdapter(dict[str, AivmInfo])
34+
35+
class AivmInfosCacheData(BaseModel):
36+
"""キャッシュファイルに保存されるデータ構造"""
37+
38+
aivm_infos: dict[str, AivmInfo]
39+
default_model_uuid_order: list[str] | None = None
3540

3641

3742
class AivmInfosRepository:
@@ -120,6 +125,7 @@ def get_installed_aivm_infos(self) -> dict[str, AivmInfo]:
120125

121126
# この時点で確実にインストール済み音声合成モデルの情報が存在しているべき
122127
assert self._installed_aivm_infos is not None
128+
123129
return self._installed_aivm_infos
124130

125131
def upsert_model_from_metadata(
@@ -141,13 +147,13 @@ def upsert_model_from_metadata(
141147
# この時点で確実にインストール済み音声合成モデルの情報が存在しているべき
142148
assert self._installed_aivm_infos is not None
143149

144-
# 当該モデルが既に存在する場合(つまりモデル更新時)、既存のロード状態とデフォルトモデルフラグを引き継ぐ
150+
# 当該モデルが既に存在する場合(つまりモデル更新時)、既存のロード状態を引き継ぐ
145151
manifest_uuid = str(aivm_metadata.manifest.uuid)
146152
existing_info = self._installed_aivm_infos.get(manifest_uuid)
147153
is_loaded = existing_info.is_loaded if existing_info is not None else False
148-
is_default_model = (
149-
existing_info.is_default_model if existing_info is not None else False
150-
)
154+
155+
# デフォルトモデルかどうかは常に _default_model_uuid_order に基づいて判定する
156+
is_default_model = self._is_default_model(manifest_uuid)
151157

152158
# AIVM メタデータから AivmInfo を構築する
153159
build_result = self._build_aivm_info_from_metadata(
@@ -199,21 +205,16 @@ def mark_default_models(self, default_model_uuids: list[uuid.UUID]) -> None:
199205
assert self._installed_aivm_infos is not None
200206

201207
# デフォルトモデルの順序を保持
208+
old_order = self._default_model_uuid_order
202209
self._default_model_uuid_order = [str(uuid) for uuid in default_model_uuids]
203210

204-
# UUID セットを作成してフラグ更新に使用
205-
default_model_uuid_set = set(default_model_uuids)
206-
207-
is_changed = False
211+
# 全てのモデルの is_default_model フラグを現在の _default_model_uuid_order に基づいて再設定
208212
for model_uuid, info in self._installed_aivm_infos.items():
209-
new_flag = uuid.UUID(model_uuid) in default_model_uuid_set
210-
if info.is_default_model != new_flag:
211-
# サーバー側の指定に追従してフラグを更新
212-
info.is_default_model = new_flag
213-
is_changed = True
214-
215-
# フラグが更新されていた場合はソートを実行してからキャッシュに反映
216-
if is_changed is True:
213+
info.is_default_model = self._is_default_model(model_uuid)
214+
215+
# 順序情報が設定された場合、または順序が変わった場合は必ず再ソートしてキャッシュに反映
216+
order_set = old_order != self._default_model_uuid_order
217+
if order_set is True:
217218
self._installed_aivm_infos = self._sort_models(self._installed_aivm_infos)
218219
self._persist_to_cache()
219220

@@ -290,7 +291,11 @@ def update_repository(self) -> None:
290291
exc_info=ex,
291292
)
292293

293-
# 最終的にデフォルトモデル優先・名前順でソート
294+
# スキャンで取得したモデル情報の is_default_model フラグを正しく設定
295+
for model_uuid, info in self._installed_aivm_infos.items():
296+
info.is_default_model = self._is_default_model(model_uuid)
297+
298+
# ソート
294299
self._installed_aivm_infos = self._sort_models(self._installed_aivm_infos)
295300

296301
# 現在保持している情報をキャッシュに反映
@@ -345,6 +350,25 @@ def sort_key(item: tuple[str, AivmInfo]) -> tuple[bool, int, str]:
345350

346351
return dict(sorted(aivm_infos.items(), key=sort_key))
347352

353+
def _is_default_model(self, model_uuid: str) -> bool:
354+
"""
355+
指定されたモデル UUID がデフォルトモデルかどうかを判定する。
356+
357+
Parameters
358+
----------
359+
model_uuid : str
360+
判定対象のモデル UUID
361+
362+
Returns
363+
-------
364+
bool
365+
デフォルトモデルの場合 True、そうでない場合 False
366+
"""
367+
368+
if self._default_model_uuid_order is None:
369+
return False
370+
return model_uuid in self._default_model_uuid_order
371+
348372
def _load_from_cache(self) -> bool:
349373
"""
350374
キャッシュファイルからすべてのインストール済み音声合成モデルの情報を取得し、
@@ -365,12 +389,37 @@ def _load_from_cache(self) -> bool:
365389
try:
366390
# キャッシュファイルからインストール済みの音声合成モデルの情報を読み込む
367391
with open(self.CACHE_FILE_PATH, encoding="utf-8") as f:
368-
result = AivmInfosCache.validate_json(f.read())
392+
cache_json = f.read()
393+
394+
# 既存のキャッシュ形式(dict[str, AivmInfo])との互換性を保つ
395+
try:
396+
cache_data = AivmInfosCacheData.model_validate_json(cache_json)
397+
aivm_infos = cache_data.aivm_infos
398+
self._default_model_uuid_order = cache_data.default_model_uuid_order
399+
except Exception:
400+
# 旧形式のキャッシュファイルの場合は、dict[str, AivmInfo] として読み込む
401+
aivm_infos = TypeAdapter(dict[str, AivmInfo]).validate_json(
402+
cache_json
403+
)
404+
self._default_model_uuid_order = None
405+
369406
# すべてのモデルのロード状態を False にする
370-
for aivm_info in result.values():
407+
for aivm_info in aivm_infos.values():
371408
aivm_info.is_loaded = False
372-
# デフォルトモデル優先・名前順でソート
373-
self._installed_aivm_infos = self._sort_models(result)
409+
410+
# 一旦読み込んだ結果を保持
411+
self._installed_aivm_infos = aivm_infos
412+
413+
# キャッシュから読み込んだモデル情報の is_default_model フラグを正しく設定
414+
# (キャッシュに保存されている値は古い可能性があるため、現在の _default_model_uuid_order に基づいて再設定)
415+
for model_uuid, info in self._installed_aivm_infos.items():
416+
info.is_default_model = self._is_default_model(model_uuid)
417+
418+
# ソート
419+
self._installed_aivm_infos = self._sort_models(
420+
self._installed_aivm_infos
421+
)
422+
374423
logger.info(
375424
f"Loaded {len(self._installed_aivm_infos)} models from cache."
376425
)
@@ -393,22 +442,23 @@ def _persist_to_cache(self) -> None:
393442

394443
with self._lock:
395444
try:
445+
# キャッシュデータを構築(デフォルトモデルの順序情報も含める)
446+
cache_data = AivmInfosCacheData(
447+
aivm_infos=self._installed_aivm_infos,
448+
default_model_uuid_order=self._default_model_uuid_order,
449+
)
450+
396451
# 一時ファイルに書き込んでから名前変更することで、
397452
# 書き込み中にクラッシュしてもキャッシュファイルが壊れないようにする
398453
temp_path = self.CACHE_FILE_PATH.with_suffix(".tmp")
399454
with open(temp_path, mode="w", encoding="utf-8") as f:
400-
f.write(
401-
AivmInfosCache.dump_json(
402-
self._installed_aivm_infos, indent=4
403-
).decode("utf-8")
404-
)
455+
f.write(cache_data.model_dump_json(indent=4))
405456
# ファイル名を変更(既存のファイルは上書き)
406457
temp_path.replace(self.CACHE_FILE_PATH)
407458
except Exception as ex:
408459
logger.warning("Failed to save cache file:", exc_info=ex)
409460

410-
@classmethod
411-
def _scan_models(cls, installed_models_dir: Path) -> dict[str, AivmInfo]:
461+
def _scan_models(self, installed_models_dir: Path) -> dict[str, AivmInfo]:
412462
"""
413463
指定されたディレクトリに保存されている *.aivmx ファイルを走査し、すべての音声合成モデルの情報を取得する。
414464
このメソッドはソートを行わないので、呼び出し元の責任でソートを行う必要がある。
@@ -464,13 +514,16 @@ def _scan_models(cls, installed_models_dir: Path) -> dict[str, AivmInfo]:
464514
)
465515
continue
466516

517+
# デフォルトモデルかどうかは常に _default_model_uuid_order に基づいて判定する
518+
is_default_model = self._is_default_model(aivm_model_uuid)
519+
467520
# AIVM メタデータから AivmInfo を構築する
468-
## スキャン時は一旦 is_loaded=False、is_default_model=False として構築する
469-
build_result = cls._build_aivm_info_from_metadata(
521+
## スキャン時は is_loaded=False として構築する
522+
build_result = self._build_aivm_info_from_metadata(
470523
aivm_metadata,
471524
aivm_file_path,
472525
is_loaded=False,
473-
is_default_model=False,
526+
is_default_model=is_default_model,
474527
)
475528

476529
# AIVM マニフェストのバリデーションエラーやサポートされていないバージョンなどで None が返された場合はスキップ

0 commit comments

Comments
 (0)