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.
- leostudio-0.1.0/PKG-INFO +43 -0
- leostudio-0.1.0/README.md +13 -0
- leostudio-0.1.0/pyproject.toml +47 -0
- leostudio-0.1.0/setup.cfg +4 -0
- leostudio-0.1.0/src/leostudio/__init__.py +7 -0
- leostudio-0.1.0/src/leostudio/__main__.py +8 -0
- leostudio-0.1.0/src/leostudio/cli.py +111 -0
- leostudio-0.1.0/src/leostudio/file_cache.py +243 -0
- leostudio-0.1.0/src/leostudio/rvc/__init__.py +0 -0
- leostudio-0.1.0/src/leostudio/rvc/configs/config.json +1 -0
- leostudio-0.1.0/src/leostudio/rvc/configs/config.py +265 -0
- leostudio-0.1.0/src/leostudio/rvc/configs/inuse/v1/32k.json +46 -0
- leostudio-0.1.0/src/leostudio/rvc/configs/inuse/v1/40k.json +46 -0
- leostudio-0.1.0/src/leostudio/rvc/configs/inuse/v1/48k.json +46 -0
- leostudio-0.1.0/src/leostudio/rvc/configs/inuse/v2/32k.json +46 -0
- leostudio-0.1.0/src/leostudio/rvc/configs/inuse/v2/48k.json +46 -0
- leostudio-0.1.0/src/leostudio/rvc/configs/v1/32k.json +46 -0
- leostudio-0.1.0/src/leostudio/rvc/configs/v1/40k.json +46 -0
- leostudio-0.1.0/src/leostudio/rvc/configs/v1/48k.json +46 -0
- leostudio-0.1.0/src/leostudio/rvc/configs/v2/32k.json +46 -0
- leostudio-0.1.0/src/leostudio/rvc/configs/v2/48k.json +46 -0
- leostudio-0.1.0/src/leostudio/rvc/i18n/i18n.py +8 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/audio.py +74 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/fairseq_minimal/__init__.py +14 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/fairseq_minimal/checkpoint_utils.py +224 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/fairseq_minimal/namespace_stub.py +101 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/infer_pack/attentions.py +457 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/infer_pack/attentions_onnx.py +457 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/infer_pack/commons.py +169 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/infer_pack/models.py +1223 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/infer_pack/models_onnx.py +818 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/infer_pack/modules/F0Predictor/DioF0Predictor.py +91 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/infer_pack/modules/F0Predictor/F0Predictor.py +16 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/infer_pack/modules/F0Predictor/HarvestF0Predictor.py +87 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/infer_pack/modules/F0Predictor/PMF0Predictor.py +98 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/infer_pack/modules/F0Predictor/__init__.py +0 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/infer_pack/modules.py +612 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/infer_pack/onnx_inference.py +148 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/infer_pack/transforms.py +207 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/jit/__init__.py +163 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/jit/get_hubert.py +345 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/jit/get_rmvpe.py +15 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/jit/get_synthesizer.py +38 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/rmvpe.py +685 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/rtrvc.py +462 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/slicer2.py +260 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/train/data_utils.py +517 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/train/losses.py +58 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/train/mel_processing.py +127 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/train/process_ckpt.py +268 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/train/utils.py +485 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/dataset.py +183 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/layers.py +118 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/layers_123812KB .py +118 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/layers_123821KB.py +118 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/layers_33966KB.py +126 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/layers_537227KB.py +126 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/layers_537238KB.py +126 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/layers_new.py +125 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/model_param_init.py +68 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/nets.py +122 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/nets_123812KB.py +122 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/nets_123821KB.py +122 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/nets_33966KB.py +122 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/nets_537227KB.py +122 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/nets_537238KB.py +122 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/nets_61968KB.py +122 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/nets_new.py +133 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/lib_v5/spec_utils.py +674 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/lib/uvr5_pack/utils.py +121 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/modules/train/extract/extract_f0_print.py +176 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/modules/train/extract/extract_f0_rmvpe.py +144 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/modules/train/extract/extract_f0_rmvpe_dml.py +140 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/modules/train/extract_feature_print.py +151 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/modules/train/preprocess.py +155 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/modules/train/train.py +645 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/modules/train/train_index.py +90 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/modules/vc/__init__.py +0 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/modules/vc/modules.py +308 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/modules/vc/pipeline.py +464 -0
- leostudio-0.1.0/src/leostudio/rvc/infer/modules/vc/utils.py +113 -0
- leostudio-0.1.0/src/leostudio/rvc/train_wrapper_headless.py +498 -0
- leostudio-0.1.0/src/leostudio/server.py +2453 -0
- leostudio-0.1.0/src/leostudio/setup_helper.py +113 -0
- leostudio-0.1.0/src/leostudio.egg-info/PKG-INFO +43 -0
- leostudio-0.1.0/src/leostudio.egg-info/SOURCES.txt +93 -0
- leostudio-0.1.0/src/leostudio.egg-info/dependency_links.txt +1 -0
- leostudio-0.1.0/src/leostudio.egg-info/entry_points.txt +2 -0
- leostudio-0.1.0/src/leostudio.egg-info/requires.txt +21 -0
- leostudio-0.1.0/src/leostudio.egg-info/top_level.txt +1 -0
- leostudio-0.1.0/tests/test_auth_middleware.py +257 -0
- leostudio-0.1.0/tests/test_error_response.py +525 -0
- leostudio-0.1.0/tests/test_file_cache_lru_property.py +352 -0
- leostudio-0.1.0/tests/test_file_cache_property.py +394 -0
- leostudio-0.1.0/tests/test_health_endpoint.py +335 -0
leostudio-0.1.0/PKG-INFO
ADDED
|
@@ -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,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"}
|