leostudio 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. leostudio-0.1.0/PKG-INFO +43 -0
  2. leostudio-0.1.0/README.md +13 -0
  3. leostudio-0.1.0/pyproject.toml +47 -0
  4. leostudio-0.1.0/setup.cfg +4 -0
  5. leostudio-0.1.0/src/leostudio/__init__.py +7 -0
  6. leostudio-0.1.0/src/leostudio/__main__.py +8 -0
  7. leostudio-0.1.0/src/leostudio/cli.py +111 -0
  8. leostudio-0.1.0/src/leostudio/file_cache.py +243 -0
  9. leostudio-0.1.0/src/leostudio/rvc/__init__.py +0 -0
  10. leostudio-0.1.0/src/leostudio/rvc/configs/config.json +1 -0
  11. leostudio-0.1.0/src/leostudio/rvc/configs/config.py +265 -0
  12. leostudio-0.1.0/src/leostudio/rvc/configs/inuse/v1/32k.json +46 -0
  13. leostudio-0.1.0/src/leostudio/rvc/configs/inuse/v1/40k.json +46 -0
  14. leostudio-0.1.0/src/leostudio/rvc/configs/inuse/v1/48k.json +46 -0
  15. leostudio-0.1.0/src/leostudio/rvc/configs/inuse/v2/32k.json +46 -0
  16. leostudio-0.1.0/src/leostudio/rvc/configs/inuse/v2/48k.json +46 -0
  17. leostudio-0.1.0/src/leostudio/rvc/configs/v1/32k.json +46 -0
  18. leostudio-0.1.0/src/leostudio/rvc/configs/v1/40k.json +46 -0
  19. leostudio-0.1.0/src/leostudio/rvc/configs/v1/48k.json +46 -0
  20. leostudio-0.1.0/src/leostudio/rvc/configs/v2/32k.json +46 -0
  21. leostudio-0.1.0/src/leostudio/rvc/configs/v2/48k.json +46 -0
  22. leostudio-0.1.0/src/leostudio/rvc/i18n/i18n.py +8 -0
  23. leostudio-0.1.0/src/leostudio/rvc/infer/lib/audio.py +74 -0
  24. leostudio-0.1.0/src/leostudio/rvc/infer/lib/fairseq_minimal/__init__.py +14 -0
  25. leostudio-0.1.0/src/leostudio/rvc/infer/lib/fairseq_minimal/checkpoint_utils.py +224 -0
  26. leostudio-0.1.0/src/leostudio/rvc/infer/lib/fairseq_minimal/namespace_stub.py +101 -0
  27. leostudio-0.1.0/src/leostudio/rvc/infer/lib/infer_pack/attentions.py +457 -0
  28. leostudio-0.1.0/src/leostudio/rvc/infer/lib/infer_pack/attentions_onnx.py +457 -0
  29. leostudio-0.1.0/src/leostudio/rvc/infer/lib/infer_pack/commons.py +169 -0
  30. leostudio-0.1.0/src/leostudio/rvc/infer/lib/infer_pack/models.py +1223 -0
  31. leostudio-0.1.0/src/leostudio/rvc/infer/lib/infer_pack/models_onnx.py +818 -0
  32. leostudio-0.1.0/src/leostudio/rvc/infer/lib/infer_pack/modules/F0Predictor/DioF0Predictor.py +91 -0
  33. leostudio-0.1.0/src/leostudio/rvc/infer/lib/infer_pack/modules/F0Predictor/F0Predictor.py +16 -0
  34. leostudio-0.1.0/src/leostudio/rvc/infer/lib/infer_pack/modules/F0Predictor/HarvestF0Predictor.py +87 -0
  35. leostudio-0.1.0/src/leostudio/rvc/infer/lib/infer_pack/modules/F0Predictor/PMF0Predictor.py +98 -0
  36. leostudio-0.1.0/src/leostudio/rvc/infer/lib/infer_pack/modules/F0Predictor/__init__.py +0 -0
  37. leostudio-0.1.0/src/leostudio/rvc/infer/lib/infer_pack/modules.py +612 -0
  38. leostudio-0.1.0/src/leostudio/rvc/infer/lib/infer_pack/onnx_inference.py +148 -0
  39. leostudio-0.1.0/src/leostudio/rvc/infer/lib/infer_pack/transforms.py +207 -0
  40. leostudio-0.1.0/src/leostudio/rvc/infer/lib/jit/__init__.py +163 -0
  41. leostudio-0.1.0/src/leostudio/rvc/infer/lib/jit/get_hubert.py +345 -0
  42. leostudio-0.1.0/src/leostudio/rvc/infer/lib/jit/get_rmvpe.py +15 -0
  43. leostudio-0.1.0/src/leostudio/rvc/infer/lib/jit/get_synthesizer.py +38 -0
  44. leostudio-0.1.0/src/leostudio/rvc/infer/lib/rmvpe.py +685 -0
  45. leostudio-0.1.0/src/leostudio/rvc/infer/lib/rtrvc.py +462 -0
  46. leostudio-0.1.0/src/leostudio/rvc/infer/lib/slicer2.py +260 -0
  47. leostudio-0.1.0/src/leostudio/rvc/infer/lib/train/data_utils.py +517 -0
  48. leostudio-0.1.0/src/leostudio/rvc/infer/lib/train/losses.py +58 -0
  49. leostudio-0.1.0/src/leostudio/rvc/infer/lib/train/mel_processing.py +127 -0
  50. leostudio-0.1.0/src/leostudio/rvc/infer/lib/train/process_ckpt.py +268 -0
  51. leostudio-0.1.0/src/leostudio/rvc/infer/lib/train/utils.py +485 -0
  52. leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/dataset.py +183 -0
  53. leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/layers.py +118 -0
  54. leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/layers_123812KB .py +118 -0
  55. leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/layers_123821KB.py +118 -0
  56. leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/layers_33966KB.py +126 -0
  57. leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/layers_537227KB.py +126 -0
  58. leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/layers_537238KB.py +126 -0
  59. leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/layers_new.py +125 -0
  60. leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/model_param_init.py +68 -0
  61. leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/nets.py +122 -0
  62. leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/nets_123812KB.py +122 -0
  63. leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/nets_123821KB.py +122 -0
  64. leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/nets_33966KB.py +122 -0
  65. leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/nets_537227KB.py +122 -0
  66. leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/nets_537238KB.py +122 -0
  67. leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/nets_61968KB.py +122 -0
  68. leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/nets_new.py +133 -0
  69. leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/spec_utils.py +674 -0
  70. leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/utils.py +121 -0
  71. leostudio-0.1.0/src/leostudio/rvc/infer/modules/train/extract/extract_f0_print.py +176 -0
  72. leostudio-0.1.0/src/leostudio/rvc/infer/modules/train/extract/extract_f0_rmvpe.py +144 -0
  73. leostudio-0.1.0/src/leostudio/rvc/infer/modules/train/extract/extract_f0_rmvpe_dml.py +140 -0
  74. leostudio-0.1.0/src/leostudio/rvc/infer/modules/train/extract_feature_print.py +151 -0
  75. leostudio-0.1.0/src/leostudio/rvc/infer/modules/train/preprocess.py +155 -0
  76. leostudio-0.1.0/src/leostudio/rvc/infer/modules/train/train.py +645 -0
  77. leostudio-0.1.0/src/leostudio/rvc/infer/modules/train/train_index.py +90 -0
  78. leostudio-0.1.0/src/leostudio/rvc/infer/modules/vc/__init__.py +0 -0
  79. leostudio-0.1.0/src/leostudio/rvc/infer/modules/vc/modules.py +308 -0
  80. leostudio-0.1.0/src/leostudio/rvc/infer/modules/vc/pipeline.py +464 -0
  81. leostudio-0.1.0/src/leostudio/rvc/infer/modules/vc/utils.py +113 -0
  82. leostudio-0.1.0/src/leostudio/rvc/train_wrapper_headless.py +498 -0
  83. leostudio-0.1.0/src/leostudio/server.py +2453 -0
  84. leostudio-0.1.0/src/leostudio/setup_helper.py +113 -0
  85. leostudio-0.1.0/src/leostudio.egg-info/PKG-INFO +43 -0
  86. leostudio-0.1.0/src/leostudio.egg-info/SOURCES.txt +93 -0
  87. leostudio-0.1.0/src/leostudio.egg-info/dependency_links.txt +1 -0
  88. leostudio-0.1.0/src/leostudio.egg-info/entry_points.txt +2 -0
  89. leostudio-0.1.0/src/leostudio.egg-info/requires.txt +21 -0
  90. leostudio-0.1.0/src/leostudio.egg-info/top_level.txt +1 -0
  91. leostudio-0.1.0/tests/test_auth_middleware.py +257 -0
  92. leostudio-0.1.0/tests/test_error_response.py +525 -0
  93. leostudio-0.1.0/tests/test_file_cache_lru_property.py +352 -0
  94. leostudio-0.1.0/tests/test_file_cache_property.py +394 -0
  95. leostudio-0.1.0/tests/test_health_endpoint.py +335 -0
