Files

87 lines
6.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Owned audio.cpp music jobs, coordinated GPU lease, no persistent prompts."""
import os,signal,subprocess,threading,time,uuid,wave
from pathlib import Path
from profiles import music_recipe
from image_test import probe
class Music:
def __init__(self,root,profiles,catalog,runtime,scheduler):
self.root=Path(root);self.profiles=profiles;self.catalog=catalog;self.runtime=runtime;self.scheduler=scheduler;self.lock=threading.RLock();self.job=None;self.process=None;self.cancel=threading.Event()
def blockers(self,p):
if not music_recipe(p['model']):return ['Für diese Musik-Modellfamilie fehlt noch eine geprüfte audio.cpp-Anbindung.']
result=[] if self.runtime.status()['installed'] else ['audio.cpp zuerst unter Laufzeiten installieren.']
for role,info in music_recipe(p['model']).items():
try:self.profiles._component(p['model'],role,p.get('components',{}).get(role))
except ValueError:result.append(info['label']+' fehlt oder ist nicht zugeordnet.')
return result
def status(self):
with self.lock:return dict(job=dict(self.job) if self.job else None,loaded=bool(self.process and self.process.poll() is None),mode=self.scheduler.gpu_mode,runtime_installed=self.runtime.status()['installed'])
def start(self,profile_id,lyrics,style,seed=1234,max_tokens=400,steps=8,wait=False):
if not isinstance(lyrics,str) or not 1<=len(lyrics.strip())<=8000:raise ValueError('Songtext mit 1–8000 Zeichen eingeben.')
if not isinstance(style,str) or not 1<=len(style.strip())<=2000:raise ValueError('Musikstil mit 1–2000 Zeichen eingeben.')
for value,lo,hi in ((seed,0,2**31-1),(max_tokens,32,9000),(steps,1,32)):
if type(value) is not int or not lo<=value<=hi:raise ValueError('Ungültige Musikparameter.')
if self.scheduler.gpu_mode!='music':raise ValueError('Bitte in der Übersicht auf Musik umschalten.')
with self.lock:
if self.job and self.job['state']=='running':raise ValueError('Musikauftrag läuft bereits.')
p=next((p for p in self.profiles.status()['profiles'] if p['id']==profile_id and p['kind']=='music'),None)
if not p or not p['runnable']:raise ValueError('Musikprofil noch nicht vollständig eingerichtet.')
self.cancel.clear();self.root.mkdir(parents=True,exist_ok=True,mode=0o700)
self.job=dict(id=uuid.uuid4().hex,state='running',phase='Wartet auf Deck-GPU-Freigabe',profile_id=profile_id,started_at=time.time(),max_tokens=max_tokens)
threading.Thread(target=self._run,args=(p,lyrics,style,seed,max_tokens,steps,wait),daemon=True).start();return dict(self.job)
def _run(self,p,lyrics,style,seed,max_tokens,steps,wait):
release=None;state='failed';phase='Musikgenerierung fehlgeschlagen.';directory=self.root/self.job['id'];log=directory/'worker.tmp'
try:
release=self.scheduler.image_reservation(wait=wait,kind="music")
if self.cancel.is_set():raise InterruptedError()
required=int(p['model']['size']/1024**2)+1800
gpu=next((g for g in probe() if 'RTX 5080' in g['name'] and not g['processes'] and g['free_mib']>required),None)
if not gpu:raise ValueError('RTX 5080 nicht ausreichend frei. Fremde GPU-Prozesse werden nicht beendet.')
directory.mkdir(mode=0o700);bundle=directory/'package';bundle.mkdir();(bundle/'sidecars').mkdir()
def link(entry,name):
source=self.catalog.root/entry['id']/('model'+Path(entry['file']).suffix)
(bundle/name).symlink_to(source.resolve())
link(p['model'],p['model']['file'])
for role in music_recipe(p['model']):
entry=self.profiles._component(p['model'],role,p['components'][role]);link(entry,entry['file'])
vae=self.catalog.entry(p['components']['vae'])['file'];cli=self.runtime.paths()[0]
args=[str(cli),'--task','gen','--family','yue2','--model',str(bundle),'--backend','cuda','--device','0','--threads','6','--session-option','yue2.model_gguf='+p['model']['file'],'--session-option','yue2.vae_gguf='+vae,'--lyrics',lyrics,'--request-option','style='+style,'--request-option','cot=off','--request-option','semantic_min_tokens=0','--request-option','semantic_max_tokens='+str(max_tokens),'--request-option','num_inference_steps='+str(steps),'--seed',str(seed),'--out',str(directory/'result.wav'),'--log']
with self.lock:self.job.update(phase='YuE2 wird auf der RTX 5080 geladen · anschließend Musik erzeugen',gpu=gpu['name'])
with log.open('w') as out:
with self.lock:
if self.cancel.is_set():raise InterruptedError()
self.process=subprocess.Popen(args,stdout=out,stderr=subprocess.STDOUT,start_new_session=True,env=dict(os.environ,CUDA_VISIBLE_DEVICES=gpu['uuid'],OMP_NUM_THREADS='6'))
deadline=time.monotonic()+1800
while self.process.poll() is None:
if self.cancel.wait(.5):raise InterruptedError()
if time.monotonic()>deadline:raise ValueError('Zeitlimit von 30 Minuten erreicht.')
with self.lock:self.job['elapsed_seconds']=round(time.time()-self.job['started_at'])
if self.process.returncode:
output=log.read_text(errors='replace').lower()
if 'out of memory' in output or 'cuda error 2' in output:raise ValueError('GPU-Speicher reicht nicht (Out of Memory). Kleinere Quantisierung oder kürzeren Test wählen.')
raise ValueError('audio.cpp hat die Generierung abgebrochen. Modellpaket oder Laufzeit prüfen.')
with wave.open(str(directory/'result.wav'),'rb') as wav:
if wav.getnframes()<1 or wav.getnchannels() not in (1,2):raise ValueError('Ungültige WAV-Ausgabe.')
duration=wav.getnframes()/wav.getframerate()
(directory/'complete').touch();state='complete';phase='Musik fertig · Modell entladen'
with self.lock:self.job['duration_seconds']=round(duration,2)
except InterruptedError:state='cancelled';phase='Musikgenerierung abgebrochen · Modell entladen'
except Exception as exc:phase=str(exc) if isinstance(exc,ValueError) else 'Musikgenerierung fehlgeschlagen. Modellpaket und Laufzeit prüfen.'
finally:
process=self.process
if process and process.poll() is None:
try:os.killpg(process.pid,signal.SIGTERM);process.wait(5)
except subprocess.TimeoutExpired:os.killpg(process.pid,signal.SIGKILL);process.wait()
except ProcessLookupError:pass
log.unlink(missing_ok=True)
with self.lock:self.process=None;self.job.update(state=state,phase=phase,finished_at=time.time())
if release:release()
def stop(self):
self.cancel.set();return {'cancellation_requested':True}
def audio(self,ident):
if not isinstance(ident,str) or len(ident)!=32 or any(c not in '0123456789abcdef' for c in ident):raise ValueError('Ungültige Auftrag-ID.')
directory=self.root/ident
if not (directory/'complete').is_file():raise ValueError('Musik noch nicht fertig.')
wav=directory/'result.wav'
if wav.stat().st_size>256*1024**2:raise ValueError('Audioausgabe zu groß.')
return wav.read_bytes()