""" 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 /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)."