Spaces:
Running on Zero
Running on Zero
| """ | |
| Preset Manager | |
| ============== | |
| Allows users to save and load combinations of voice model + processing | |
| settings so they don't have to re-configure everything for each run. | |
| A preset stores: | |
| - name, description, voice_model | |
| - pitch, pitch_all, index_rate, filter_radius, rms_mix_rate, protect | |
| - f0_method, crepe_hop_length, steps | |
| - main_gain, backup_gain, inst_gain | |
| - reverb_rm_size, reverb_wet, reverb_dry, reverb_damping | |
| - output_format, extra_denoise | |
| Presets are persisted as JSON in <BASE_DIR>/data/presets.json. | |
| """ | |
| import json | |
| import os | |
| import time | |
| from typing import Any, Dict, List, Optional | |
| PRESET_FIELDS = [ | |
| "voice_model", | |
| "pitch", | |
| "pitch_all", | |
| "index_rate", | |
| "filter_radius", | |
| "rms_mix_rate", | |
| "protect", | |
| "f0_method", | |
| "crepe_hop_length", | |
| "steps", | |
| "main_gain", | |
| "backup_gain", | |
| "inst_gain", | |
| "reverb_rm_size", | |
| "reverb_wet", | |
| "reverb_dry", | |
| "reverb_damping", | |
| "output_format", | |
| "extra_denoise", | |
| ] | |
| # Built-in starter presets that ship with the app | |
| BUILTIN_PRESETS: List[Dict[str, Any]] = [ | |
| { | |
| "name": "Default", | |
| "description": "Balanced settings suitable for most songs.", | |
| "voice_model": "", | |
| "pitch": 0, | |
| "pitch_all": 0, | |
| "index_rate": 0.5, | |
| "filter_radius": 3, | |
| "rms_mix_rate": 0.25, | |
| "protect": 0.33, | |
| "f0_method": "rmvpe+", | |
| "crepe_hop_length": 128, | |
| "steps": 1, | |
| "main_gain": 0, | |
| "backup_gain": 0, | |
| "inst_gain": 0, | |
| "reverb_rm_size": 0.15, | |
| "reverb_wet": 0.2, | |
| "reverb_dry": 0.8, | |
| "reverb_damping": 0.7, | |
| "output_format": "mp3", | |
| "extra_denoise": True, | |
| "builtin": True, | |
| }, | |
| { | |
| "name": "Male -> Female", | |
| "description": "Pitch up by 12 semitones, smooth rmvpe detection.", | |
| "voice_model": "", | |
| "pitch": 1, | |
| "pitch_all": 0, | |
| "index_rate": 0.6, | |
| "filter_radius": 3, | |
| "rms_mix_rate": 0.25, | |
| "protect": 0.33, | |
| "f0_method": "rmvpe", | |
| "crepe_hop_length": 128, | |
| "steps": 1, | |
| "main_gain": 0, | |
| "backup_gain": 0, | |
| "inst_gain": 0, | |
| "reverb_rm_size": 0.15, | |
| "reverb_wet": 0.2, | |
| "reverb_dry": 0.8, | |
| "reverb_damping": 0.7, | |
| "output_format": "mp3", | |
| "extra_denoise": True, | |
| "builtin": True, | |
| }, | |
| { | |
| "name": "Female -> Male", | |
| "description": "Pitch down by 12 semitones, extra protection.", | |
| "voice_model": "", | |
| "pitch": -1, | |
| "pitch_all": 0, | |
| "index_rate": 0.6, | |
| "filter_radius": 3, | |
| "rms_mix_rate": 0.3, | |
| "protect": 0.25, | |
| "f0_method": "rmvpe", | |
| "crepe_hop_length": 128, | |
| "steps": 1, | |
| "main_gain": 0, | |
| "backup_gain": 0, | |
| "inst_gain": 0, | |
| "reverb_rm_size": 0.18, | |
| "reverb_wet": 0.18, | |
| "reverb_dry": 0.82, | |
| "reverb_damping": 0.7, | |
| "output_format": "mp3", | |
| "extra_denoise": True, | |
| "builtin": True, | |
| }, | |
| { | |
| "name": "Studio Quality", | |
| "description": "WAV output, mangio-crepe, max index rate.", | |
| "voice_model": "", | |
| "pitch": 0, | |
| "pitch_all": 0, | |
| "index_rate": 0.85, | |
| "filter_radius": 3, | |
| "rms_mix_rate": 0.25, | |
| "protect": 0.33, | |
| "f0_method": "mangio-crepe", | |
| "crepe_hop_length": 128, | |
| "steps": 2, | |
| "main_gain": 0, | |
| "backup_gain": 0, | |
| "inst_gain": 0, | |
| "reverb_rm_size": 0.12, | |
| "reverb_wet": 0.15, | |
| "reverb_dry": 0.85, | |
| "reverb_damping": 0.7, | |
| "output_format": "wav", | |
| "extra_denoise": True, | |
| "builtin": True, | |
| }, | |
| { | |
| "name": "Karaoke Mode", | |
| "description": "Reduced reverb, clean dry vocals for karaoke.", | |
| "voice_model": "", | |
| "pitch": 0, | |
| "pitch_all": 0, | |
| "index_rate": 0.5, | |
| "filter_radius": 3, | |
| "rms_mix_rate": 0.25, | |
| "protect": 0.33, | |
| "f0_method": "rmvpe+", | |
| "crepe_hop_length": 128, | |
| "steps": 1, | |
| "main_gain": 2, | |
| "backup_gain": -3, | |
| "inst_gain": -2, | |
| "reverb_rm_size": 0.08, | |
| "reverb_wet": 0.08, | |
| "reverb_dry": 0.92, | |
| "reverb_damping": 0.6, | |
| "output_format": "mp3", | |
| "extra_denoise": True, | |
| "builtin": True, | |
| }, | |
| { | |
| "name": "Concert Hall", | |
| "description": "Heavy reverb for a live-concert feel.", | |
| "voice_model": "", | |
| "pitch": 0, | |
| "pitch_all": 0, | |
| "index_rate": 0.5, | |
| "filter_radius": 3, | |
| "rms_mix_rate": 0.25, | |
| "protect": 0.33, | |
| "f0_method": "rmvpe+", | |
| "crepe_hop_length": 128, | |
| "steps": 1, | |
| "main_gain": 0, | |
| "backup_gain": 0, | |
| "inst_gain": 0, | |
| "reverb_rm_size": 0.55, | |
| "reverb_wet": 0.4, | |
| "reverb_dry": 0.6, | |
| "reverb_damping": 0.5, | |
| "output_format": "mp3", | |
| "extra_denoise": True, | |
| "builtin": True, | |
| }, | |
| ] | |
| class PresetManager: | |
| """Manages persistence and lookup of preset configurations.""" | |
| def __init__(self, presets_path: str): | |
| self.path = presets_path | |
| self._ensure_file() | |
| def _ensure_file(self) -> None: | |
| if not os.path.exists(self.path): | |
| os.makedirs(os.path.dirname(self.path), exist_ok=True) | |
| self._write(BUILTIN_PRESETS) | |
| def _read(self) -> List[Dict[str, Any]]: | |
| try: | |
| with open(self.path, "r", encoding="utf-8") as f: | |
| return json.load(f) | |
| except (json.JSONDecodeError, OSError): | |
| return list(BUILTIN_PRESETS) | |
| def _write(self, presets: List[Dict[str, Any]]) -> None: | |
| os.makedirs(os.path.dirname(self.path), exist_ok=True) | |
| with open(self.path, "w", encoding="utf-8") as f: | |
| json.dump(presets, f, indent=2, ensure_ascii=False) | |
| def list_names(self) -> List[str]: | |
| return [p["name"] for p in self._read()] | |
| def get(self, name: str) -> Optional[Dict[str, Any]]: | |
| for p in self._read(): | |
| if p["name"] == name: | |
| return p | |
| return None | |
| def save(self, name: str, description: str, **values) -> str: | |
| name = (name or "").strip() | |
| if not name: | |
| return "Error: preset name is required." | |
| presets = self._read() | |
| # Build the preset from provided values (only keep known fields) | |
| preset: Dict[str, Any] = { | |
| "name": name, | |
| "description": description or "", | |
| "builtin": False, | |
| "created_at": int(time.time()), | |
| } | |
| for field in PRESET_FIELDS: | |
| preset[field] = values.get(field, None) | |
| # Replace if exists (but never overwrite builtin) | |
| new_list = [] | |
| replaced = False | |
| for p in presets: | |
| if p["name"] == name: | |
| if p.get("builtin"): | |
| return f"Error: cannot overwrite built-in preset '{name}'." | |
| new_list.append(preset) | |
| replaced = True | |
| else: | |
| new_list.append(p) | |
| if not replaced: | |
| new_list.append(preset) | |
| self._write(new_list) | |
| return f"Saved preset '{name}'." | |
| def delete(self, name: str) -> str: | |
| presets = self._read() | |
| new_list = [] | |
| deleted = False | |
| for p in presets: | |
| if p["name"] == name: | |
| if p.get("builtin"): | |
| return f"Error: cannot delete built-in preset '{name}'." | |
| deleted = True | |
| continue | |
| new_list.append(p) | |
| if not deleted: | |
| return f"Error: preset '{name}' not found." | |
| self._write(new_list) | |
| return f"Deleted preset '{name}'." | |
| def reset_builtin(self) -> str: | |
| """Restore the built-in presets (keeps user presets).""" | |
| presets = self._read() | |
| names = {p["name"] for p in presets if not p.get("builtin")} | |
| new_list = [p for p in presets if not p.get("builtin")] + list(BUILTIN_PRESETS) | |
| self._write(new_list) | |
| return f"Restored {len(BUILTIN_PRESETS)} built-in presets ({len(names)} user presets kept)." | |