Commit fa5b6147 fa5b61472888ffd6c44865480cc764e35e97de20 by cnb.bofCdSsphPA

Let the ACR API start before heavy model dependencies load

Constraint: service health and config endpoints should stay reachable even when training-time dependencies like torch are not installed
Rejected: importing retrieval engines at module load | it makes the whole API crash before reporting dependency gaps clearly
Confidence: high
Scope-risk: narrow
Directive: keep runtime dependency checks inside request-time engine loading so infra can health-check the service independently of model installation state
Tested: /usr/local/miniconda3/bin/python -m unittest discover -s acr-engine/tests -v; /usr/local/miniconda3/bin/python -m uvicorn src.service.app:app --host 127.0.0.1 --port 8000 with successful /health response; POST /recognize/voice currently returns a clear 500 dependency error when torch is missing
Not-tested: successful end-to-end /recognize/voice inference without torch installed
1 parent bd66c06b
...@@ -10,9 +10,6 @@ from fastapi import FastAPI, File, HTTPException, UploadFile ...@@ -10,9 +10,6 @@ from fastapi import FastAPI, File, HTTPException, UploadFile
10 from pydantic import BaseModel 10 from pydantic import BaseModel
11 11
12 from src.data.voice_chunker import voice_to_chunks 12 from src.data.voice_chunker import voice_to_chunks
13 from src.engines.chromaprint_matcher import ChromaprintMatcher
14 from src.engines.ecapa_embedder import ECAPAEmbedder
15 from src.engines.hybrid_engine import HybridEngine
16 from src.service.settings import ServiceSettings 13 from src.service.settings import ServiceSettings
17 from src.utils.context_exporter import export_match_context, find_best_matching_window 14 from src.utils.context_exporter import export_match_context, find_best_matching_window
18 15
...@@ -35,7 +32,7 @@ class BuildIndexRequest(BaseModel): ...@@ -35,7 +32,7 @@ class BuildIndexRequest(BaseModel):
35 32
36 app = FastAPI(title='ACR Service', version='0.4.0') 33 app = FastAPI(title='ACR Service', version='0.4.0')
37 settings = ServiceSettings() 34 settings = ServiceSettings()
38 _engine_cache: dict[tuple[str, str, str, str], HybridEngine] = {} 35 _engine_cache: dict[tuple[str, str, str, str], object] = {}
39 _cache_lock = Lock() 36 _cache_lock = Lock()
40 37
41 38
...@@ -67,7 +64,14 @@ def _readiness_snapshot(data_dir: str, model_path: str, index_prefix: str) -> di ...@@ -67,7 +64,14 @@ def _readiness_snapshot(data_dir: str, model_path: str, index_prefix: str) -> di
67 return {'ready': all(item['exists'] for item in files.values()), 'files': files, 'manifests': manifest_candidates} 64 return {'ready': all(item['exists'] for item in files.values()), 'files': files, 'manifests': manifest_candidates}
68 65
69 66
70 def _load_engine_uncached(data_dir: str, model_path: str, index_prefix: str, device: str) -> HybridEngine: 67 def _load_engine_uncached(data_dir: str, model_path: str, index_prefix: str, device: str):
68 try:
69 from src.engines.chromaprint_matcher import ChromaprintMatcher
70 from src.engines.ecapa_embedder import ECAPAEmbedder
71 from src.engines.hybrid_engine import HybridEngine
72 except Exception as exc:
73 raise HTTPException(status_code=500, detail=f"Engine dependencies unavailable: {exc}")
74
71 matcher = ChromaprintMatcher() 75 matcher = ChromaprintMatcher()
72 chroma_path = str(Path(index_prefix).parent / 'chromaprint.pkl') 76 chroma_path = str(Path(index_prefix).parent / 'chromaprint.pkl')
73 if not Path(chroma_path).exists(): 77 if not Path(chroma_path).exists():
...@@ -93,7 +97,7 @@ def _load_engine_uncached(data_dir: str, model_path: str, index_prefix: str, dev ...@@ -93,7 +97,7 @@ def _load_engine_uncached(data_dir: str, model_path: str, index_prefix: str, dev
93 return engine 97 return engine
94 98
95 99
96 def _load_engine(data_dir: str, model_path: str, index_prefix: str, device: str) -> tuple[HybridEngine, bool]: 100 def _load_engine(data_dir: str, model_path: str, index_prefix: str, device: str):
97 key = (str(Path(data_dir).resolve()), str(Path(model_path).resolve()), str(Path(index_prefix).resolve()), device) 101 key = (str(Path(data_dir).resolve()), str(Path(model_path).resolve()), str(Path(index_prefix).resolve()), device)
98 with _cache_lock: 102 with _cache_lock:
99 cached = _engine_cache.get(key) 103 cached = _engine_cache.get(key)
......