From 346f9ccbda6c299f1dfce717c57388b1916ee50b Mon Sep 17 00:00:00 2001 From: Koko210 Date: Tue, 13 Jan 2026 00:20:55 +0200 Subject: [PATCH] unified soprano to rvc script --- __pycache__/headless_rvc.cpython-310.pyc | Bin 0 -> 13190 bytes headless_rvc.py | 33 +- rvc_config.json | 23 ++ unified_soprano_rvc.py | 415 +++++++++++++++++++++++ 4 files changed, 469 insertions(+), 2 deletions(-) create mode 100644 __pycache__/headless_rvc.cpython-310.pyc create mode 100644 rvc_config.json create mode 100644 unified_soprano_rvc.py diff --git a/__pycache__/headless_rvc.cpython-310.pyc b/__pycache__/headless_rvc.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..378b02264a3424d2b2c164bd9f8995ff7e82c188 GIT binary patch literal 13190 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 'RVCConfig': + """Load configuration from JSON file""" + with open(config_path, 'r') as f: + data = json.load(f) + return cls(**data) + + def to_file(self, config_path: str): + """Save configuration to JSON file""" + with open(config_path, 'w') as f: + json.dump(asdict(self), f, indent=2) + + +class UnifiedPipeline: + """Unified Soprano TTS + RVC pipeline""" + + def __init__(self, rvc_config: RVCConfig, virtual_sink_name: str = "soprano_to_rvc"): + self.rvc_config = rvc_config + self.virtual_sink_name = virtual_sink_name + self.soprano = None + self.rvc_process = None + self.rvc_thread = None + self.soprano_stream = None + self.running = False + + # Soprano audio parameters + self.soprano_sample_rate = 32000 + self.virtual_sink_sample_rate = 48000 + + def ensure_virtual_sink(self): + """Ensure PulseAudio virtual sink exists""" + print("Checking virtual sink...") + + # Check if sink exists + result = subprocess.run( + ["pactl", "list", "sinks", "short"], + capture_output=True, + text=True + ) + + if self.virtual_sink_name not in result.stdout: + print(f"Creating virtual sink: {self.virtual_sink_name}") + subprocess.run([ + "pactl", "load-module", "module-null-sink", + f"sink_name={self.virtual_sink_name}", + f"sink_properties=device.description='Soprano_to_RVC_Virtual_Sink'", + f"rate={self.virtual_sink_sample_rate}", + "channels=2" + ]) + time.sleep(0.5) + else: + print(f"✓ Virtual sink '{self.virtual_sink_name}' already exists") + + def initialize_soprano(self): + """Initialize Soprano TTS""" + print("\n" + "="*70) + print("Initializing Soprano TTS...") + print("="*70) + + # Suppress verbose output during initialization + with open(os.devnull, 'w') as devnull: + with redirect_stdout(devnull), redirect_stderr(devnull): + self.soprano = SopranoTTS(device="cuda") + + # Open audio stream to virtual sink + try: + self.soprano_stream = sd.OutputStream( + device=self.virtual_sink_name, + samplerate=self.virtual_sink_sample_rate, + channels=2, + dtype='float32', + blocksize=1024 + ) + self.soprano_stream.start() + print("✓ Soprano TTS initialized successfully") + print(f" Output: {self.virtual_sink_name} ({self.virtual_sink_sample_rate}Hz, stereo)") + except Exception as e: + print(f"✗ Failed to open audio stream: {e}") + raise + + def start_rvc(self): + """Start headless RVC in a separate thread""" + print("\n" + "="*70) + print("Starting RVC Voice Conversion...") + print("="*70) + + def run_rvc(): + # Suppress logging from RVC + import logging + logging.getLogger('faiss').setLevel(logging.ERROR) + logging.getLogger('fairseq').setLevel(logging.ERROR) + + # Import here to avoid conflicts + from headless_rvc import HeadlessRVC, HeadlessRVCConfig + + # Redirect RVC output + with open(os.devnull, 'w') as devnull: + with redirect_stdout(devnull), redirect_stderr(devnull): + # Convert our config to HeadlessRVCConfig + config_dict = { + 'pth_path': self.rvc_config.pth, + 'index_path': self.rvc_config.index, + 'pitch': self.rvc_config.pitch, + 'formant': self.rvc_config.formant, + 'index_rate': self.rvc_config.index_rate, + 'filter_radius': self.rvc_config.filter_radius, + 'rms_mix_rate': self.rvc_config.rms_mix_rate, + 'protect': self.rvc_config.protect, + 'f0method': self.rvc_config.f0method, + 'input_device': self.rvc_config.input_device, + 'output_device': self.rvc_config.output_device, + 'samplerate': self.rvc_config.samplerate, + 'channels': self.rvc_config.channels, + 'block_time': self.rvc_config.block_time, + 'crossfade_time': self.rvc_config.crossfade_time, + 'extra_time': self.rvc_config.extra_time, + 'n_cpu': self.rvc_config.n_cpu, + 'I_noise_reduce': self.rvc_config.I_noise_reduce, + 'O_noise_reduce': self.rvc_config.O_noise_reduce, + 'use_pv': self.rvc_config.use_pv, + 'threshold': self.rvc_config.threshold + } + gui_config = HeadlessRVCConfig(config_dict) + + self.rvc = HeadlessRVC(gui_config) + self.rvc.start() + + # Keep running until stopped + while self.running: + time.sleep(0.1) + + # Suppress stop output too + with open(os.devnull, 'w') as devnull: + with redirect_stdout(devnull), redirect_stderr(devnull): + self.rvc.stop() + + self.rvc_thread = threading.Thread(target=run_rvc, daemon=True) + self.running = True + self.rvc_thread.start() + + # Wait for RVC to initialize + time.sleep(3) # Give it time to load models + print("✓ RVC initialized successfully") + + def stream_audio_chunk(self, audio_chunk): + """Stream an audio chunk to the virtual sink""" + if audio_chunk is None: + return + + # Convert torch tensor to numpy if needed + if torch.is_tensor(audio_chunk): + audio_chunk = audio_chunk.cpu().numpy() + + if len(audio_chunk) == 0: + return + + # Ensure float32 + audio_chunk = audio_chunk.astype(np.float32) + + # Resample from 32kHz to 48kHz + if self.soprano_sample_rate != self.virtual_sink_sample_rate: + num_samples_output = int(len(audio_chunk) * self.virtual_sink_sample_rate / self.soprano_sample_rate) + audio_chunk = scipy_signal.resample(audio_chunk, num_samples_output) + + # Clean audio (handle NaN/inf) + audio_chunk = np.nan_to_num(audio_chunk, nan=0.0, posinf=0.0, neginf=0.0) + audio_chunk = np.clip(audio_chunk, -1.0, 1.0) + + # Convert mono to stereo + if audio_chunk.ndim == 1: + audio_chunk = np.column_stack((audio_chunk, audio_chunk)) + + # Write to stream + try: + self.soprano_stream.write(audio_chunk) + except Exception as e: + print(f"Warning: Failed to write audio chunk: {e}") + + def process_text(self, text: str): + """Process text through TTS and stream to virtual sink""" + if not text.strip(): + return + + print(f"\n🎤 Processing: {text}", flush=True) + + # Generate and stream audio (suppress Soprano's verbose output) + with open(os.devnull, 'w') as devnull: + with redirect_stdout(devnull), redirect_stderr(devnull): + for audio_chunk in self.soprano.infer_stream(text): + if audio_chunk is not None: + self.stream_audio_chunk(audio_chunk) + + # Small silence at the end + silence = np.zeros(int(0.1 * self.virtual_sink_sample_rate), dtype=np.float32) + self.stream_audio_chunk(silence) + print("✓ Done\n", flush=True) + + def run(self): + """Run the unified pipeline""" + try: + # Setup + self.ensure_virtual_sink() + self.initialize_soprano() + self.start_rvc() + + print("\n" + "="*70) + print("UNIFIED SOPRANO TTS + RVC PIPELINE") + print("="*70) + print(f"\n✓ Ready! Voice conversion active (pitch: {self.rvc_config.pitch:+d} semitones)") + print("\nCommands:") + print(" - Type text and press Enter to generate speech") + print(" - Type 'quit' or 'exit' to stop") + print(" - Press Ctrl+C to stop") + print("="*70 + "\n") + + # Interactive loop + while self.running: + try: + text = input("💬 > ").strip() + + if text.lower() in ['quit', 'exit', 'q']: + break + + if text: + self.process_text(text) + + except EOFError: + break + except KeyboardInterrupt: + break + + finally: + self.cleanup() + + def cleanup(self): + """Clean up resources""" + print("\n\n⏹️ Stopping...") + self.running = False + + if self.soprano_stream: + self.soprano_stream.stop() + self.soprano_stream.close() + + if self.rvc_thread and self.rvc_thread.is_alive(): + self.rvc_thread.join(timeout=2) + + print("✓ Stopped") + print("👋 Goodbye!\n") + + +def main(): + parser = argparse.ArgumentParser( + description="Unified Soprano TTS + RVC Pipeline", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Run with command-line arguments + python unified_soprano_rvc.py --pth model.pth --index model.index --pitch 0 + + # Load from config file + python unified_soprano_rvc.py --config rvc_config.json + + # Save current config + python unified_soprano_rvc.py --pth model.pth --index model.index --save-config my_config.json + """ + ) + + # Config file options + parser.add_argument('--config', type=str, help='Load RVC configuration from JSON file') + parser.add_argument('--save-config', type=str, help='Save configuration to JSON file and exit') + + # RVC parameters + parser.add_argument('--pth', type=str, help='Path to RVC model (.pth file)') + parser.add_argument('--index', type=str, help='Path to index file') + parser.add_argument('--pitch', type=int, default=0, help='Pitch shift in semitones (default: 0)') + parser.add_argument('--formant', type=float, default=0.0, help='Formant shift (default: 0.0)') + parser.add_argument('--index-rate', type=float, default=0.75, help='Index rate (default: 0.75)') + parser.add_argument('--filter-radius', type=int, default=3, help='Filter radius (default: 3)') + parser.add_argument('--rms-mix-rate', type=float, default=0.25, help='RMS mix rate (default: 0.25)') + parser.add_argument('--protect', type=float, default=0.33, help='Protect voiceless consonants (default: 0.33)') + parser.add_argument('--f0method', type=str, default='rmvpe', + choices=['rmvpe', 'harvest', 'crepe', 'fcpe'], + help='F0 extraction method (default: rmvpe)') + + # Audio device settings + parser.add_argument('--input-device', type=str, default='soprano_rvc', + help='Input audio device for RVC (default: soprano_rvc)') + parser.add_argument('--output-device', type=str, help='Output audio device (default: system default)') + parser.add_argument('--samplerate', type=int, default=48000, help='Sample rate (default: 48000)') + + # Advanced options + parser.add_argument('--n-cpu', type=int, default=4, help='Number of CPU cores for F0 extraction (default: 4)') + parser.add_argument('--threshold', type=float, default=-60.0, help='Silence threshold in dB (default: -60)') + parser.add_argument('--I-noise-reduce', action='store_true', help='Enable input noise reduction') + parser.add_argument('--O-noise-reduce', action='store_true', help='Enable output noise reduction') + parser.add_argument('--no-use-pv', action='store_true', help='Disable phase vocoder') + + # Virtual sink name + parser.add_argument('--virtual-sink', type=str, default='soprano_to_rvc', + help='Name of virtual sink (default: soprano_to_rvc)') + + args = parser.parse_args() + + # Load or create config + if args.config: + print(f"Loading configuration from: {args.config}") + rvc_config = RVCConfig.from_file(args.config) + else: + # Validate required arguments + if not args.pth or not args.index: + parser.error("--pth and --index are required (or use --config)") + + rvc_config = RVCConfig( + pth=args.pth, + index=args.index, + pitch=args.pitch, + formant=args.formant, + index_rate=args.index_rate, + filter_radius=args.filter_radius, + rms_mix_rate=args.rms_mix_rate, + protect=args.protect, + f0method=args.f0method, + input_device=args.input_device, + output_device=args.output_device, + samplerate=args.samplerate, + n_cpu=args.n_cpu, + I_noise_reduce=args.I_noise_reduce, + O_noise_reduce=args.O_noise_reduce, + use_pv=not args.no_use_pv, + threshold=args.threshold + ) + + # Save config if requested + if args.save_config: + rvc_config.to_file(args.save_config) + print(f"✓ Configuration saved to: {args.save_config}") + return + + # Run pipeline + pipeline = UnifiedPipeline(rvc_config, virtual_sink_name=args.virtual_sink) + pipeline.run() + + +if __name__ == "__main__": + main()