Files
Athena-Deck/tts_runtime.py
T

63 lines
4.9 KiB
Python

"""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()