Run YuE2 music profiles through audio.cpp with WAV API and test UI

This commit is contained in:
Mikei386
2026-09-30 22:03:48 +02:00
parent 94bfb1ac78
commit 0ca40bc624
20 changed files with 248 additions and 26 deletions
+21 -5
View File
@@ -39,7 +39,7 @@ def chat_upstream_error(status, raw):
class Endpoint:
def __init__(self,root,profiles,worker,scheduler,images,credentials,management_port):
self.root=Path(root);self.profiles=profiles;self.worker=worker;self.scheduler=scheduler;self.images=images;self.tts=None;self.stt=None;self.video=None;self.credentials=credentials
self.root=Path(root);self.profiles=profiles;self.worker=worker;self.scheduler=scheduler;self.images=images;self.tts=None;self.stt=None;self.music=None;self.video=None;self.credentials=credentials
self.management_port=management_port;self.lock=threading.RLock();self.http=None;self.thread=None;self.state='stopped';self.error=None;self.inflight=0
self.allowed_ports=[int(p) for p in os.environ.get('DECK_API_PORTS','').split(',') if p]
self.config=dict(port=self.allowed_ports[0] if self.allowed_ports else 8120,enabled_profiles=[],autostart=False)
@@ -58,9 +58,9 @@ class Endpoint:
return [dict(p,enabled=p['id'] in enabled) for p in rows if p['kind']!='video']
def status(self):
rows=self.rows();worker=self.worker.status();job=self.images.status()['job'];counts={}
for key,kind in [('llm','chat'),('image','image'),('tts','audio'),('stt','stt')]:
for key,kind in [('llm','chat'),('image','image'),('tts','audio'),('stt','stt'),('music','music')]:
subset=[p for p in rows if p['kind']==kind]
counts[key]=dict(enabled=sum(p['enabled'] for p in subset),available=sum(p['enabled'] and p['runnable'] for p in subset),loaded=bool(worker['state']=='ready' and kind=='chat') if kind=='chat' else bool(self.tts and self.tts.status().get('loaded')) if kind=='audio' else bool(self.stt and self.stt.status().get('loaded')) if kind=='stt' else bool(kind=='image' and job and job['state']=='running'),supported=kind in ('chat','image') or (kind=='audio' and self.tts is not None) or (kind=='stt' and self.stt is not None))
counts[key]=dict(enabled=sum(p['enabled'] for p in subset),available=sum(p['enabled'] and p['runnable'] for p in subset),loaded=bool(worker['state']=='ready' and kind=='chat') if kind=='chat' else bool(self.tts and self.tts.status().get('loaded')) if kind=='audio' else bool(self.stt and self.stt.status().get('loaded')) if kind=='stt' else bool(self.music and self.music.status().get('loaded')) if kind=='music' else bool(kind=='image' and job and job['state']=='running'),supported=kind in ('chat','image') or (kind=='audio' and self.tts is not None) or (kind=='stt' and self.stt is not None) or (kind=='music' and self.music is not None))
scheduler_status=self.scheduler.status()
with self.lock:return dict(video=self.video.status() if self.video else None,state=self.state,reachable=bool(self.thread and self.thread.is_alive() and self.state=='running'),port=self.config['port'],bind=os.environ.get('DECK_API_BIND','127.0.0.1'),base_url=f"http://127.0.0.1:{self.config['port']}/v1",allowed_ports=self.allowed_ports,error=self.error,counts=counts,worker=worker,scheduler=scheduler_status,profiles=[dict(id=p['id'],name=p['name'],kind=p['kind'],enabled=p['enabled'],runnable=p['runnable'],blockers=p['blockers']) for p in rows],active_requests=self.inflight)
def configure(self,data):
@@ -127,6 +127,7 @@ class Endpoint:
# Process shutdown does not change the user's autostart preference.
with self.lock:self.state='stopping';http=self.http
if http:http.shutdown();http.server_close()
if self.music:self.music.stop()
if self.tts:self.tts.stop()
if self.stt:self.stt.stop()
self.images.stop();self.worker.stop()
@@ -203,7 +204,7 @@ class APIHandler(BaseHTTPRequestHandler):
try:ep.video.relay(self) if callable(getattr(type(ep.video),'relay',None)) else relay(self,video['selected'])
except ValueError as exc:raise APIError(str(exc)) from None
return
model_routes={'/v1/models':'chat','/v1/images/models':'image','/v1/audio/speech/models':'audio','/v1/audio/transcriptions/models':'stt'}
model_routes={'/v1/models':'chat','/v1/images/models':'image','/v1/audio/speech/models':'audio','/v1/audio/transcriptions/models':'stt','/v1/audio/music/models':'music'}
if self.command=='GET' and self.path in model_routes:return self.send(ep.model_list(model_routes[self.path]))
if self.command=='GET' and self.path=='/health':return self.send({'status':'ok','service':'athena-deck-api'})
if self.video_route(ep):return
@@ -219,7 +220,7 @@ class APIHandler(BaseHTTPRequestHandler):
fields['n']=int(fields.get('n','1'))
return self.image(ep,fields,images)
except ValueError as exc:raise APIError(str(exc)) from None
if self.path not in ('/v1/chat/completions','/v1/images/generations','/v1/audio/speech'):raise APIError('Route nicht implementiert.',404)
if self.path not in ('/v1/chat/completions','/v1/images/generations','/v1/audio/speech','/v1/audio/music'):raise APIError('Route nicht implementiert.',404)
if self.headers.get('Transfer-Encoding'):raise APIError('Chunked Upload wird nicht unterstützt.')
try:length=int(self.headers.get('Content-Length','0'))
except ValueError:raise APIError('Ungültige Content-Length.') from None
@@ -228,6 +229,7 @@ class APIHandler(BaseHTTPRequestHandler):
except (ValueError,UnicodeError):raise APIError('Ungültiges JSON.') from None
if not isinstance(data,dict):raise APIError('JSON-Objekt erforderlich.')
if self.path=='/v1/chat/completions':return self.chat(ep,data)
if self.path=='/v1/audio/music':return self.music_generation(ep,data)
if self.path=='/v1/audio/speech':return self.speech(ep,data)
return self.image(ep,data)
except (APIError,InferenceError) as exc:
@@ -272,6 +274,20 @@ class APIHandler(BaseHTTPRequestHandler):
except (ValueError,OSError):time.sleep(.25);continue
self.sent=True;self.send_response(200);self.send_header('Content-Type','audio/wav');self.send_header('Content-Length',str(len(body)));self.send_header('Cache-Control','no-store');self.send_header('Connection','close');self.end_headers();self.wfile.write(body);return
raise APIError('TTS-Zeitlimit überschritten.',504)
def music_generation(self,ep,data):
if not ep.music:raise APIError('Musikworker nicht eingerichtet.',501)
if set(data)-{'model','lyrics','style','seed','max_tokens','steps'}:raise APIError('Nicht unterstützte Musikfelder.')
profile=ep.find_profile(data.get('model'),'music')
try:job=ep.music.start(profile['id'],data.get('lyrics'),data.get('style'),seed=data.get('seed',1234),max_tokens=data.get('max_tokens',400),steps=data.get('steps',8),wait=True)
except ValueError as exc:raise APIError(str(exc)) from None
end=time.monotonic()+1850
while time.monotonic()<end:
current=ep.music.status()['job']
if current and current['id']==job['id'] and current['state'] in ('failed','cancelled'):raise APIError(current['phase'],503)
try:body=ep.music.audio(job['id'])
except (ValueError,OSError):time.sleep(.5);continue
self.sent=True;self.send_response(200);self.send_header('Content-Type','audio/wav');self.send_header('Content-Length',str(len(body)));self.send_header('Cache-Control','no-store');self.end_headers();self.wfile.write(body);return
ep.music.stop();raise APIError('Musik-Zeitlimit überschritten.',504)
def chat(self,ep,data):
try:data,self.compatibility=normalize_chat(data)
except CompatibilityError as exc:raise APIError(str(exc)) from None