"""VoxCPM2 speech and reference-voice generation, serialized by Deck GPU scheduler.""" import io,json,os,shutil,signal,subprocess,threading,time,uuid,wave from pathlib import Path from tts_test import TTSTests from image_test import probe,cgroup_headroom from profiles import voice_recipe WORKER=Path(__file__).with_name('voxcpm_worker.py') def validate_reference(audio): if not isinstance(audio,bytes) or not 0=6500),None) if not gpu:raise ValueError('RTX '+device+' benötigt mindestens 6,5 GiB freien VRAM und darf nicht fremd belegt sein.') headroom=cgroup_headroom() if headroom is not None and headroom<8*1024**3:raise ValueError('Mindestens 8 GiB freier Deck-Systemspeicher erforderlich.') self.root.mkdir(parents=True,exist_ok=True,mode=0o700) self.job.update(phase='VoxCPM2 wird geladen',gpu=gpu['name']);self.gpu_uuid=gpu['uuid'] threading.Thread(target=self._run_voice,args=(p,ident,dict(text=text,cfg=cfg,steps=steps,seed=seed),reference,gpu,release),daemon=True).start();return dict(self.job) except Exception as exc: if release:release() with self.lock:self.job.update(state='failed',phase=str(exc),finished_at=time.time()) raise def _run_voice(self,p,ident,request,reference,gpu,release): root=self.root/ident;state='failed';phase='VoxCPM2-Ausführung fehlgeschlagen' try: root.mkdir(mode=0o700);self.stage(p,root) if reference is not None:(root/'reference.wav').write_bytes(reference) env={k:v for k,v in os.environ.items() if not k.startswith(('HF_','LLAMA_','DECK_'))};env.update(CUDA_VISIBLE_DEVICES=gpu['uuid'],HF_HUB_OFFLINE='1',TRANSFORMERS_OFFLINE='1',OMP_NUM_THREADS='4',HOME=str(root)) with self.lock: if self.cancel.is_set():raise InterruptedError() self.process=subprocess.Popen([str(self.runtime.paths()[0]),str(WORKER),str(root)],stdin=subprocess.PIPE,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,env=env,start_new_session=True,text=True) process=self.process process.stdin.write(json.dumps(request));process.stdin.close();process.stdin=None;deadline=time.monotonic()+600 while process.poll() is None: if self.cancel.wait(.25):raise InterruptedError() if time.monotonic()>deadline:raise ValueError('Zeitlimit für VoxCPM2 erreicht.') if (root/'phase').exists(): with self.lock:self.job['phase']=(root/'phase').read_text() if self.cancel.is_set():raise InterruptedError() if not (root/'result.json').exists():raise ValueError('VoxCPM2-Worker beendet ohne Ergebnis. Speicher oder Laufzeit prüfen.') result=json.loads((root/'result.json').read_text()) if not result['ok']:raise ValueError(result['error']) output=root/'result.wav' with wave.open(str(output)) as w: if w.getnframes()==0 or output.stat().st_size>32*1024**2:raise ValueError('Ungültige Audioausgabe.') state='complete';phase='Stimme fertig · Modell entladen' with self.lock:self.job.update(duration=result['duration'],sample_rate=result['sample_rate']) except InterruptedError:state='cancelled';phase='Stimmgenerierung abgebrochen · Modell entladen' except Exception as exc: if self.cancel.is_set():state='cancelled';phase='Stimmgenerierung abgebrochen · Modell entladen' else:phase=str(exc) if isinstance(exc,ValueError) else 'VoxCPM2-Ausführung fehlgeschlagen ('+type(exc).__name__+').' finally: self.unload_idle();shutil.rmtree(root/'model',ignore_errors=True) for name in ('reference.wav','phase'):(root/name).unlink(missing_ok=True) if state=='complete':(root/'complete').touch() with self.lock:self.job.update(state=state,phase=phase,finished_at=time.time()) release()