"""Own CUDA environment and pinned official CustomVoice model, installable via GUI.""" import json,sys,time,os,uuid,shutil,threading,hashlib from pathlib import Path from image_runtime import ImageRuntime REPO='Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice' REVISION='0c0e3051f131929182e2c023b9537f8b1c68adfe' SPEAKERS=['Vivian','Serena','Uncle_Fu','Dylan','Eric','Ryan','Aiden','Ono_Anna','Sohee'] LANGUAGES=['Auto','German','English','Chinese','Japanese','Korean','French','Russian','Portuguese','Spanish','Italian'] class TTSRuntime(ImageRuntime): def paths(self): marker=self.root/'active.json' if marker.exists(): ident=json.loads(marker.read_text()).get('id','') if len(ident)==32 and all(c in '0123456789abcdef' for c in ident):return self.root/ident/'python/bin/python',self.root/ident/'model' return self.root/'missing/python',self.root/'missing/model' def status(self): with self.lock: python,model=self.paths() return dict(installed=python.is_file() and (model/'ready').exists(),job=dict(self.job) if self.job else None,repo=REPO,revision=REVISION,speakers=SPEAKERS,languages=LANGUAGES) def ensure_library(self): if not self.status()['installed']:raise ValueError('Zuerst TTS einrichten.') _,model=self.paths();ident=hashlib.sha256((REPO+REVISION+'model.safetensors').encode()).hexdigest();target=self.root.parent/'models'/ident;target.mkdir(parents=True,exist_ok=True,mode=0o700) if (target/'entry.json').is_file() and (target/'model.safetensors').is_file() and json.loads((target/'entry.json').read_text()).get('sha256'):return ident dest=target/'model.safetensors' if not dest.exists():dest.symlink_to(model/'model.safetensors') digest=hashlib.sha256() with (model/'model.safetensors').open('rb') as source: for block in iter(lambda:source.read(1024*1024),b''):digest.update(block) entry=dict(repo=REPO,revision=REVISION,file='model.safetensors',size=(model/'model.safetensors').stat().st_size,kind='audio',downloaded_at=time.time(),sha256=digest.hexdigest(),upstream_hash_verified=False,state='downloaded',runtime_ready=True) tmp=target/'entry.tmp';tmp.write_text(json.dumps(entry));tmp.replace(target/'entry.json');return ident def start(self): with self.lock: if self.job and self.job['state']=='running':raise ValueError('Installation läuft bereits.') if self.status()['installed']:self.ensure_library();return self.status() if sys.platform!='linux':raise ValueError('Die TTS-Laufzeit benötigt Debian/Linux mit NVIDIA-CUDA.') self.root.mkdir(parents=True,exist_ok=True,mode=0o700) if shutil.disk_usage(self.root).free<25*1024**3:raise ValueError('Mindestens 25 GiB freier Speicher erforderlich.') self.cancel.clear();self.job=dict(id=uuid.uuid4().hex,state='running',phase='TTS-Einrichtung vorbereitet',started_at=time.time());self._save() self.worker=threading.Thread(target=self._run,daemon=True);self.worker.start();return self.status() def _run(self): base=self.root/self.job['id'];python=base/'python/bin/python';model=base/'model';state='failed';phase='Einrichtung fehlgeschlagen' try: base.mkdir(mode=0o700) self._phase('Eigene TTS-Python-Umgebung erstellen');self._command([sys.executable,'-m','venv',base/'python']) self._phase('CUDA-PyTorch installieren');self._command([python,'-m','pip','install','--no-cache-dir','torch==2.11.0','torchaudio==2.11.0','--index-url','https://download.pytorch.org/whl/cu128']) self._phase('Qwen-TTS 0.1.1 installieren');self._command([python,'-m','pip','install','--no-cache-dir','-c',Path(__file__).parent/'deploy/tts-requirements.lock','qwen-tts==0.1.1']) self._command([python,'-m','pip','check']) self._phase('Offizielles CUDA-Modell und Sprach-Tokenizer laden · rund 4,5 GB') code='from huggingface_hub import snapshot_download; snapshot_download(repo_id='+repr(REPO)+',revision='+repr(REVISION)+',local_dir='+repr(str(model))+',allow_patterns=["*.json","*.txt","*.safetensors"],max_workers=2)' self._command([python,'-c',code]) self._phase('Dateien und Laufzeit prüfen');self._command([python,'-c','from qwen_tts import Qwen3TTSModel; import torch,soundfile; assert torch.version.cuda']) for name in ['model.safetensors','config.json','merges.txt','vocab.json','tokenizer_config.json','speech_tokenizer/model.safetensors']: if not (model/name).is_file():raise RuntimeError('Modelldatei fehlt: '+name) if self.cancel.is_set():raise InterruptedError() (model/'ready').write_text(REVISION) marker=self.root/'active.tmp';marker.write_text(json.dumps(dict(id=base.name)));marker.replace(self.root/'active.json') self.ensure_library() state='complete';phase='TTS bereit · CUDA-Modell mit eingebauten Stimmen installiert' except InterruptedError:state='cancelled';phase='TTS-Einrichtung abgebrochen' except Exception as exc:phase=str(exc) if isinstance(exc,RuntimeError) else 'TTS-Einrichtung fehlgeschlagen. Speicher und Internetverbindung prüfen.' finally: with self.lock:self.process=None;self.job.update(state=state,phase=phase,finished_at=time.time());self._save()