diff --git a/README.md b/README.md
index 4f138a1..b0eb004 100644
--- a/README.md
+++ b/README.md
@@ -234,3 +234,7 @@ Sprachmodell-GPUs und Vision-Projektor werden unabhängig zugeordnet. Der Modell
Referenzbilder: Alle derzeit ausführbaren Bildrezepte (Qwen Image 2.1 und FLUX.2 Klein 9B) erlauben bis zu vier Bilder pro Bearbeitungsauftrag. FLUX nutzt VAEEncode und verkettete ReferenceLatent-Nodes für beide Conditioning-Zweige nach dem offiziellen ComfyUI-Workflow: https://github.com/Comfy-Org/workflow_templates/blob/main/templates/image_flux2_klein_image_edit_9b_distilled.json . Neue Modellfamilien benötigen einen passenden Workflow; hochgeladene Referenzen werden niemals stillschweigend verworfen.
Bildadapter werden zentral in `profiles.IMAGE_ADAPTERS` definiert: Modellfamilie, unterstütztes Gewichtsformat, Komponenten, Laufzeit, Workflow, Referenzbildlimit und Speicherpolitik. Bibliothek, Profile und Bildworker verwenden denselben Resolver `image_adapter`. Bekannte Quellen bleiben kompatibel; neue Varianten können über gespeicherte präzise Architektur-Metadaten oder die Hugging-Face-Basismodell-Zuordnung dieselbe Anbindung wiederverwenden. Ein Name mit „Qwen“ oder „Flux“ genügt nicht. Allgemeine FLUX-Architekturangaben unterscheiden Klein 9B nicht sicher von anderen FLUX-Varianten und schalten daher keinen Worker frei. Metadaten sind eine Zuordnungshilfe, keine Garantie für den Inhalt oder die Qualität veränderter Gewichte. Neue Architekturen und Dateiformate benötigen einen registrierten Adapter. Bereits heruntergeladene Varianten ohne ausreichende Metadaten bleiben ungeprüft; bestehende bekannte Profile bleiben erhalten.
+
+### Audio-Trennung
+
+Eigener Werkzeugbereich `#separation` mit BS-Roformer, MelBand und Demucs FT/6s. Die alte Web-GUI läuft optional als CPU-only Container unter Weitere Dienste und nutzt den nativen Deck-Worker. [Installation und Grenzen](deploy/separator-web/README.md). Modellquellen, alle Ensemble-Dateien und SHA-256 sind in `separator_sources.json` festgelegt. Paket-Downloads und Backup-Restore verwenden diese geprüften Rezepte; Nutzmedien werden nicht exportiert. Bei lokalen Bibliotheksdateien wird eine belegte Originalquelle separat vom Übernahmeort angezeigt, ohne IDs oder Profilreferenzen zu ändern. Unbekannte lokale Quellen werden nicht geraten.
diff --git a/app.js b/app.js
index 979c51a..d98ff57 100644
--- a/app.js
+++ b/app.js
@@ -24,7 +24,7 @@ function appearancePage(){
sync();
}
async function refresh(){if(refreshing)return;refreshing=true;const page=location.hash.slice(1)||'dashboard';document.querySelectorAll('nav a').forEach(a=>a.classList.toggle('active',a.hash==='#'+page));try{error.textContent='';let html;
-if(page==='separator-runtime'||page==='separator-test'){SeparatorUI.render(page==='separator-test');return;}
+if(['separator-runtime','separator-test','separation'].includes(page)){SeparatorUI.render(page!=='separator-runtime');return;}
if(page==='audio-cpp-runtime'){AudioCppUI.render();return;}
if(page==='ltx-original-runtime'){LTXOriginalUI.render();return;}
if(['video','video-runtime'].includes(page)){VideoUI.runtime();return;}
diff --git a/backup.py b/backup.py
index d84a59b..92bff04 100644
--- a/backup.py
+++ b/backup.py
@@ -76,7 +76,10 @@ def validate(doc):
if 'separator' in runtimes:
from separator_runtime import MODELS
value=runtimes['separator']
- if not isinstance(value,dict) or set(value)!={'installed','models'} or type(value['installed']) is not bool or not isinstance(value['models'],list) or len(value['models'])>len(MODELS) or any(not isinstance(m,str) or m not in MODELS for m in value['models']) or len(set(value['models']))!=len(value['models']):raise ValueError('Ungültiges Audio-Separator-Rezept.')
+ if not isinstance(value,dict) or set(value)-{'installed','models','sources'} or not {'installed','models'}<=set(value) or type(value['installed']) is not bool or not isinstance(value['models'],list) or len(value['models'])>len(MODELS) or any(not isinstance(m,str) or m not in MODELS for m in value['models']) or len(set(value['models']))!=len(value['models']):raise ValueError('Ungültiges Audio-Separator-Rezept.')
+ if 'sources' in value:
+ from separator_runtime import SOURCES
+ if value['sources']!={name:SOURCES[name] for name in value['models']}:raise ValueError('Audio-Separator-Quellen stimmen nicht mit dem geprüften Rezept überein.')
if 'audio_cpp' in runtimes and type(runtimes['audio_cpp']) is not bool:raise ValueError('Ungültige audio.cpp-Laufzeit.')
if 'ltx_original' in runtimes and type(runtimes['ltx_original']) is not bool:raise ValueError('Ungültige LTX-Laufzeit.')
if not isinstance(runtimes['llama_builds'],list) or len(runtimes['llama_builds'])>50 or any(not isinstance(b,dict) or set(b)!={'commit','backend'} or not re.fullmatch('[a-f0-9]{40}',b.get('commit','')) or b.get('backend') not in ('CUDA','CPU') for b in runtimes['llama_builds']):raise ValueError('Ungültige Buildrezepte.')
@@ -144,7 +147,8 @@ class Backup:
else:warnings.append('Deck-Netzwerkmodul deaktiviert; fremder WireGuard-Gateway wird nicht gesichert.')
document=dict(format='athena-deck',version=VERSION,created_at=time.time(),settings=settings,models=models,credentials=s.credentials.read(),theme=theme,network=network,services=services,runtime_versions=dict(image=dict(comfy=s.image_runtime.status()['comfy_revision'],gguf=s.image_runtime.status()['gguf_revision']),tts=s.tts_runtime.status()['revision']),runtimes=dict(audio_cpp=bool(getattr(s,'audio_cpp_runtime',None) and s.audio_cpp_runtime.status()['installed']),ltx_original=bool(getattr(s,'ltx_original_runtime',None) and s.ltx_original_runtime.status()['installed']),llama=llama,llama_builds=[dict(commit=b['commit'],backend=b['backend']) for b in state['builds']],image=s.image_runtime.status()['installed'],tts=s.tts_runtime.status()['installed'],enhancers={task:data['revision'] for task in ('t2i','i2i') if (data:=s.prompt_enhancer.installed(task))},swarm_nodes=(self.root/'video/swarm-comfy-nodes').is_dir()),warnings=warnings)
if getattr(s,'separator_runtime',None):
- separator=s.separator_runtime.status();document['runtimes']['separator']=dict(installed=separator['installed'],models=[m['id'] for m in separator['models'] if m['installed']]);document['runtime_versions']['separator']=separator['version']
+ from separator_runtime import SOURCES
+ separator=s.separator_runtime.status();document['runtimes']['separator']=dict(installed=separator['installed'],models=[m['id'] for m in separator['models'] if m['installed']],sources={m['id']:SOURCES[m['id']] for m in separator['models'] if m['installed']});document['runtime_versions']['separator']=separator['version']
if document['runtimes']['audio_cpp']:
from audio_cpp_runtime import REVISION
document['runtime_versions']['audio_cpp']=REVISION
@@ -244,8 +248,9 @@ class Backup:
if r.get('separator',{}).get('installed'):
if not s.separator_runtime.status()['installed']:
self._phase('Audio Separator installieren');s.separator_runtime.start();self._wait(s.separator_runtime.status)
+ from separator_runtime import verify_package
for name in r['separator']['models']:
- if not s.separator_runtime.model_ready(name):
+ if not s.separator_runtime.model_ready(name) or not verify_package(s.separator_runtime.root/'models',name):
self._phase('Audio-Separator-Modellpaket laden');s.separator_runtime.download(name);self._wait(s.separator_runtime.status)
targets=r['llama_builds']+([r['llama']] if r['llama'] and r['llama'] not in r['llama_builds'] else [])
for target in targets:
diff --git a/catalog-ui.js b/catalog-ui.js
index 5098819..1e2d885 100644
--- a/catalog-ui.js
+++ b/catalog-ui.js
@@ -34,7 +34,7 @@ window.CatalogUI = (() => {
const openRows=new Set([...root.querySelectorAll('.detail-row[open]')].map(x=>x.dataset.rowId));
const entries = state.entries.filter(x => x.kind === kind);
const groups = [['model', kind==='chat'?'Sprachmodelle · Hauptgewichte':'Modelle · Hauptgewichte'],['vision_projector','Vision-Projektoren'],['audio_projector','Audio-Projektoren'],['text_encoder','Textencoder'],['vae','VAE · Decoder'],['configuration','Konfigurationen'],['auxiliary','Tokenizer und weitere Zusatzkomponenten']];
- const row = x => `${escape(x.file)}${escape(x.role_label)}${size(x.size)}${x.used_by_profiles?.length?`${x.used_by_profiles.length} Profil(e)`:'Nicht zugeordnet'}
Quelle: ${escape(x.repo)}
${x.profile_eligible?'Modelldatei · über ein Profil verwenden':'Zusatzdatei · wird einem Modellprofil unter Komponenten zugeordnet'}
Version und Integrität
Revision ${escape(x.revision)}
${x.upstream_hash_verified ? 'SHA-256 mit Quelle geprüft' : 'SHA-256 lokal erfasst; Größe mit Quelle geprüft'}
BS-RoFormer und MelBand RoFormer trennen Gesang und Instrumental. Die native Laufzeit wird einmal installiert und für passende Modellpakete wiederverwendet.
Version 0.47.0 · mindestens 15 GiB frei. Installiert CUDA-PyTorch und Audio Separator in einer eigenen Umgebung. Keine Änderungen an Host-Treibern oder alten Diensten.
Installationsausgabe
Kurzer Trenntest
PCM-WAV, 16 Bit, Mono oder Stereo, bis 30 Sekunden und 8 MiB. Im LLM-Modus koordiniert Deck die GPU-Nutzung und entlädt gegebenenfalls eigene Modelle. Fremde Dienste bleiben unberührt. Das Trennmodell wird nach dem Test entladen.
BS-RoFormer und MelBand RoFormer trennen Gesang und Instrumental. Die native Laufzeit wird einmal installiert und für passende Modellpakete wiederverwendet.
Version 0.47.0 · mindestens 15 GiB frei. Installiert CUDA-PyTorch und Audio Separator in einer eigenen Umgebung. Keine Änderungen an Host-Treibern oder alten Diensten.
Installationsausgabe
Kurzer Trenntest
PCM-WAV, 16 Bit, Mono oder Stereo, bis 30 Sekunden und 8 MiB. Im LLM-Modus koordiniert Deck die GPU-Nutzung und entlädt gegebenenfalls eigene Modelle. Fremde Dienste bleiben unberührt. Das Trennmodell wird nach dem Test entladen.
`;
const root=document.querySelector('#separator-panel'),msg=root.querySelector('#sep-message');let pending=false,signature='';
async function action(path,data){pending=true;try{await api(path,data);msg.textContent='Aktion gestartet.';}catch(error){msg.textContent=error.message;}finally{pending=false;signature='';poll();}}
async function poll(){if(!root.isConnected)return;try{const [s,t]=await Promise.all([api('separator-runtime'),api('separator-tests')]);if(!root.isConnected)return;
const running=s.job?.state==='running';if(s.job){const log=await api('separator-runtime/log');if(!root.isConnected)return;root.querySelector('#sep-log').textContent=log.text;}root.querySelector('#sep-status').innerHTML=`
${s.installed?'Installiert':'Noch nicht installiert'} · ${e(s.version)}
`).join(''):'';}
diff --git a/separator.py b/separator.py
index 864ad78..694afce 100644
--- a/separator.py
+++ b/separator.py
@@ -8,17 +8,19 @@ class SeparatorTests:
self.root=Path(root);self.runtime=runtime;self.scheduler=scheduler;self.lock=threading.RLock();self.job=None;self.process=None;self.cancel=threading.Event();self.thread=None
def status(self):
with self.lock:return dict(job=dict(self.job) if self.job else None)
- def start(self,model,audio):
+ def start(self,model,audio,target=None,full=False):
+ 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'}
+ if target is not None and targets.get(target)!=model:raise ValueError('Zielspur und Modellpaket passen nicht zusammen.')
if not isinstance(model,str) or model not in MODELS or not self.runtime.status()['installed'] or not self.runtime.model_ready(model):raise ValueError('Laufzeit und Modellpaket zuerst unter Audio Separator einrichten.')
- if not isinstance(audio,bytes) or not 44<=len(audio)<=8*1024**2:raise ValueError('WAV bis 8 MiB erforderlich.')
+ if not isinstance(audio,bytes) or not 44<=len(audio)<=(256 if full else 8)*1024**2:raise ValueError('WAV überschreitet die erlaubte Upload-Größe.')
try:
with wave.open(io.BytesIO(audio)) as wav:
- if wav.getnchannels() not in (1,2) or wav.getsampwidth()!=2 or not 8000<=wav.getframerate()<=48000 or not 0deadline:raise ValueError('Trenntest überschreitet zehn Minuten.')
+ if time.monotonic()>deadline:raise ValueError('Trennauftrag überschreitet das Zeitlimit.')
with self.lock:self.job['elapsed_seconds']=round(time.time()-self.job['started_at'])
diagnostics.close();error=(directory/'worker.tmp').read_bytes()[-65536:].decode(errors='replace')
if self.process.returncode:raise ValueError('GPU-Speicher reicht nicht (OOM).' if 'out of memory' in error.lower() else 'Audio Separator hat den Test abgebrochen. Modellpaket und Laufzeit prüfen.')
@@ -67,5 +69,5 @@ class SeparatorTests:
if type(index) is not int or not 0<=index32*1024**2:raise ValueError('Spur zu groß.')
+ if path.stat().st_size>512*1024**2:raise ValueError('Spur zu groß.')
return path.read_bytes()
diff --git a/separator_runtime.py b/separator_runtime.py
index dc9608d..1e03011 100644
--- a/separator_runtime.py
+++ b/separator_runtime.py
@@ -3,9 +3,22 @@ import hashlib,json,os,shutil,sys,time
from pathlib import Path
from ltx_original_runtime import LTXOriginalRuntime
VERSION='0.47.0'
+SOURCES=json.loads(Path(__file__).with_name('separator_sources.json').read_text())
+def package_files(name):return [x['file'] for x in SOURCES[name]['files']]
+def verify_package(directory,name):
+ for item in SOURCES[name]['files']:
+ path=Path(directory)/item['file']
+ if not path.is_file() or path.is_symlink():return False
+ digest=hashlib.sha256()
+ with path.open('rb') as stream:
+ for chunk in iter(lambda:stream.read(1024**2),b''):digest.update(chunk)
+ if digest.hexdigest()!=item['sha256']:return False
+ return True
MODELS={
'vocals_mel_band_roformer.ckpt':('MelBand RoFormer · Gesang / Instrumental','vocals_mel_band_roformer.yaml'),
- 'model_bs_roformer_ep_317_sdr_12.9755.ckpt':('BS-RoFormer · Gesang / Instrumental','model_bs_roformer_ep_317_sdr_12.9755.yaml')}
+ 'model_bs_roformer_ep_317_sdr_12.9755.ckpt':('BS-RoFormer · Gesang / Instrumental','model_bs_roformer_ep_317_sdr_12.9755.yaml'),
+ 'htdemucs_ft.yaml':('Demucs FT · Gesang / Schlagzeug / Bass / Rest','htdemucs_ft.yaml'),
+ 'htdemucs_6s.yaml':('Demucs 6s · zusätzlich Gitarre / Piano','htdemucs_6s.yaml')}
class SeparatorRuntime(LTXOriginalRuntime):
name='Audio Separator'
def __init__(self,root):super().__init__(root,None)
@@ -19,8 +32,15 @@ class SeparatorRuntime(LTXOriginalRuntime):
return self.root/'missing-python',self.root/'missing-runtime'
def status(self):
python,base=self.paths()
- return dict(installed=python.is_file() and (base/'ready').is_file(),version=VERSION,required_disk_gib=15,job=dict(self.job) if self.job else None,models=[dict(id=name,name=label,configuration=config,installed=self.model_ready(name)) for name,(label,config) in MODELS.items()])
- def model_ready(self,name):return name in MODELS and (self.root/'models'/(name+'.ready')).is_file() and all((self.root/'models'/f).is_file() and (self.root/'models'/f).stat().st_size>0 for f in (name,MODELS[name][1]))
+ return dict(installed=python.is_file() and (base/'ready').is_file(),version=VERSION,required_disk_gib=15,job=dict(self.job) if self.job else None,models=[dict(id=name,name=label,configuration=config,source=SOURCES[name],installed=self.model_ready(name)) for name,(label,config) in MODELS.items()])
+ def model_ready(self,name):
+ if name not in MODELS:return False
+ directory=self.root/'models';marker=directory/(name+'.ready')
+ try:
+ recorded=json.loads(marker.read_text())
+ return recorded=={x['file']:x['sha256'] for x in SOURCES[name]['files']} and all((directory/f).is_file() and not (directory/f).is_symlink() and (directory/f).stat().st_size>0 for f in package_files(name))
+ except (OSError,ValueError):return False
+
def start(self):
if not shutil.which('ffmpeg'):raise ValueError('FFmpeg fehlt. Deck-Systemvoraussetzungen installieren.')
with self.lock:
@@ -41,9 +61,26 @@ class SeparatorRuntime(LTXOriginalRuntime):
try:
if model:
directory=base;directory.mkdir(mode=0o700)
- self._command([self.paths()[0],'-c','from audio_separator.separator import Separator; import sys; Separator(model_file_dir=sys.argv[1],info_only=True).download_model_files(sys.argv[2])',directory,model])
- filenames=(model,MODELS[model][1])
- if any(not (directory/f).is_file() or (directory/f).stat().st_size==0 for f in filenames):raise ValueError('Modell oder YAML-Konfiguration fehlt.')
+ import urllib.request
+ for item in SOURCES[model]['files']:
+ self._phase('Modellpaket: '+item['file'])
+ succeeded=False
+ for url in item['urls']:
+ try:
+ digest=hashlib.sha256();size=0
+ with urllib.request.urlopen(url,timeout=60) as response,(directory/item['file']).open('wb') as out:
+ while chunk:=response.read(1024**2):
+ if self.cancel.is_set():raise InterruptedError()
+ size+=len(chunk)
+ if size>2*1024**3:raise ValueError('Quelldatei zu groß.')
+ digest.update(chunk);out.write(chunk)
+ if digest.hexdigest()!=item['sha256']:raise ValueError('Quelldatei stimmt nicht mit dem geprüften Rezept überein.')
+ succeeded=True;break
+ except InterruptedError:raise
+ except Exception:continue
+ if not succeeded:raise ValueError('Geprüfte Quelle nicht verfügbar: '+item['file'])
+ filenames=package_files(model)
+ if not verify_package(directory,model):raise ValueError('Modellpaket-Prüfsumme stimmt nicht.')
if self.cancel.is_set():raise InterruptedError()
destination=self.root/'models';destination.mkdir(exist_ok=True);hashes={}
for f in filenames:
diff --git a/separator_sources.json b/separator_sources.json
new file mode 100644
index 0000000..c3c1162
--- /dev/null
+++ b/separator_sources.json
@@ -0,0 +1,113 @@
+{
+ "vocals_mel_band_roformer.ckpt": {
+ "recipe_version": 1,
+ "publisher": "Kimberley Jensen / UVR",
+ "homepage": "https://github.com/nomadkaraoke/python-audio-separator",
+ "files": [
+ {
+ "file": "vocals_mel_band_roformer.ckpt",
+ "sha256": "87201f4d31afb5bc79993230fc49446918425574db48c01c405e44f365c7559e",
+ "urls": [
+ "https://github.com/TRvlvr/model_repo/releases/download/all_public_uvr_models/vocals_mel_band_roformer.ckpt",
+ "https://github.com/nomadkaraoke/python-audio-separator/releases/download/model-configs/vocals_mel_band_roformer.ckpt"
+ ]
+ },
+ {
+ "file": "vocals_mel_band_roformer.yaml",
+ "sha256": "b958b29c8f7195f0d86bee6759a33980db675c4ecaf2fcaa80fa125828e6cd38",
+ "urls": [
+ "https://github.com/TRvlvr/model_repo/releases/download/all_public_uvr_models/mdx_model_data/mdx_c_configs/vocals_mel_band_roformer.yaml",
+ "https://github.com/nomadkaraoke/python-audio-separator/releases/download/model-configs/vocals_mel_band_roformer.yaml"
+ ]
+ }
+ ]
+ },
+ "model_bs_roformer_ep_317_sdr_12.9755.ckpt": {
+ "recipe_version": 1,
+ "publisher": "Viperx / UVR",
+ "homepage": "https://github.com/nomadkaraoke/python-audio-separator",
+ "files": [
+ {
+ "file": "model_bs_roformer_ep_317_sdr_12.9755.ckpt",
+ "sha256": "5b84f37e8d444c8cb30c79d77f613a41c05868ff9c9ac6c7049c00aefae115aa",
+ "urls": [
+ "https://github.com/TRvlvr/model_repo/releases/download/all_public_uvr_models/model_bs_roformer_ep_317_sdr_12.9755.ckpt",
+ "https://github.com/nomadkaraoke/python-audio-separator/releases/download/model-configs/model_bs_roformer_ep_317_sdr_12.9755.ckpt"
+ ]
+ },
+ {
+ "file": "model_bs_roformer_ep_317_sdr_12.9755.yaml",
+ "sha256": "2bfdd16c656bd9519aba757cc4f8834b7ede675eb1e00ec4772d74ae1c41af7f",
+ "urls": [
+ "https://github.com/TRvlvr/model_repo/releases/download/all_public_uvr_models/mdx_model_data/mdx_c_configs/model_bs_roformer_ep_317_sdr_12.9755.yaml",
+ "https://github.com/nomadkaraoke/python-audio-separator/releases/download/model-configs/model_bs_roformer_ep_317_sdr_12.9755.yaml"
+ ]
+ }
+ ]
+ },
+ "htdemucs_ft.yaml": {
+ "recipe_version": 1,
+ "publisher": "Meta / Demucs",
+ "homepage": "https://github.com/facebookresearch/demucs",
+ "files": [
+ {
+ "file": "htdemucs_ft.yaml",
+ "sha256": "69470b8c1bbd674437b51bc9fb491327a10ab0396b702c93389b9cf750016346",
+ "urls": [
+ "https://github.com/TRvlvr/model_repo/releases/download/all_public_uvr_models/htdemucs_ft.yaml",
+ "https://github.com/nomadkaraoke/python-audio-separator/releases/download/model-configs/htdemucs_ft.yaml"
+ ]
+ },
+ {
+ "file": "f7e0c4bc-ba3fe64a.th",
+ "sha256": "ba3fe64ae8ef66ac9a4857222ce48efbdc5eb3ad375cb79dd13debee5aaa4066",
+ "urls": [
+ "https://dl.fbaipublicfiles.com/demucs/hybrid_transformer/f7e0c4bc-ba3fe64a.th"
+ ]
+ },
+ {
+ "file": "d12395a8-e57c48e6.th",
+ "sha256": "e57c48e6b0e38af4f7118d7bd08c49f0a0c0edf7d09143bdd902ea0d237303e6",
+ "urls": [
+ "https://dl.fbaipublicfiles.com/demucs/hybrid_transformer/d12395a8-e57c48e6.th"
+ ]
+ },
+ {
+ "file": "92cfc3b6-ef3bcb9c.th",
+ "sha256": "ef3bcb9c8b40d14ae5d51b6db2587339cc12c6b77c0be151ce6d69002e087bf2",
+ "urls": [
+ "https://dl.fbaipublicfiles.com/demucs/hybrid_transformer/92cfc3b6-ef3bcb9c.th"
+ ]
+ },
+ {
+ "file": "04573f0d-f3cf25b2.th",
+ "sha256": "f3cf25b222c4eed7cd49dd8b2c9597d50c18bd154090f7b919cfa5f93cf22c49",
+ "urls": [
+ "https://dl.fbaipublicfiles.com/demucs/hybrid_transformer/04573f0d-f3cf25b2.th"
+ ]
+ }
+ ]
+ },
+ "htdemucs_6s.yaml": {
+ "recipe_version": 1,
+ "publisher": "Meta / Demucs",
+ "homepage": "https://github.com/facebookresearch/demucs",
+ "files": [
+ {
+ "file": "htdemucs_6s.yaml",
+ "sha256": "207405151270af8fd81c2373c25d27950916682ac91dca7884a11ce13dad6f58",
+ "urls": [
+ "https://github.com/TRvlvr/model_repo/releases/download/all_public_uvr_models/htdemucs_6s.yaml",
+ "https://github.com/nomadkaraoke/python-audio-separator/releases/download/model-configs/htdemucs_6s.yaml"
+ ]
+ },
+ {
+ "file": "5c90dfd2-34c22ccb.th",
+ "sha256": "34c22ccb381c6f9fdbf324f04e1e2fe21aaaf293f5ded163a162697ff9a02ddd",
+ "urls": [
+ "https://dl.fbaipublicfiles.com/demucs/hybrid_transformer/5c90dfd2-34c22ccb.th"
+ ]
+ }
+ ]
+ }
+}
diff --git a/separator_worker.py b/separator_worker.py
index 684fc6a..d1c52a3 100644
--- a/separator_worker.py
+++ b/separator_worker.py
@@ -3,9 +3,12 @@ import json,sys
from pathlib import Path
from audio_separator.separator import Separator
import torch
-model_dir,model,input_file,output_dir=sys.argv[1:]
+model_dir,model,input_file,output_dir=sys.argv[1:5]
+target=sys.argv[5] if len(sys.argv)>5 else ''
+from separator_runtime import verify_package
+if not verify_package(model_dir,model):raise RuntimeError('Modellpaket-Prüfsumme stimmt nicht.')
if not torch.cuda.is_available():raise RuntimeError('CUDA nicht verfügbar; kein stiller CPU-Fallback.')
-separator=Separator(model_file_dir=model_dir,output_dir=output_dir,output_format='WAV',use_soundfile=True,mdxc_params={'segment_size':256,'override_model_segment_size':False,'batch_size':1,'overlap':2,'pitch_shift':0})
+separator=Separator(model_file_dir=model_dir,output_dir=output_dir,output_format='WAV',use_soundfile=True,demucs_params={'segment_size':7,'shifts':2,'overlap':.25},mdxc_params={'segment_size':256,'override_model_segment_size':False,'batch_size':1,'overlap':2,'pitch_shift':0})
separator.load_model(model)
files=separator.separate(input_file)
root=Path(output_dir).resolve();names=[]
@@ -14,4 +17,15 @@ for name in files:
if path.parent!=root or not path.is_file() or path.suffix.lower()!='.wav':raise RuntimeError('Ungültige Ausgabe.')
names.append(path.name)
if not names:raise RuntimeError('Keine Spuren erzeugt.')
+if target:
+ import subprocess
+ chosen=next((n for n in names if target in Path(n).stem.lower()),None)
+ if not chosen:raise RuntimeError('Zielspur fehlt.')
+ others=[root/n for n in names if n!=chosen]
+ if not others:raise RuntimeError('Restspuren fehlen.')
+ args=['ffmpeg','-hide_banner','-loglevel','error','-y']
+ for f in others:args+=['-i',str(f)]
+ args+=['-filter_complex',''.join(f'[{i}:a]' for i in range(len(others)))+f'amix=inputs={len(others)}:normalize=0:dropout_transition=0[rest]','-map','[rest]','-c:a','pcm_s16le',str(root/'rest.wav')]
+ subprocess.run(args,check=True,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=180)
+ names=[chosen,'rest.wav']
(root/'results.json').write_text(json.dumps(names))
diff --git a/server.py b/server.py
index 61f8262..d5cb95f 100644
--- a/server.py
+++ b/server.py
@@ -187,7 +187,7 @@ class Handler(BaseHTTPRequestHandler):
return secrets.compare_digest(actual,record['api_token_hash'])
def token_route(self):
- return self.command == 'GET' and self.path in ('/api/v1/status','/api/v1/hardware')
+ return (self.command == 'GET' and (self.path in ('/api/v1/status','/api/v1/hardware','/api/v1/separator-runtime','/api/v1/separator-tests') or urlsplit(self.path).path=='/api/v1/separator-tests/audio')) or (self.command=='POST' and self.path in ('/api/v1/separator-jobs/start','/api/v1/separator-tests/cancel'))
def session_token(self):
try:
@@ -547,15 +547,15 @@ class Handler(BaseHTTPRequestHandler):
if data:raise ValueError('Keine Parameter erwartet.')
return self.respond(self.server.separator_runtime.stop() if self.path.endswith('/cancel') else self.server.separator_runtime.start())
except (ValueError,OSError) as exc:return self.respond({'error':str(exc)},400)
- if self.path in ('/api/v1/separator-tests/start','/api/v1/separator-tests/cancel'):
+ if self.path in ('/api/v1/separator-tests/start','/api/v1/separator-jobs/start','/api/v1/separator-tests/cancel'):
try:
if self.path.endswith('/cancel'):
if self.read_json():raise ValueError('Keine Parameter erwartet.')
return self.respond(self.server.separator_tests.stop())
# Reuse bounded multipart parsing; separation permits stereo WAV.
- fields,audio=read_upload(self,validate=False)
- if set(fields)!={'model'}:raise ValueError('Nur Modell und WAV-Datei erwartet.')
- return self.respond(self.server.separator_tests.start(fields['model'],audio))
+ fields,audio=read_upload(self,validate=False,max_audio=256*1024**2 if self.path=='/api/v1/separator-jobs/start' else 8*1024**2)
+ if not {'model'}<=set(fields) or set(fields)-{'model','target'}:raise ValueError('Nur Modell, Zielspur und WAV-Datei erwartet.')
+ return self.respond(self.server.separator_tests.start(fields['model'],audio,target=fields.get('target'),full=self.path=='/api/v1/separator-jobs/start'))
except (ValueError,OSError) as exc:return self.respond({'error':str(exc)},400)
if self.path in ('/api/v1/audio-cpp-runtime/install','/api/v1/audio-cpp-runtime/cancel'):
try:
diff --git a/stt.py b/stt.py
index 8730e68..9d93d59 100644
--- a/stt.py
+++ b/stt.py
@@ -22,13 +22,13 @@ def validate_wav(audio):
if len(w.readframes(w.getnframes()))!=w.getnframes()*2:raise ValueError('WAV-Datei ist unvollständig.')
except (wave.Error,EOFError):raise ValueError('Ungültige WAV-Datei.') from None
-def read_upload(handler,validate=True):
+def read_upload(handler,validate=True,max_audio=MAX_AUDIO):
handler.connection.settimeout(30)
if handler.headers.get('Transfer-Encoding'):raise ValueError('Chunked Upload wird nicht unterstützt.')
try:length=int(handler.headers.get('Content-Length','0'))
except ValueError:raise ValueError('Ungültige Upload-Länge.') from None
content_type=handler.headers.get('Content-Type','')
- if not 0256:raise ValueError('Ungültiges oder doppeltes Feld.')
+ if name not in ('model','profile_id','language','response_format','target') or name in fields or len(data)>256:raise ValueError('Ungültiges oder doppeltes Feld.')
try:fields[name]=data.decode('utf-8')
except UnicodeError:raise ValueError('Ungültiges Textfeld.') from None
if validate:validate_wav(audio)
diff --git a/test_separator.py b/test_separator.py
index 944f249..8b413e4 100644
--- a/test_separator.py
+++ b/test_separator.py
@@ -19,7 +19,9 @@ class Tests(unittest.TestCase):
def test_model_requires_configuration_and_unknown_download_rejected(self):
with tempfile.TemporaryDirectory() as root:
runtime=SeparatorRuntime(root);models=Path(root,'models');models.mkdir();name=next(iter(MODELS));(models/name).write_bytes(b'weights')
- self.assertFalse(runtime.model_ready(name));(models/MODELS[name][1]).write_text('model: {}');(models/(name+'.ready')).write_text('{}');self.assertTrue(runtime.model_ready(name))
+ self.assertFalse(runtime.model_ready(name));(models/MODELS[name][1]).write_text('model: {}');(models/(name+'.ready')).write_text('{}');self.assertFalse(runtime.model_ready(name))
+ from separator_runtime import SOURCES
+ (models/(name+'.ready')).write_text(json.dumps({f['file']:f['sha256'] for f in SOURCES[name]['files']}));self.assertTrue(runtime.model_ready(name))
with self.assertRaises(ValueError):runtime.download('../other')
with self.assertRaises(ValueError):runtime.download([])
def test_stereo_accepted_and_long_or_truncated_wav_rejected(self):
diff --git a/test_separator_sources.py b/test_separator_sources.py
new file mode 100644
index 0000000..4bab521
--- /dev/null
+++ b/test_separator_sources.py
@@ -0,0 +1,19 @@
+import hashlib,json,tempfile,unittest
+from pathlib import Path
+from unittest.mock import patch
+from separator_runtime import SOURCES,MODELS,verify_package,package_files
+class Tests(unittest.TestCase):
+ def test_demucs_ensemble_complete_and_sources_https(self):
+ self.assertEqual(set(SOURCES),set(MODELS))
+ self.assertEqual(len(package_files('htdemucs_ft.yaml')),5)
+ self.assertEqual(len(package_files('htdemucs_6s.yaml')),2)
+ for recipe in SOURCES.values():
+ for f in recipe['files']:
+ self.assertEqual(len(f['sha256']),64)
+ self.assertTrue(all(u.startswith('https://') for u in f['urls']))
+ def test_wrong_or_missing_weight_rejected(self):
+ with tempfile.TemporaryDirectory() as d:
+ recipe={'x':{'files':[{'file':'weight','sha256':hashlib.sha256(b'valid').hexdigest()}]}}
+ with patch.dict(SOURCES,recipe):
+ self.assertFalse(verify_package(d,'x'));Path(d,'weight').write_bytes(b'wrong');self.assertFalse(verify_package(d,'x'))
+ Path(d,'weight').write_bytes(b'valid');self.assertTrue(verify_package(d,'x'))