Spaces:
Running on Zero
Running on Zero
| from __future__ import annotations | |
| import hashlib | |
| import os | |
| import shutil | |
| import sys | |
| import tarfile | |
| import tempfile | |
| import threading | |
| import urllib.request | |
| import zipfile | |
| from pathlib import Path | |
| from types import SimpleNamespace | |
| os.environ.setdefault("TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD", "1") | |
| ROOT = Path(__file__).resolve().parent | |
| CACHE_ROOT = Path( | |
| os.environ.get("GAME_CACHE_DIR", Path.home() / ".cache" / "game") | |
| ) | |
| SOURCE_REVISION = "4ad815c90dfe2442730f3fdc866fd23e737cbc97" | |
| SOURCE_NAME = f"GAME-{SOURCE_REVISION}" | |
| SOURCE_DIR = CACHE_ROOT / SOURCE_NAME | |
| SOURCE_ARCHIVE = CACHE_ROOT / f"{SOURCE_NAME}.tar.gz" | |
| SOURCE_URL = ( | |
| f"https://codeload.github.com/openvpi/GAME/tar.gz/{SOURCE_REVISION}" | |
| ) | |
| SOURCE_SHA256 = ( | |
| "b1c1584d2326d6920228695a3b401f6483e6ef50c7d1695b984b63da6ba86f3b" | |
| ) | |
| MODEL_NAME = "GAME-1.0-small" | |
| MODEL_FILES = ("model.pt", "config.yaml", "lang_map.json") | |
| MODEL_DIR = CACHE_ROOT / MODEL_NAME | |
| MODEL_ARCHIVE = CACHE_ROOT / f"{MODEL_NAME}.zip" | |
| MODEL_URL = ( | |
| "https://github.com/openvpi/GAME/releases/" | |
| "download/v1.0.0/GAME-1.0-small.zip" | |
| ) | |
| MODEL_SHA256 = ( | |
| "3d3e1ac0a83234b2a163a3d43043455d15670765eaa25ef6285c399da1ccc576" | |
| ) | |
| _source_lock = threading.Lock() | |
| _model_archive_lock = threading.Lock() | |
| _runtime_lock = threading.Lock() | |
| _inference_model_lock = threading.Lock() | |
| _runtime: SimpleNamespace | None = None | |
| _inference_model = None | |
| _language_map: dict[str, int] | None = None | |
| def _sha256(path: Path) -> str: | |
| digest = hashlib.sha256() | |
| with path.open("rb") as file: | |
| for chunk in iter(lambda: file.read(1024 * 1024), b""): | |
| digest.update(chunk) | |
| return digest.hexdigest() | |
| def _download( | |
| url: str, | |
| destination: Path, | |
| expected_sha256: str, | |
| label: str, | |
| ) -> None: | |
| CACHE_ROOT.mkdir(parents=True, exist_ok=True) | |
| if destination.exists(): | |
| if _sha256(destination) == expected_sha256: | |
| return | |
| destination.unlink() | |
| partial = Path(f"{destination}.part") | |
| partial.unlink(missing_ok=True) | |
| request = urllib.request.Request( | |
| url, | |
| headers={"User-Agent": "TEAMuP-GAME-Space/1.0"}, | |
| ) | |
| print(f"Downloading {label}...", flush=True) | |
| try: | |
| with ( | |
| urllib.request.urlopen(request, timeout=60) as response, | |
| partial.open("wb") as file, | |
| ): | |
| shutil.copyfileobj(response, file) | |
| actual_sha256 = _sha256(partial) | |
| if actual_sha256 != expected_sha256: | |
| raise RuntimeError( | |
| f"{label} failed SHA-256 validation: " | |
| f"expected {expected_sha256}, received {actual_sha256}" | |
| ) | |
| partial.replace(destination) | |
| except Exception: | |
| partial.unlink(missing_ok=True) | |
| raise | |
| def _source_complete(path: Path) -> bool: | |
| required = ( | |
| path / "inference" / "api.py", | |
| path / "inference" / "callbacks.py", | |
| path / "inference" / "data.py", | |
| path / "inference" / "slicer2.py", | |
| path / "lib" / "config" / "schema.py", | |
| ) | |
| return all(file.is_file() for file in required) | |
| def _get_source_dir() -> Path: | |
| local_source = ROOT / "GAME" | |
| if _source_complete(local_source): | |
| return local_source | |
| if _source_complete(SOURCE_DIR): | |
| return SOURCE_DIR | |
| with _source_lock: | |
| if _source_complete(SOURCE_DIR): | |
| return SOURCE_DIR | |
| _download( | |
| SOURCE_URL, | |
| SOURCE_ARCHIVE, | |
| SOURCE_SHA256, | |
| f"GAME source revision {SOURCE_REVISION}", | |
| ) | |
| with tempfile.TemporaryDirectory( | |
| prefix="game-source-", | |
| dir=CACHE_ROOT, | |
| ) as temporary_dir: | |
| temporary_path = Path(temporary_dir) | |
| with tarfile.open(SOURCE_ARCHIVE, "r:gz") as archive: | |
| archive.extractall(temporary_path, filter="data") | |
| extracted = temporary_path / SOURCE_NAME | |
| if not _source_complete(extracted): | |
| raise RuntimeError( | |
| "The GAME source archive is missing inference files." | |
| ) | |
| if SOURCE_DIR.exists(): | |
| shutil.rmtree(SOURCE_DIR) | |
| shutil.move(str(extracted), str(SOURCE_DIR)) | |
| return SOURCE_DIR | |
| def _model_complete(path: Path) -> bool: | |
| return all((path / filename).is_file() for filename in MODEL_FILES) | |
| def _get_model_dir() -> Path: | |
| local_model = ROOT / "models" / MODEL_NAME | |
| if _model_complete(local_model): | |
| return local_model | |
| if _model_complete(MODEL_DIR): | |
| return MODEL_DIR | |
| with _model_archive_lock: | |
| if _model_complete(MODEL_DIR): | |
| return MODEL_DIR | |
| _download( | |
| MODEL_URL, | |
| MODEL_ARCHIVE, | |
| MODEL_SHA256, | |
| f"{MODEL_NAME} checkpoint", | |
| ) | |
| with tempfile.TemporaryDirectory( | |
| prefix="game-model-", | |
| dir=CACHE_ROOT, | |
| ) as temporary_dir: | |
| temporary_path = Path(temporary_dir) | |
| with zipfile.ZipFile(MODEL_ARCHIVE) as archive: | |
| archive.extractall(temporary_path) | |
| extracted = temporary_path / MODEL_NAME | |
| if not _model_complete(extracted): | |
| raise RuntimeError( | |
| "The GAME checkpoint archive is incomplete." | |
| ) | |
| if MODEL_DIR.exists(): | |
| shutil.rmtree(MODEL_DIR) | |
| shutil.move(str(extracted), str(MODEL_DIR)) | |
| return MODEL_DIR | |
| def _get_runtime() -> SimpleNamespace: | |
| global _runtime | |
| if _runtime is not None: | |
| return _runtime | |
| with _runtime_lock: | |
| if _runtime is not None: | |
| return _runtime | |
| source_path = str(_get_source_dir()) | |
| if source_path not in sys.path: | |
| sys.path.insert(0, source_path) | |
| from inference.api import infer_model, load_inference_model | |
| from inference.callbacks import ( | |
| SaveCombinedMidiFileCallback, | |
| SaveCombinedTextFileCallback, | |
| ) | |
| from inference.data import SlicedAudioFileIterableDataset | |
| from inference.slicer2 import Slicer | |
| from lib.config.schema import ValidationConfig | |
| _runtime = SimpleNamespace( | |
| infer_model=infer_model, | |
| load_inference_model=load_inference_model, | |
| MidiCallback=SaveCombinedMidiFileCallback, | |
| TextCallback=SaveCombinedTextFileCallback, | |
| Dataset=SlicedAudioFileIterableDataset, | |
| Slicer=Slicer, | |
| ValidationConfig=ValidationConfig, | |
| ) | |
| return _runtime | |
| def _get_model(runtime: SimpleNamespace): | |
| global _inference_model, _language_map | |
| if _inference_model is not None: | |
| return _inference_model, _language_map | |
| with _inference_model_lock: | |
| if _inference_model is None: | |
| _inference_model, _language_map = runtime.load_inference_model( | |
| _get_model_dir() / "model.pt" | |
| ) | |
| return _inference_model, _language_map | |
| def _language_id( | |
| language_code: str, | |
| language_map: dict[str, int] | None, | |
| ) -> int: | |
| if not language_code: | |
| return 0 | |
| if language_map is None or language_code not in language_map: | |
| supported = ", ".join(language_map or ()) | |
| raise ValueError( | |
| f"Language '{language_code}' is not supported. " | |
| f"Supported languages: {supported}" | |
| ) | |
| return language_map[language_code] | |
| def transcribe( | |
| audio_path: Path, | |
| output_dir: Path, | |
| language_code: str, | |
| steps: int, | |
| ) -> None: | |
| runtime = _get_runtime() | |
| model, language_map = _get_model(runtime) | |
| sample_rate = model.inference_config.features.audio_sample_rate | |
| dataset = runtime.Dataset( | |
| filemap={audio_path.stem: audio_path}, | |
| samplerate=sample_rate, | |
| slicer=runtime.Slicer( | |
| sr=sample_rate, | |
| threshold=-40.0, | |
| min_length=1000, | |
| min_interval=200, | |
| max_sil_kept=100, | |
| ), | |
| language=_language_id(language_code, language_map), | |
| ) | |
| callbacks = [ | |
| runtime.MidiCallback(output_dir=output_dir, tempo=120), | |
| runtime.TextCallback( | |
| output_dir=output_dir, | |
| file_format="csv", | |
| pitch_format="name", | |
| round_pitch=False, | |
| ), | |
| ] | |
| config = runtime.ValidationConfig( | |
| d3pm_sample_t0=0.0, | |
| d3pm_sample_steps=steps, | |
| d3pm_sample_ts=None, | |
| boundary_decoding_threshold=0.2, | |
| boundary_decoding_radius=round(0.02 / model.timestep), | |
| note_presence_threshold=0.2, | |
| ) | |
| runtime.infer_model( | |
| model=model, | |
| dataset=dataset, | |
| config=config, | |
| callbacks=callbacks, | |
| batch_size=1, | |
| num_workers=0, | |
| precision="32-true", | |
| ) | |