83 lines
6.2 KiB
Python
83 lines
6.2 KiB
Python
"""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 archive_name(target,filename):
|
|
if target not in TARGETS:raise ValueError('Unbekannte Zielspur.')
|
|
if Path(filename).name=='rest.wav':return 'Instrumental.wav' if target=='vocals' else 'Rest.wav'
|
|
return {'vocals':'Vocals','drums':'Drums','bass':'Bass','guitar':'Guitar','piano':'Piano','other':'Other'}[target]+'.wav'
|
|
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(archive_name(target,f['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()
|