87 lines
6.6 KiB
Python
87 lines
6.6 KiB
Python
"""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()
|