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