1212
1313import aivmlib
1414from aivmlib .schemas .aivm_manifest import AivmMetadata , ModelArchitecture
15- from pydantic import TypeAdapter
15+ from pydantic import BaseModel , TypeAdapter
1616from semver .version import Version
1717
1818from voicevox_engine .library .model import LibrarySpeaker
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
3742class 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