@@ -0,0 +1,43 @@
1
+ Metadata-Version: 2.4
2
+ Name: leostudio
3
+ Version: 0.1.0
4
+ Summary: Leo Studio Pro — Remote Worker Backend for GPU Inference & Training for LeoStudio Pro
5
+ Author-email: Leo Studio Pro <admin@ray.ac>
6
+ License: MIT
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: fastapi
10
+ Requires-Dist: uvicorn[standard]
11
+ Requires-Dist: python-socketio
12
+ Requires-Dist: aiohttp
13
+ Requires-Dist: aiofiles
14
+ Requires-Dist: python-multipart
15
+ Requires-Dist: soundfile
16
+ Requires-Dist: librosa
17
+ Requires-Dist: ffmpeg-python
18
+ Requires-Dist: av
19
+ Requires-Dist: numpy
20
+ Requires-Dist: scipy
21
+ Requires-Dist: tqdm
22
+ Requires-Dist: faiss-cpu
23
+ Requires-Dist: praat-parselmouth
24
+ Requires-Dist: pyworld
25
+ Requires-Dist: torchcrepe
26
+ Requires-Dist: fairseq
27
+ Requires-Dist: gradio
28
+ Requires-Dist: pyngrok
29
+ Requires-Dist: psutil
30
+
31
+ # Leo Studio Pro — Remote Worker
32
+
33
+ Remote Worker package for offloading RVC voice conversion, dataset prep, and training to remote GPU servers (Google Colab, Kaggle, RunPod, Vast.ai, Linux VPS).
34
+
35
+ ## Quick Start
36
+
37
+ ```bash
38
+ # Install
39
+ pip install leostudio
40
+
41
+ # Run
42
+ leostudio --secret Your_secure_token --raytunnel-server your-server.domain.com --raytunnel-token Your_secure_token --raytunnel-subdomain rvc
43
+ ```
@@ -0,0 +1,13 @@
1
+ # Leo Studio Pro — Remote Worker
2
+
3
+ Remote Worker package for offloading RVC voice conversion, dataset prep, and training to remote GPU servers (Google Colab, Kaggle, RunPod, Vast.ai, Linux VPS).
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # Install
9
+ pip install leostudio
10
+
11
+ # Run
12
+ leostudio --secret Your_secure_token --raytunnel-server your-server.domain.com --raytunnel-token Your_secure_token --raytunnel-subdomain rvc
13
+ ```
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "leostudio"
7
+ version = "0.1.0"
8
+ description = "Leo Studio Pro — Remote Worker Backend for GPU Inference & Training for LeoStudio Pro"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = {text = "MIT"}
12
+ authors = [
13
+ {name = "Leo Studio Pro", email = "admin@ray.ac"}
14
+ ]
15
+
16
+ dependencies = [
17
+ "fastapi",
18
+ "uvicorn[standard]",
19
+ "python-socketio",
20
+ "aiohttp",
21
+ "aiofiles",
22
+ "python-multipart",
23
+ "soundfile",
24
+ "librosa",
25
+ "ffmpeg-python",
26
+ "av",
27
+ "numpy",
28
+ "scipy",
29
+ "tqdm",
30
+ "faiss-cpu",
31
+ "praat-parselmouth",
32
+ "pyworld",
33
+ "torchcrepe",
34
+ "fairseq",
35
+ "gradio",
36
+ "pyngrok",
37
+ "psutil",
38
+ ]
39
+
40
+ [project.scripts]
41
+ leostudio = "leostudio.cli:main"
42
+
43
+ [tool.setuptools.packages.find]
44
+ where = ["src"]
45
+
46
+ [tool.setuptools.package-data]
47
+ leostudio = ["rvc/configs/*.json", "rvc/configs/**/*.json"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,7 @@
1
+ """
2
+ Leo Studio Pro — Remote Worker Library
3
+ """
4
+
5
+ from leostudio.cli import main as start_worker
6
+
7
+ __all__ = ["start_worker"]
@@ -0,0 +1,8 @@
1
+ """
2
+ Enable `python -m leostudio`
3
+ """
4
+
5
+ from leostudio.cli import main
6
+
7
+ if __name__ == "__main__":
8
+ main()
@@ -0,0 +1,111 @@
1
+ """
2
+ Leo Studio Pro — Remote Worker CLI Entrypoint
3
+ ==============================================
4
+
5
+ Usage:
6
+ leostudio --secret Your_secure_token --raytunnel-server s.ray.ac --raytunnel-token Your_secure_token --raytunnel-subdomain rvc
7
+ """
8
+
9
+ import argparse
10
+ import os
11
+ import sys
12
+
13
+ from leostudio.setup_helper import (
14
+ ensure_asset_models,
15
+ get_bundled_rvc_dir,
16
+ patch_fairseq_if_needed,
17
+ )
18
+
19
+
20
+ def main() -> None:
21
+ parser = argparse.ArgumentParser(
22
+ prog="leostudio",
23
+ description="Leo Studio Pro Remote Worker Backend (PyPI Standalone)",
24
+ )
25
+ parser.add_argument("--secret", type=str, default="", help="WORKER_SECRET token for authentication")
26
+ parser.add_argument("--port", type=int, default=7866, help="Port to listen on (default: 7866)")
27
+ parser.add_argument("--host", type=str, default="0.0.0.0", help="Host address (default: 0.0.0.0)")
28
+ parser.add_argument("--max-cache", type=str, default="10", help="MAX_CACHE_GB for model caching")
29
+ parser.add_argument("--raytunnel-server", type=str, default="", help="Raytunnel server address (e.g. s.ray.ac)")
30
+ parser.add_argument("--raytunnel-token", type=str, default="", help="Raytunnel auth token")
31
+ parser.add_argument("--raytunnel-subdomain", type=str, default="", help="Raytunnel subdomain")
32
+ parser.add_argument("--ngrok-token", type=str, default="", help="Ngrok authtoken")
33
+ parser.add_argument("--gradio", action="store_true", help="Enable Gradio share tunnel")
34
+ parser.add_argument("--skip-audio-separator", action="store_true", help="Skip audio separator installation")
35
+
36
+ args = parser.parse_args()
37
+
38
+ # Determine work directory
39
+ if os.path.isdir("/kaggle/working"):
40
+ work_dir = "/kaggle/working"
41
+ elif os.path.isdir("/content"):
42
+ work_dir = "/content"
43
+ else:
44
+ work_dir = os.getcwd()
45
+
46
+ if os.getcwd() != work_dir:
47
+ try:
48
+ os.chdir(work_dir)
49
+ except OSError:
50
+ pass
51
+
52
+ print("╔══════════════════════════════════════════════════════════════╗")
53
+ print("║ Leo Studio Pro — Remote Worker CLI Launcher ║")
54
+ print("╚══════════════════════════════════════════════════════════════╝")
55
+ print(f" Working Directory : {work_dir}")
56
+ print(f" Port : {args.port}")
57
+ print(f" Secret : {'***' if args.secret else '(none)'}")
58
+ print(f" Max Cache GB : {args.max_cache}")
59
+ if args.raytunnel_server:
60
+ print(f" Tunnel : raytunnel ({args.raytunnel_server})")
61
+ elif args.ngrok_token:
62
+ print(f" Tunnel : ngrok")
63
+ elif args.gradio:
64
+ print(f" Tunnel : gradio")
65
+ print()
66
+
67
+ # Get bundled RVC engine directory
68
+ rvc_dir = get_bundled_rvc_dir()
69
+ ensure_asset_models(work_dir)
70
+ patch_fairseq_if_needed()
71
+
72
+ # Add rvc_dir to PYTHONPATH and sys.path
73
+ if rvc_dir not in sys.path:
74
+ sys.path.insert(0, rvc_dir)
75
+ current_pp = os.environ.get("PYTHONPATH", "")
76
+ if rvc_dir not in current_pp:
77
+ os.environ["PYTHONPATH"] = f"{rvc_dir}:{current_pp}".rstrip(":")
78
+
79
+ # Add package directory to sys.path so leostudio modules can be imported smoothly
80
+ pkg_dir = os.path.abspath(os.path.dirname(__file__))
81
+ if pkg_dir not in sys.path:
82
+ sys.path.insert(0, pkg_dir)
83
+
84
+ # Set environment variables for server
85
+ os.environ["PORT"] = str(args.port)
86
+ os.environ["MAX_CACHE_GB"] = str(args.max_cache)
87
+ if args.secret:
88
+ os.environ["WORKER_SECRET"] = args.secret
89
+ if args.raytunnel_server:
90
+ os.environ["RAYTUNNEL_SERVER"] = args.raytunnel_server
91
+ os.environ["RAYTUNNEL_TOKEN"] = args.raytunnel_token
92
+ if args.raytunnel_subdomain:
93
+ os.environ["RAYTUNNEL_SUBDOMAIN"] = args.raytunnel_subdomain
94
+ if args.ngrok_token:
95
+ os.environ["NGROK_TOKEN"] = args.ngrok_token
96
+ if args.gradio:
97
+ os.environ["GRADIO_TUNNEL"] = "1"
98
+ if args.skip_audio_separator:
99
+ os.environ["SKIP_AUDIO_SEPARATOR"] = "true"
100
+
101
+ # Launch server from leostudio package
102
+ try:
103
+ from leostudio import server
104
+ server.main()
105
+ except (ImportError, AttributeError):
106
+ import server
107
+ server.main()
108
+
109
+
110
+ if __name__ == "__main__":
111
+ main()
@@ -0,0 +1,243 @@
1
+ """
2
+ Leo Studio Pro — Remote Worker File Cache
3
+ ==========================================
4
+ SHA-256 keyed LRU cache for uploaded model files.
5
+
6
+ Usage:
7
+ from file_cache import init_cache, resolve_model_file, get_cache_stats
8
+
9
+ # Call once at startup (e.g., from server.py):
10
+ init_cache(models_dir="/path/to/models", max_cache_gb=4.0)
11
+
12
+ # In an endpoint:
13
+ path = await resolve_model_file(upload_file, model_hash)
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ import os
20
+ import time
21
+ from pathlib import Path
22
+ from typing import TYPE_CHECKING
23
+
24
+ import aiofiles
25
+
26
+ if TYPE_CHECKING:
27
+ from fastapi import UploadFile
28
+
29
+ logger = logging.getLogger("remote_worker.file_cache")
30
+
31
+ # ---------------------------------------------------------------------------
32
+ # Module-level state — overridden by init_cache()
33
+ # ---------------------------------------------------------------------------
34
+
35
+ _cache_dir: Path = Path("models") / "_upload_cache"
36
+ _max_cache_bytes: int = 4 * 1024**3 # 4 GB default
37
+
38
+ # sha256_hex → local Path
39
+ _model_cache: dict[str, Path] = {}
40
+
41
+
42
+ # ---------------------------------------------------------------------------
43
+ # Initialisation
44
+ # ---------------------------------------------------------------------------
45
+
46
+
47
+ def init_cache(models_dir: str | Path, max_cache_gb: float) -> None:
48
+ """Configure the cache directory and size limit.
49
+
50
+ Call this once at worker startup before any upload is handled.
51
+ Thread-safe for read-heavy workloads; writes are serialised by the
52
+ single-threaded asyncio event loop.
53
+ """
54
+ global _cache_dir, _max_cache_bytes # noqa: PLW0603
55
+
56
+ _cache_dir = Path(models_dir) / "_upload_cache"
57
+ _cache_dir.mkdir(parents=True, exist_ok=True)
58
+ _max_cache_bytes = int(max_cache_gb * 1024**3)
59
+
60
+ logger.info(
61
+ "File cache initialised: dir=%s limit=%.2f GB",
62
+ _cache_dir,
63
+ max_cache_gb,
64
+ )
65
+
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # Core API
69
+ # ---------------------------------------------------------------------------
70
+
71
+
72
+ async def resolve_model_file(upload: "UploadFile", model_hash: str) -> Path:
73
+ """Return a local Path for *upload*, writing to disk only on cache miss.
74
+
75
+ Args:
76
+ upload: FastAPI ``UploadFile`` object (the file body will be read if
77
+ there is a cache miss).
78
+ model_hash: SHA-256 hex digest of the file, supplied by the caller.
79
+ If empty / falsy the file is always written fresh (no
80
+ caching).
81
+
82
+ Returns:
83
+ Absolute ``Path`` to the locally cached file.
84
+ """
85
+ # ---- Cache hit -------------------------------------------------------
86
+ if model_hash and model_hash in _model_cache:
87
+ cached_path = _model_cache[model_hash]
88
+ if cached_path.exists():
89
+ _touch_lru(model_hash)
90
+ logger.debug("Cache hit: hash=%s path=%s", model_hash[:12], cached_path)
91
+ return cached_path
92
+ # File was deleted externally — remove stale entry and fall through
93
+ logger.warning("Stale cache entry removed: hash=%s path=%s", model_hash[:12], cached_path)
94
+ del _model_cache[model_hash]
95
+
96
+ # ---- Cache miss — write to disk -------------------------------------
97
+ _cache_dir.mkdir(parents=True, exist_ok=True)
98
+
99
+ filename = upload.filename or "unknown"
100
+ if model_hash:
101
+ dest = _cache_dir / f"{model_hash}_{filename}"
102
+ else:
103
+ # No hash supplied — use timestamp to avoid collisions
104
+ dest = _cache_dir / f"nohash_{int(time.time() * 1000)}_{filename}"
105
+
106
+ data = await upload.read()
107
+ async with aiofiles.open(dest, "wb") as f:
108
+ await f.write(data)
109
+
110
+ logger.info(
111
+ "Cache miss — written: hash=%s size=%d bytes path=%s",
112
+ (model_hash[:12] if model_hash else "N/A"),
113
+ len(data),
114
+ dest,
115
+ )
116
+
117
+ if model_hash:
118
+ _model_cache[model_hash] = dest
119
+
120
+ _evict_if_needed()
121
+ return dest
122
+
123
+
124
+ # ---------------------------------------------------------------------------
125
+ # LRU helpers
126
+ # ---------------------------------------------------------------------------
127
+
128
+
129
+ def _touch_lru(model_hash: str) -> None:
130
+ """Update the mtime of the cached file to mark it as recently used."""
131
+ path = _model_cache.get(model_hash)
132
+ if path and path.exists():
133
+ now = time.time()
134
+ try:
135
+ os.utime(path, (now, now))
136
+ except OSError as exc:
137
+ logger.warning("Could not touch LRU file %s: %s", path, exc)
138
+
139
+
140
+ def _evict_if_needed() -> None:
141
+ """Remove least-recently-used cache files until total size ≤ limit.
142
+
143
+ Ordering is determined by ``os.path.getmtime`` (oldest = LRU).
144
+ Only files whose hash is tracked in ``_model_cache`` are candidates
145
+ for eviction; untracked files in the directory are left untouched.
146
+ """
147
+ if not _model_cache:
148
+ return
149
+
150
+ # Build list of (mtime, hash, path) for tracked files that exist
151
+ entries: list[tuple[float, str, Path]] = []
152
+ for h, p in list(_model_cache.items()):
153
+ if p.exists():
154
+ try:
155
+ mtime = os.path.getmtime(p)
156
+ except OSError:
157
+ mtime = 0.0
158
+ entries.append((mtime, h, p))
159
+
160
+ # Sort oldest-first (LRU at front)
161
+ entries.sort(key=lambda e: e[0])
162
+
163
+ total_bytes = sum(p.stat().st_size for _, _, p in entries if p.exists())
164
+
165
+ if total_bytes <= _max_cache_bytes:
166
+ return
167
+
168
+ logger.info(
169
+ "Cache eviction triggered: total=%.2f GB limit=%.2f GB",
170
+ total_bytes / 1024**3,
171
+ _max_cache_bytes / 1024**3,
172
+ )
173
+
174
+ for mtime, h, p in entries:
175
+ if total_bytes <= _max_cache_bytes:
176
+ break
177
+ try:
178
+ file_size = p.stat().st_size
179
+ p.unlink()
180
+ del _model_cache[h]
181
+ total_bytes -= file_size
182
+ logger.info(
183
+ "Evicted (LRU): hash=%s size=%d bytes mtime=%s",
184
+ h[:12],
185
+ file_size,
186
+ time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(mtime)),
187
+ )
188
+ except OSError as exc:
189
+ logger.warning("Could not evict %s: %s", p, exc)
190
+
191
+
192
+ # ---------------------------------------------------------------------------
193
+ # Stats
194
+ # ---------------------------------------------------------------------------
195
+
196
+
197
+ def get_cache_stats() -> dict:
198
+ """Return a snapshot of cache state for logging / health endpoints.
199
+
200
+ Returns:
201
+ dict with keys:
202
+ ``num_entries`` — number of tracked cache entries
203
+ ``total_bytes`` — combined size of all tracked files on disk
204
+ ``total_gb`` — same in gigabytes (float, 4 dp)
205
+ ``max_bytes`` — configured limit
206
+ ``max_gb`` — configured limit in gigabytes
207
+ ``cache_dir`` — absolute path to cache directory (str)
208
+ ``entries`` — list of {hash_prefix, filename, size_bytes,
209
+ last_used} sorted newest-first
210
+ """
211
+ entries_detail: list[dict] = []
212
+ total_bytes = 0
213
+
214
+ for h, p in list(_model_cache.items()):
215
+ if p.exists():
216
+ try:
217
+ size = p.stat().st_size
218
+ mtime = os.path.getmtime(p)
219
+ except OSError:
220
+ size = 0
221
+ mtime = 0.0
222
+ total_bytes += size
223
+ entries_detail.append(
224
+ {
225
+ "hash_prefix": h[:16],
226
+ "filename": p.name,
227
+ "size_bytes": size,
228
+ "last_used": time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime(mtime)),
229
+ }
230
+ )
231
+
232
+ # Sort newest-first for readability
233
+ entries_detail.sort(key=lambda e: e["last_used"], reverse=True)
234
+
235
+ return {
236
+ "num_entries": len(entries_detail),
237
+ "total_bytes": total_bytes,
238
+ "total_gb": round(total_bytes / 1024**3, 4),
239
+ "max_bytes": _max_cache_bytes,
240
+ "max_gb": round(_max_cache_bytes / 1024**3, 4),
241
+ "cache_dir": str(_cache_dir),
242
+ "entries": entries_detail,
243
+ }
File without changes
@@ -0,0 +1 @@
1
+ {"pth_path": "assets/weights/kikiV1.pth", "index_path": "logs/kikiV1.index", "sg_hostapi": "MME", "sg_wasapi_exclusive": false, "sg_input_device": "VoiceMeeter Output (VB-Audio Vo", "sg_output_device": "VoiceMeeter Input (VB-Audio Voi", "sr_type": "sr_device", "threhold": -60.0, "pitch": 12.0, "formant": 0.0, "rms_mix_rate": 0.5, "index_rate": 0.0, "block_time": 0.15, "crossfade_length": 0.08, "extra_time": 2.0, "n_cpu": 4.0, "use_jit": false, "use_pv": false, "f0method": "fcpe"}