"""Owned audio-separator environment and explicitly supported model packages.""" 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'), '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) def paths(self): marker=self.root/'active.json' if marker.is_file(): ident=json.loads(marker.read_text()).get('id','') if len(ident)==32 and all(c in '0123456789abcdef' for c in ident): base=self.root/ident if (base/'ready').is_file():return base/'python/bin/python',base 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,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: if self.job and self.job['state']=='running':raise ValueError('Einrichtungsauftrag läuft bereits.') self.requested_model=None return super().start() def download(self,name): import threading,uuid if not isinstance(name,str) or name not in MODELS:raise ValueError('Dieses Modellpaket ist nicht unterstützt.') with self.lock: if not self.status()['installed']:raise ValueError('Zuerst Audio Separator installieren.') if self.job and self.job['state']=='running':raise ValueError('Einrichtungsauftrag läuft bereits.') if shutil.disk_usage(self.root).free<3*1024**3:raise ValueError('Mindestens 3 GiB freier Speicher erforderlich.') self.cancel.clear();self.requested_model=name;self.job=dict(id=uuid.uuid4().hex,state='running',phase='Modellpaket herunterladen',started_at=time.time());self._save() self.worker=threading.Thread(target=self._run,daemon=True);self.worker.start();return self.status() def _run(self): base=self.root/self.job['id'];state='failed';phase='Einrichtung fehlgeschlagen.';model=getattr(self,'requested_model',None) try: if model: directory=base;directory.mkdir(mode=0o700) 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: digest=hashlib.sha256() with (directory/f).open('rb') as source: for chunk in iter(lambda:source.read(1024**2),b''):digest.update(chunk) hashes[f]=digest.hexdigest();(directory/f).replace(destination/f) (destination/(model+'.ready')).write_text(json.dumps(hashes)) state='complete';phase='Modell und passende Konfiguration heruntergeladen. Kein Modell geladen.' else: base.mkdir(mode=0o700);tmp=base/'tmp';tmp.mkdir() env=dict(os.environ,TMPDIR=str(tmp),PIP_NO_INPUT='1',PIP_DISABLE_PIP_VERSION_CHECK='1') python=base/'python/bin/python';self._phase('Eigene Python-Umgebung erstellen');self._command([sys.executable,'-m','venv',base/'python'],env) self._phase('CUDA-PyTorch herunterladen · mehrere GiB');self._command([python,'-m','pip','install','--no-cache-dir','torch==2.11.0','torchaudio==2.11.0','--index-url','https://download.pytorch.org/whl/cu128'],env) self._phase('Audio Separator und GPU-Abhängigkeiten installieren');self._command([python,'-m','pip','install','--no-cache-dir',f'audio-separator[gpu]=={VERSION}','onnxruntime-gpu==1.22.0','audioread==3.1.0'],env) self._phase('Paketkonsistenz und CUDA-Unterstützung prüfen');self._command([python,'-m','pip','check'],env) self._command([python,'-c','import torch; from audio_separator.separator import Separator; assert torch.version.cuda'],env) if self.cancel.is_set():raise InterruptedError() (base/'ready').write_text(VERSION);marker=self.root/'active.tmp';marker.write_text(json.dumps({'id':base.name}));marker.replace(self.root/'active.json') state='complete';phase='Audio Separator installiert. Jetzt Modellpaket auswählen.' except InterruptedError:state='cancelled';phase='Einrichtung abgebrochen.' except Exception as exc:phase=str(exc) finally: if model or state!='complete':shutil.rmtree(base,ignore_errors=True) with self.lock:self.process=None;self.job.update(state=state,phase=phase,finished_at=time.time());self._save()