Add audio separation web app and verified recovery sources

This commit is contained in:
Mikei386 committed 2026-10-01 16:19:33 +02:00
1 parent b5ee7c1e01
commit 433e6709cf
25 files changed
+381 -46

No files matched your search

+78
View File
@@ -0,0 +1,78 @@
"""Old Athena separation UI, remote Deck processing; no CUDA or models here."""
import io,json,os,secrets,subprocess,tempfile,time,urllib.request,urllib.error,zipfile
from pathlib import Path
from http.server import BaseHTTPRequestHandler,ThreadingHTTPServer
from email import policy
from email.parser import BytesParser
BASE='http://127.0.0.1:8108'
CONFIG=Path('/data/deck-connection.json')
TARGETS={'vocals':'model_bs_roformer_ep_317_sdr_12.9755.ckpt','drums':'htdemucs_ft.yaml','bass':'htdemucs_ft.yaml','guitar':'htdemucs_6s.yaml','piano':'htdemucs_6s.yaml','other':'htdemucs_6s.yaml'}
def request(path,body=None,ctype='application/json',token=None):
if token is None:token=json.loads(CONFIG.read_text())['token'] if CONFIG.exists() else ''
if not token:raise ValueError('Zuerst die Deck-Verbindung einrichten.')
req=urllib.request.Request(BASE+path,data=body,headers={'Authorization':'Bearer '+token,'Content-Type':ctype})
try:
with urllib.request.urlopen(req,timeout=90) as response:return response.read()
except urllib.error.HTTPError as exc:
try:message=json.loads(exc.read()).get('error','Deck-Anfrage abgelehnt')
except Exception:message='Deck-Anfrage abgelehnt'
raise ValueError(message if isinstance(message,str) else message.get('message','Deck-Anfrage abgelehnt')) from None
class Handler(BaseHTTPRequestHandler):
def log_message(self,*args):pass
def send(self,body,status=200,mime='application/json'):
if isinstance(body,dict):body=json.dumps(body).encode()
self.send_response(status);self.send_header('Content-Type',mime);self.send_header('Content-Length',str(len(body)));self.send_header('Cache-Control','no-store');self.send_header('X-Content-Type-Options','nosniff');self.send_header('Content-Security-Policy',"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'");self.end_headers();self.wfile.write(body)
def do_GET(self):
if self.path=='/health':return self.send({'status':'ok','connected':CONFIG.exists()})
if self.path=='/':return self.send(Path('/app/index.html').read_bytes(),mime='text/html; charset=utf-8')
if self.path=='/api/status':
try:return self.send(request('/api/v1/separator-runtime'))
except ValueError as exc:return self.send({'detail':str(exc)},400)
return self.send({'detail':'Nicht gefunden'},404)
def do_POST(self):
origin=self.headers.get('Origin');host=self.headers.get('Host','')
if self.headers.get('X-Athena-UI')!='1' or origin and origin!='http://'+host:return self.send({'detail':'Same-origin erforderlich'},403)
try:
length=int(self.headers.get('Content-Length','0'))
if self.headers.get('Transfer-Encoding') or not 0<length<=256*1024**2:raise ValueError('Upload maximal 256 MiB.')
self.connection.settimeout(60);raw=self.rfile.read(length)
if len(raw)!=length:raise ValueError('Upload unvollständig.')
if self.path=='/api/connection':
if length>4096:raise ValueError('Verbindungsdaten zu groß.')
value=json.loads(raw);token=value.get('token','')
if not isinstance(token,str) or not 1<=len(token)<=256:raise ValueError('Ungültiger Token.')
request('/api/v1/separator-runtime',token=token)
CONFIG.parent.mkdir(exist_ok=True);tmp=CONFIG.with_suffix('.tmp');tmp.write_text(json.dumps({'token':token}));tmp.chmod(0o600);tmp.replace(CONFIG)
return self.send({'ok':True})
if self.path!='/v1/separate':return self.send({'detail':'Nicht gefunden'},404)
ctype=self.headers.get('Content-Type','')
if not ctype.startswith('multipart/form-data;'):raise ValueError('Audiodatei fehlt.')
msg=BytesParser(policy=policy.default).parsebytes(b'Content-Type: '+ctype.encode()+b'\r\n\r\n'+raw)
fields={}
if not msg.is_multipart() or msg.defects:raise ValueError('Ungültiger Upload.')
for part in msg.iter_parts():
name=part.get_param('name',header='content-disposition')
if name not in ('file','target') or name in fields:raise ValueError('Ungültiges Upload-Feld.')
fields[name]=part.get_payload(decode=True)
target=fields.get('target',b'').decode()
if target not in TARGETS:raise ValueError('Zielspur noch nicht angebunden.')
with tempfile.TemporaryDirectory() as work:
source=Path(work)/'input';source.write_bytes(fields['file']);wav=Path(work)/'input.wav'
subprocess.run(['ffmpeg','-nostdin','-hide_banner','-loglevel','error','-y','-i',str(source),'-t','1201','-ac','2','-ar','44100','-c:a','pcm_s16le',str(wav)],check=True,timeout=120,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
data=wav.read_bytes();boundary=secrets.token_hex(24)
body=b''
for name,value in [('model',TARGETS[target]),('target',target)]:body+=f'--{boundary}\r\nContent-Disposition: form-data; name="{name}"\r\n\r\n{value}\r\n'.encode()
body+=f'--{boundary}\r\nContent-Disposition: form-data; name="file"; filename="input.wav"\r\nContent-Type: audio/wav\r\n\r\n'.encode()+data+f'\r\n--{boundary}--\r\n'.encode()
job=json.loads(request('/api/v1/separator-jobs/start',body,'multipart/form-data; boundary='+boundary));ident=job['id'];deadline=time.monotonic()+7250
while time.monotonic()<deadline:
time.sleep(2);current=json.loads(request('/api/v1/separator-tests'))['job']
if not current or current['id']!=ident:raise ValueError('Auftrag wurde ersetzt.')
if current['state']=='running':continue
if current['state']!='complete':raise ValueError(current['phase'])
result=io.BytesIO()
with zipfile.ZipFile(result,'w',zipfile.ZIP_STORED) as bundle:
for f in current['files']:bundle.writestr(Path(f['name']).name,request(f"/api/v1/separator-tests/audio?id={ident}&index={f['index']}"))
return self.send(result.getvalue(),mime='application/zip')
raise ValueError('Trennauftrag hat das Zeitlimit überschritten.')
except Exception as exc:return self.send({'detail':str(exc) if isinstance(exc,ValueError) else 'Verarbeitung fehlgeschlagen; Verbindung und Audioformat prüfen.'},400)
if __name__=='__main__':ThreadingHTTPServer(('127.0.0.1',8128),Handler).serve_forever()