From aee31359b669e1e34b5775d08a62c18782fa9e36 Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Fri, 2 Oct 2026 11:55:25 +0200 Subject: [PATCH] Offer isolated VoxCPM runtime installation from model setup --- VOXCPM2_COMPONENTS.md | 17 +++++++++-- app.js | 2 +- backup.py | 13 +++++++-- deploy/Dockerfile | 6 ++-- deploy/Dockerfile.app-update | 6 ++-- deploy/install.py | 4 +-- deploy/voxcpm-requirements.lock | 5 ++++ execution_setup.py | 5 ++-- index.html | 2 +- profiles-ui.js | 2 +- profiles.py | 2 +- server.py | 16 +++++++++-- studio.js | 4 ++- test_backup.py | 13 +++++++++ test_voxcpm_components.py | 2 +- test_voxcpm_runtime.py | 28 ++++++++++++++++++ voxcpm-ui.js | 11 +++++++ voxcpm_runtime.py | 51 +++++++++++++++++++++++++++++++++ 18 files changed, 168 insertions(+), 21 deletions(-) create mode 100644 deploy/voxcpm-requirements.lock create mode 100644 test_voxcpm_runtime.py create mode 100644 voxcpm-ui.js create mode 100644 voxcpm_runtime.py diff --git a/VOXCPM2_COMPONENTS.md b/VOXCPM2_COMPONENTS.md index ec9e4b0..928eb1d 100644 --- a/VOXCPM2_COMPONENTS.md +++ b/VOXCPM2_COMPONENTS.md @@ -16,8 +16,7 @@ nicht Bestandteil dieses Grundpakets. Das Dateirezept ist anhand des [offiziellen Modells](https://huggingface.co/openbmb/VoxCPM2) und des [originalen Loaders](https://github.com/OpenBMB/VoxCPM/blob/main/src/voxcpm/model/voxcpm2.py) -geprüft. Es ist keine Zusage getesteter Inferenz: **Die VoxCPM-Python/PyTorch-Laufzeit -und ihr Deck-Worker sind noch nicht angebunden.** Eine vorhandene audio.cpp- oder +geprüft. Es ist keine Zusage getesteter Inferenz: **Der Deck-Generierungsworker ist noch nicht angebunden.** Eine vorhandene audio.cpp- oder llama.cpp-Installation ersetzt diese Anbindung nicht. Das Profil bleibt gesperrt. Der Katalog erlaubt `.pth` ausschließlich für die exakt benannte offizielle @@ -30,3 +29,17 @@ als allgemeine Konfiguration. „Alle fehlenden Dateien herunterladen“ prüft ob für alle Pflichtkomponenten eine herunterladbare Datei angeboten wird. Erst danach werden Downloads eingereiht; bei einem späteren Fehler zeigt Deck bereits eingereihte Dateien an. Manuelle, ungeprüfte Listen erhalten weiterhin keinen Sammeldownload. + +## Laufzeit über die Oberfläche installieren + +Unter **Laufzeiten → VoxCPM** oder im Komponentenbereich über +**Passende Laufzeit installieren** lässt sich das offizielle Paket VoxCPM 2.0.3 +in einer eigenen Python-Umgebung installieren und abbrechen. Die Installation +prüft CUDA-PyTorch, Paketkonsistenz und den VoxCPM2-Loader, lädt aber weder +Modellgewichte noch ein Modell in die GPUs. Vorhandene CUDA-Pakete der +Bildlaufzeit werden lesend wiederverwendet. Versionen stehen in +`deploy/voxcpm-requirements.lock`. + +Backup exportiert den Installationswunsch und die Version; Restore installiert +diese Laufzeit erneut. **Eine installierte Laufzeit schaltet das Profil noch +nicht frei: Der Generierungsworker muss zusätzlich angebunden werden.** diff --git a/app.js b/app.js index d98ff57..c91b649 100644 --- a/app.js +++ b/app.js @@ -28,7 +28,7 @@ if(['separator-runtime','separator-test','separation'].includes(page)){Separator 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;} -if(['chat','image','audio','stt','music','voice','video','runtime','runtimes','image-runtime','stt-runtime','tts-runtime','docker-runtime','services'].includes(page)){Studio.render(page);return;} +if(['chat','image','audio','stt','music','voice','video','runtime','runtimes','image-runtime','stt-runtime','tts-runtime','voxcpm-runtime','docker-runtime','services'].includes(page)){Studio.render(page);return;} if(page==='backup'){BackupUI.render();return;} if(page==='access'){await accessPage();return;} if(page==='appearance'){appearancePage();return;} diff --git a/backup.py b/backup.py index e49f07b..24e433d 100644 --- a/backup.py +++ b/backup.py @@ -75,7 +75,7 @@ def validate(doc): validate_profiles(settings['separator-profiles.json']) validate_record(doc.get('credentials')) runtimes=doc.get('runtimes') - if not isinstance(runtimes,dict) or set(runtimes)-{'llama','llama_builds','image','tts','enhancers','swarm_nodes','ltx_original','audio_cpp','separator'} or not {'llama','llama_builds','image','tts','enhancers','swarm_nodes'}<=set(runtimes):raise ValueError('Ungültiges Laufzeitrezept.') + if not isinstance(runtimes,dict) or set(runtimes)-{'llama','llama_builds','image','tts','enhancers','swarm_nodes','ltx_original','audio_cpp','separator','voxcpm'} or not {'llama','llama_builds','image','tts','enhancers','swarm_nodes'}<=set(runtimes):raise ValueError('Ungültiges Laufzeitrezept.') if 'separator' in runtimes: from separator_runtime import MODELS value=runtimes['separator'] @@ -83,6 +83,7 @@ def validate(doc): 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 'voxcpm' in runtimes and type(runtimes['voxcpm']) is not bool:raise ValueError('Ungültige VoxCPM-Laufzeit.') 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.') @@ -152,6 +153,8 @@ class Backup: if getattr(s,'separator_runtime',None): 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 getattr(s,'voxcpm_runtime',None): + document['runtimes']['voxcpm']=s.voxcpm_runtime.status()['installed'];document['runtime_versions']['voxcpm']=s.voxcpm_runtime.status()['version'] if document['runtimes']['audio_cpp']: from audio_cpp_runtime import REVISION document['runtime_versions']['audio_cpp']=REVISION @@ -184,6 +187,9 @@ class Backup: elif s.network.status().get('enabled') or s.network.status().get('mode')!='lan':blockers.append('Deck-Tunnel vor Netzwerk-Restore im bestätigten LAN-Modus deaktivieren.') if doc['network']['mode']!='lan':warnings.append('Tunnelzugang wird mit Rückfallzeit aktiviert und muss über den Tunnel bestätigt werden.') versions=doc.get('runtime_versions',{}) + if versions.get('voxcpm'): + from voxcpm_runtime import VERSION as VOXCPM_VERSION + if versions['voxcpm']!=VOXCPM_VERSION:blockers.append('Backup verlangt eine andere VoxCPM-Version. Passende Deck-Version verwenden.') if versions.get('separator'): from separator_runtime import VERSION as SEPARATOR_VERSION if versions['separator']!=SEPARATOR_VERSION:blockers.append('Backup verlangt eine andere Audio-Separator-Version.') @@ -193,7 +199,7 @@ class Backup: if versions.get('ltx_original'): from ltx_original_runtime import DESKTOP_REV,LTX_REV if versions['ltx_original']!=dict(desktop=DESKTOP_REV,inference=LTX_REV):blockers.append('Backup verlangt eine andere originale LTX-Version. Passende Deck-Version verwenden.') - if versions and {k:v for k,v in versions.items() if k not in ('ltx_original','audio_cpp','separator')}!=dict(image=dict(comfy=s.image_runtime.status()['comfy_revision'],gguf=s.image_runtime.status()['gguf_revision']),tts=s.tts_runtime.status()['revision']):blockers.append('Backup verlangt andere Bild-/TTS-Laufzeitversionen. Passende Deck-Version oder geprüftes Runtime-Upgrade verwenden.') + if versions and {k:v for k,v in versions.items() if k not in ('ltx_original','audio_cpp','separator','voxcpm')}!=dict(image=dict(comfy=s.image_runtime.status()['comfy_revision'],gguf=s.image_runtime.status()['gguf_revision']),tts=s.tts_runtime.status()['revision']):blockers.append('Backup verlangt andere Bild-/TTS-Laufzeitversionen. Passende Deck-Version oder geprüftes Runtime-Upgrade verwenden.') for c in doc['services']: if not c.get('registry') and not c.get('build_recipe'):warnings.append(c['name']+': Image nur lokal vorhanden; Registry-/Buildquelle fehlt.') ports=s.endpoint.allowed_ports @@ -246,6 +252,8 @@ class Backup: self._phase('TTS-Laufzeit und Zusatzdateien installieren');s.tts_runtime.start();self._wait(s.tts_runtime.status) if r.get('audio_cpp') and not s.audio_cpp_runtime.status()['installed']: self._phase('audio.cpp-Laufzeit installieren');s.audio_cpp_runtime.start();self._wait(s.audio_cpp_runtime.status) + if r.get('voxcpm') and not s.voxcpm_runtime.status()['installed']: + self._phase('VoxCPM-Laufzeit installieren');s.voxcpm_runtime.start();self._wait(s.voxcpm_runtime.status) if r.get('ltx_original') and not s.ltx_original_runtime.status()['installed']: self._phase('Originale LTX-Laufzeit installieren');s.ltx_original_runtime.start();self._wait(s.ltx_original_runtime.status) if r.get('separator',{}).get('installed'): @@ -396,6 +404,7 @@ class Backup: if getattr(self.server,'separator_runtime',None):self.server.separator_runtime.stop() if getattr(self.server,'separator_tests',None):self.server.separator_tests.stop() if getattr(self.server,'music',None):self.server.music.stop() + if getattr(self.server,'voxcpm_runtime',None):self.server.voxcpm_runtime.stop() if getattr(self.server,'audio_cpp_runtime',None):self.server.audio_cpp_runtime.stop() self.server.prompt_enhancer.stop() return {'cancellation_requested':True} diff --git a/deploy/Dockerfile b/deploy/Dockerfile index f00bcbf..21856d6 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -16,8 +16,8 @@ WORKDIR /app COPY deploy/image-requirements.lock /app/deploy/image-requirements.lock COPY deploy/tts-requirements.lock /app/deploy/tts-requirements.lock COPY deploy/ltx-original-requirements.lock /app/deploy/ltx-original-requirements.lock -COPY separator_sources.json separator.py separator_runtime.py separator_worker.py music.py audio_cpp_runtime.py ltx_original_runtime.py ltx_original_entry.py video_original.py backup.py backup_codec.py audio_policy.py prompt_enhancer.py prompt_enhancer_worker.py api_compat.py stt.py execution_setup.py tts_runtime.py tts_test.py tts_worker.py auto_test.py chat_test.py endpoint.py inference.py docker_support.py image_encoder_node.py image_runtime.py image_test.py image_upload.py profiles.py capacity.py server.py runtime.py video.py video_proxy.py video_comfy.py video_comfy_node.py video_comfy_proxy.py catalog.py hub_auth.py auth.py collect_hardware.py dashboard_data.py dashboard_history.py /app/ -COPY separator-ui.js music-ui.js audio-cpp-ui.js ltx-original-ui.js backup-ui.js video-ui.js stt-ui.js tts-ui.js auto-test-ui.js chat-test-ui.js endpoint-ui.js docker-ui.js image-test-ui.js profiles-ui.js dashboard-ui.js index.html app.js studio.js runtime-ui.js catalog-ui.js style.css dashboard.css themes.css login.html login.js access-ui.js network-ui.js /app/ +COPY voxcpm_runtime.py separator_sources.json separator.py separator_runtime.py separator_worker.py music.py audio_cpp_runtime.py ltx_original_runtime.py ltx_original_entry.py video_original.py backup.py backup_codec.py audio_policy.py prompt_enhancer.py prompt_enhancer_worker.py api_compat.py stt.py execution_setup.py tts_runtime.py tts_test.py tts_worker.py auto_test.py chat_test.py endpoint.py inference.py docker_support.py image_encoder_node.py image_runtime.py image_test.py image_upload.py profiles.py capacity.py server.py runtime.py video.py video_proxy.py video_comfy.py video_comfy_node.py video_comfy_proxy.py catalog.py hub_auth.py auth.py collect_hardware.py dashboard_data.py dashboard_history.py /app/ +COPY voxcpm-ui.js separator-ui.js music-ui.js audio-cpp-ui.js ltx-original-ui.js backup-ui.js video-ui.js stt-ui.js tts-ui.js auto-test-ui.js chat-test-ui.js endpoint-ui.js docker-ui.js image-test-ui.js profiles-ui.js dashboard-ui.js index.html app.js studio.js runtime-ui.js catalog-ui.js style.css dashboard.css themes.css login.html login.js access-ui.js network-ui.js /app/ COPY deploy/docker_backup.py /app/deploy/docker_backup.py COPY network/__init__.py network/client.py network/config.py network/rpc.py /app/network/ ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 HOME=/tmp \ @@ -26,3 +26,5 @@ ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 HOME=/tmp \ DECK_NETWORK_MODE=disabled USER 65534:65534 CMD ["python3", "server.py", "--port", "8108"] + +COPY deploy/voxcpm-requirements.lock /app/deploy/voxcpm-requirements.lock diff --git a/deploy/Dockerfile.app-update b/deploy/Dockerfile.app-update index 5e54f6a..9338fe3 100644 --- a/deploy/Dockerfile.app-update +++ b/deploy/Dockerfile.app-update @@ -6,8 +6,8 @@ WORKDIR /app COPY deploy/image-requirements.lock /app/deploy/image-requirements.lock COPY deploy/tts-requirements.lock /app/deploy/tts-requirements.lock COPY deploy/ltx-original-requirements.lock /app/deploy/ltx-original-requirements.lock -COPY separator_sources.json separator.py separator_runtime.py separator_worker.py music.py audio_cpp_runtime.py ltx_original_runtime.py ltx_original_entry.py video_original.py backup.py backup_codec.py audio_policy.py prompt_enhancer.py prompt_enhancer_worker.py api_compat.py stt.py execution_setup.py tts_runtime.py tts_test.py tts_worker.py auto_test.py chat_test.py endpoint.py inference.py docker_support.py image_encoder_node.py image_runtime.py image_test.py image_upload.py profiles.py capacity.py server.py runtime.py video.py video_proxy.py video_comfy.py video_comfy_node.py video_comfy_proxy.py catalog.py hub_auth.py auth.py collect_hardware.py dashboard_data.py dashboard_history.py /app/ -COPY separator-ui.js music-ui.js audio-cpp-ui.js ltx-original-ui.js backup-ui.js video-ui.js stt-ui.js tts-ui.js auto-test-ui.js chat-test-ui.js endpoint-ui.js docker-ui.js image-test-ui.js profiles-ui.js dashboard-ui.js index.html app.js studio.js runtime-ui.js catalog-ui.js style.css dashboard.css themes.css login.html login.js access-ui.js network-ui.js /app/ +COPY voxcpm_runtime.py separator_sources.json separator.py separator_runtime.py separator_worker.py music.py audio_cpp_runtime.py ltx_original_runtime.py ltx_original_entry.py video_original.py backup.py backup_codec.py audio_policy.py prompt_enhancer.py prompt_enhancer_worker.py api_compat.py stt.py execution_setup.py tts_runtime.py tts_test.py tts_worker.py auto_test.py chat_test.py endpoint.py inference.py docker_support.py image_encoder_node.py image_runtime.py image_test.py image_upload.py profiles.py capacity.py server.py runtime.py video.py video_proxy.py video_comfy.py video_comfy_node.py video_comfy_proxy.py catalog.py hub_auth.py auth.py collect_hardware.py dashboard_data.py dashboard_history.py /app/ +COPY voxcpm-ui.js separator-ui.js music-ui.js audio-cpp-ui.js ltx-original-ui.js backup-ui.js video-ui.js stt-ui.js tts-ui.js auto-test-ui.js chat-test-ui.js endpoint-ui.js docker-ui.js image-test-ui.js profiles-ui.js dashboard-ui.js index.html app.js studio.js runtime-ui.js catalog-ui.js style.css dashboard.css themes.css login.html login.js access-ui.js network-ui.js /app/ COPY deploy/docker_backup.py /app/deploy/docker_backup.py COPY network/__init__.py network/client.py network/config.py network/rpc.py /app/network/ ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 HOME=/tmp \ @@ -16,3 +16,5 @@ ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 HOME=/tmp \ DECK_NETWORK_MODE=disabled USER 65534:65534 CMD ["python3", "server.py", "--port", "8108"] + +COPY deploy/voxcpm-requirements.lock /app/deploy/voxcpm-requirements.lock diff --git a/deploy/install.py b/deploy/install.py index 117765f..62c2699 100644 --- a/deploy/install.py +++ b/deploy/install.py @@ -14,7 +14,7 @@ import urllib.request ROOT = Path(__file__).resolve().parent.parent LABEL = 'de.casaderoll.athena-deck.standalone' -FILES = ['network/native.py','deploy/setup_network_helper.py','separator_sources.json','deploy/separator-web/Dockerfile','deploy/separator-web/app.py','deploy/separator-web/index.html','separator.py','separator_runtime.py','separator_worker.py','separator-ui.js','deploy/ladypoly/Dockerfile','deploy/ladypoly/bridge.py','music.py','music-ui.js','audio_cpp_runtime.py','audio-cpp-ui.js','ltx_original_runtime.py','ltx_original_entry.py','video_original.py','ltx-original-ui.js','deploy/ltx-original-requirements.lock','backup.py','backup_codec.py','backup-ui.js','deploy/docker_backup.py','deploy/swarm-ui/Dockerfile','deploy/swarm-ui/patch-source.py','deploy/swarm-ui/proxy.py','deploy/swarm-ui/entrypoint.py','deploy/Dockerfile.app-update','video_comfy.py','video_comfy_node.py','video_comfy_proxy.py','prompt_enhancer.py','prompt_enhancer_worker.py','image_upload.py','audio_policy.py','deploy/tts-requirements.lock','stt.py','stt-ui.js','api_compat.py','execution_setup.py','tts_runtime.py','tts_test.py','tts_worker.py','tts-ui.js','auto_test.py','auto-test-ui.js','chat_test.py','chat-test-ui.js','endpoint.py','inference.py','endpoint-ui.js','docker_support.py','docker-ui.js','deploy/docker_helper.py','deploy/setup_docker_helper.py','image_encoder_node.py','image_runtime.py','image_test.py','image-test-ui.js','profiles.py','profiles-ui.js','capacity.py','runtime.py','runtime-ui.js','video.py','video_proxy.py','video-ui.js','catalog.py','hub_auth.py','catalog-ui.js','server.py','auth.py','collect_hardware.py','dashboard_data.py','dashboard_history.py','dashboard-ui.js','dashboard.css','themes.css','deploy/import_dashboard_history.py','index.html','app.js','studio.js','style.css','login.html','login.js','access-ui.js','network-ui.js','network/__init__.py','network/client.py','network/config.py','network/rpc.py','deploy/Dockerfile','deploy/image-requirements.lock'] +FILES = ['voxcpm_runtime.py','voxcpm-ui.js','deploy/voxcpm-requirements.lock','network/native.py','deploy/setup_network_helper.py','separator_sources.json','deploy/separator-web/Dockerfile','deploy/separator-web/app.py','deploy/separator-web/index.html','separator.py','separator_runtime.py','separator_worker.py','separator-ui.js','deploy/ladypoly/Dockerfile','deploy/ladypoly/bridge.py','music.py','music-ui.js','audio_cpp_runtime.py','audio-cpp-ui.js','ltx_original_runtime.py','ltx_original_entry.py','video_original.py','ltx-original-ui.js','deploy/ltx-original-requirements.lock','backup.py','backup_codec.py','backup-ui.js','deploy/docker_backup.py','deploy/swarm-ui/Dockerfile','deploy/swarm-ui/patch-source.py','deploy/swarm-ui/proxy.py','deploy/swarm-ui/entrypoint.py','deploy/Dockerfile.app-update','video_comfy.py','video_comfy_node.py','video_comfy_proxy.py','prompt_enhancer.py','prompt_enhancer_worker.py','image_upload.py','audio_policy.py','deploy/tts-requirements.lock','stt.py','stt-ui.js','api_compat.py','execution_setup.py','tts_runtime.py','tts_test.py','tts_worker.py','tts-ui.js','auto_test.py','auto-test-ui.js','chat_test.py','chat-test-ui.js','endpoint.py','inference.py','endpoint-ui.js','docker_support.py','docker-ui.js','deploy/docker_helper.py','deploy/setup_docker_helper.py','image_encoder_node.py','image_runtime.py','image_test.py','image-test-ui.js','profiles.py','profiles-ui.js','capacity.py','runtime.py','runtime-ui.js','video.py','video_proxy.py','video-ui.js','catalog.py','hub_auth.py','catalog-ui.js','server.py','auth.py','collect_hardware.py','dashboard_data.py','dashboard_history.py','dashboard-ui.js','dashboard.css','themes.css','deploy/import_dashboard_history.py','index.html','app.js','studio.js','style.css','login.html','login.js','access-ui.js','network-ui.js','network/__init__.py','network/client.py','network/config.py','network/rpc.py','deploy/Dockerfile','deploy/image-requirements.lock'] def run(*args, check=True, interactive=False): @@ -206,7 +206,7 @@ def update(base, rollback=False, force=False, api_ports=None, reuse_runtime=Fals backup_name=config['name']+'-previous' if inspect(backup_name):raise RuntimeError('Rückfall-Containername belegt. Keine Änderung ausgeführt.') stamp=str(time.time_ns()) - shutil.copytree(base/'state',base/'backups'/stamp,ignore=shutil.ignore_patterns('music-verification','music-test-temp','music-jobs','separator-runtime','separator-tests','audio-cpp-runtime','ltx-original-runtime','original-work','tts-runtime','tts-tests','models','runtime','runtime-verification','image-tests','image-verification','image-verification-*','image-runtime','video-runtime','video-verification','video-verification-*','comfy-work','router-verification-*')) + shutil.copytree(base/'state',base/'backups'/stamp,ignore=shutil.ignore_patterns('music-verification','music-test-temp','music-jobs','separator-runtime','separator-tests','voxcpm-runtime','audio-cpp-runtime','ltx-original-runtime','original-work','tts-runtime','tts-tests','models','runtime','runtime-verification','image-tests','image-verification','image-verification-*','image-runtime','video-runtime','video-verification','video-verification-*','comfy-work','router-verification-*')) was_running=previous['State']['Running'] if was_running:run('docker','stop','--time','15',config['name']) run('docker','rename',config['name'],backup_name) diff --git a/deploy/voxcpm-requirements.lock b/deploy/voxcpm-requirements.lock new file mode 100644 index 0000000..33d0036 --- /dev/null +++ b/deploy/voxcpm-requirements.lock @@ -0,0 +1,5 @@ +# Maintained VoxCPM installer constraints. CUDA wheels use the official cu128 index. +voxcpm==2.0.3 +torch==2.11.0 +torchaudio==2.11.0 +transformers==4.57.6 diff --git a/execution_setup.py b/execution_setup.py index ac72535..41aa6ee 100644 --- a/execution_setup.py +++ b/execution_setup.py @@ -22,7 +22,7 @@ def _assess(model, installed, profile=None): result['family']='Qwen3-TTS CustomVoice' result.update(runtime='Qwen3-TTS CustomVoice',route='#tts-runtime',components=['Vollständiges Modellpaket und Sprach-Tokenizer (im Einrichtungsassistenten enthalten)']) elif voice_recipe(model): - result.update(family='VoxCPM2',evidence='Offizielles OpenBMB-Modellpaket',state='adapter_missing',label='VoxCPM2 erkannt · Ausführung noch nicht angebunden',runtime=None,components=[x['label'] for x in voice_recipe(model).values()],message='Die heruntergeladenen Dateien bilden das Modellpaket. Für die Ausführung fehlt noch die VoxCPM-Python/PyTorch-Laufzeit samt Deck-Worker. audio.cpp und llama.cpp führen diese Safetensors-Datei nicht aus. Textencoder und Vision-Projektor sind nicht separat erforderlich. Unter Komponenten werden die tatsächlich benötigten Dateien angeboten.') + result.update(family='VoxCPM2',evidence='Offizielles OpenBMB-Modellpaket',state='adapter_missing',label='VoxCPM2 erkannt · Ausführung noch nicht angebunden',runtime=None,route='#voxcpm-runtime',components=[x['label'] for x in voice_recipe(model).values()],message='Die heruntergeladenen Dateien bilden das Modellpaket. Für die Ausführung fehlt noch die VoxCPM-Python/PyTorch-Laufzeit samt Deck-Worker. audio.cpp und llama.cpp führen diese Safetensors-Datei nicht aus. Textencoder und Vision-Projektor sind nicht separat erforderlich. Unter Komponenten werden die tatsächlich benötigten Dateien angeboten.') elif kind=='chat' and filename.lower().endswith('.gguf'): result.update(runtime='llama.cpp',route='#runtime',state='check',label='Kompatibilität beim Laden prüfen',message='llama.cpp ist der mögliche Ausführungsweg für dieses Chat-GGUF. Architektur und gewählte Optionen müssen vom installierten Build unterstützt werden. Der Testchat prüft die tatsächliche Ausführung.') elif kind=='image' and filename.lower().endswith(('.safetensors','.gguf')) and (meta.get('pipeline_tag') in ('text-to-image','image-to-image') or meta.get('library_name') in ('diffusers','comfyui')): @@ -59,7 +59,8 @@ def assess(model, installed, profile=None): def add(id,label,key,route,reason,maintained=True): result['candidates'].append(dict(id=id,label=label,installed=bool(installed.get(key)),route=route,installation_available=maintained,evidence=reason)) if voice_recipe(model): - add('voxcpm','VoxCPM · Python/PyTorch','voxcpm',None,'Offizielle VoxCPM2-Laufzeit; noch kein Deck-Installer oder Worker vorhanden',False) + result['installed']=bool(installed.get('voxcpm'));result['message']='VoxCPM-Laufzeit '+('bereits installiert' if result['installed'] else 'fehlt: unter Ausführung einrichten installieren')+'. Die Modellkomponenten werden separat zugeordnet. Der Deck-Worker für Generierung fehlt noch.' + add('voxcpm','VoxCPM · Python/PyTorch','voxcpm','#voxcpm-runtime','Originale VoxCPM-Laufzeit · GUI-Installer verfügbar; Generierungsworker noch nicht angebunden',True) if result.get('runtime'): add('current',result['runtime'],kind,result['route'],'Vorhandene Deck-Zuordnung') if kind=='music' and repo=='audio-cpp/yue2-3b-gguf' and name.startswith('yue2-3b-') and name.endswith('.gguf'): diff --git a/index.html b/index.html index 76bc678..80671d9 100644 --- a/index.html +++ b/index.html @@ -1 +1 @@ -Athena Deck
ATHENA CONTROL SURFACEv0.7 · Router
+Athena Deck
ATHENA CONTROL SURFACEv0.7 · Router
diff --git a/profiles-ui.js b/profiles-ui.js index f32d250..3a9f773 100644 --- a/profiles-ui.js +++ b/profiles-ui.js @@ -37,7 +37,7 @@ window.ProfilesUI=(()=>{ el('profile-editor').innerHTML='

Komponenten werden geprüft …

'; try{ const data=await api('profiles/components?'+new URLSearchParams({model_id:profile.model_id,...(extraRepo?{repo:extraRepo}:{})}));if(!root.isConnected||sequence!==panelSequence)return; - el('profile-editor').innerHTML=`

Komponenten · ${e(profile.name)}

${e(data.message)}

Andere Komponentenquelle auswählen (ungeprüft)
${data.supported?`

Quelle: Modellkarte und Lizenz ↗ · ${e(data.license||'Lizenz nicht angegeben')}

Laufzeit: ${e(data.runtime)} · ${data.manual?'Zuordnung ungeprüft':data.runtime_installed?'installiert':'noch nicht eingerichtet'}

${!data.manual?'':'

Nur benötigte Dateien einzeln auswählen; diese Liste ist kein automatischer Downloadplan.

'}
${data.requirements.map(r=>`

${e(r.label)}

${r.candidates.map((f,i)=>`

${e(f.name)} · ${(f.size/1024**3).toFixed(2)} GiB · Version ${e(f.revision.slice(0,12))}

${f.gated&&!f.auth_configured?'

Hugging-Face-Zugang unter Einstellungen → Zugang hinterlegen und Modellfreigabe prüfen.

':''}
`).join('')||'

Keine passende Quelldatei verfügbar.

'}
`).join('')}

Lade je eine Datei pro benötigter Komponente. Downloads erscheinen im Reiter Downloads. Danach Bibliothek aktualisieren, Dateien auswählen und die Zuordnung speichern. Es startet kein Modell.

`:''}

`; + el('profile-editor').innerHTML=`

Komponenten · ${e(profile.name)}

${e(data.message)}

Andere Komponentenquelle auswählen (ungeprüft)
${data.supported?`

Quelle: Modellkarte und Lizenz ↗ · ${e(data.license||'Lizenz nicht angegeben')}

${data.runtime_route?`

${data.runtime_installed?'Laufzeit ansehen':'Passende Laufzeit installieren'} →

`:''}

Laufzeit: ${e(data.runtime)} · ${data.manual?'Zuordnung ungeprüft':data.runtime_installed?'installiert':'noch nicht eingerichtet'}

${!data.manual?'':'

Nur benötigte Dateien einzeln auswählen; diese Liste ist kein automatischer Downloadplan.

'}
${data.requirements.map(r=>`

${e(r.label)}

${r.candidates.map((f,i)=>`

${e(f.name)} · ${(f.size/1024**3).toFixed(2)} GiB · Version ${e(f.revision.slice(0,12))}

${f.gated&&!f.auth_configured?'

Hugging-Face-Zugang unter Einstellungen → Zugang hinterlegen und Modellfreigabe prüfen.

':''}
`).join('')||'

Keine passende Quelldatei verfügbar.

'}
`).join('')}

Lade je eine Datei pro benötigter Komponente. Downloads erscheinen im Reiter Downloads. Danach Bibliothek aktualisieren, Dateien auswählen und die Zuordnung speichern. Es startet kein Modell.

`:''}

`; const missing=data.requirements?.filter(r=>!r.available.length&&!r.optional)||[]; if(el('components-download-all')){el('components-download-all').disabled=!missing.length;el('components-download-all').onclick=async()=>{ const b=el('components-download-all');b.disabled=true;let count=0; diff --git a/profiles.py b/profiles.py index 9faec5a..c330104 100644 --- a/profiles.py +++ b/profiles.py @@ -277,7 +277,7 @@ class Profiles: except ValueError:pass candidates=[dict(f,repo=source.get('repo',info.get('repo',model['repo'])),revision=source['revision'],gated=source['gated'],auth_configured=source.get('auth_configured',False)) for name in info['files'] for f in source['files'] if f['name']==name] requirements.append(dict(role=role,label=info['label'],available=available,candidates=candidates)) - if voice_recipe(model):return dict(supported=True,requirements=requirements,source='https://huggingface.co/'+model['repo']+'/blob/'+model['revision']+'/README.md',license=source['license'],runtime='VoxCPM · eigene Python/PyTorch-Laufzeit',runtime_installed=False,worker_available=False,message='VoxCPM2 benötigt genau diese fünf Zusatzdateien derselben Quellversion. Ein separater Textencoder und ein Vision-Projektor werden nicht benötigt. tokenizer.json und tokenizer_config.json gehören zusammen, sie sind keine doppelten Modelle. Vorhandene Dateien werden wiederverwendet. Der VoxCPM-Worker ist in Deck noch nicht angebunden; auch ein vollständiges Dateipaket kann deshalb noch nicht gestartet werden.') + if voice_recipe(model):return dict(supported=True,requirements=requirements,source='https://huggingface.co/'+model['repo']+'/blob/'+model['revision']+'/README.md',license=source['license'],runtime='VoxCPM · eigene Python/PyTorch-Laufzeit',runtime_installed=bool(getattr(self,'voxcpm_runtime_ready',lambda:False)()),runtime_route='#voxcpm-runtime',worker_available=False,message='VoxCPM2 benötigt genau diese fünf Zusatzdateien derselben Quellversion. Ein separater Textencoder und ein Vision-Projektor werden nicht benötigt. tokenizer.json und tokenizer_config.json gehören zusammen, sie sind keine doppelten Modelle. Vorhandene Dateien werden wiederverwendet. Die passende Laufzeit kann unter Einstellungen → Laufzeiten → VoxCPM installiert werden. Der Deck-Worker fehlt noch; ein vollständiges Dateipaket allein kann noch nicht gestartet werden.') if music_recipe(model):return dict(supported=True,requirements=requirements,source='https://huggingface.co/'+model['repo']+'/blob/'+model['revision']+'/README.md',license=source['license'],runtime='audio.cpp',runtime_installed=bool(getattr(self,'music_runtime_ready',lambda:False)()),message='Passendes YuE2-GGUF-Paket: je eine VAE-Datei und die vier Zusatzdateien derselben Quellversion. F16-VAE ist die erste Downloadempfehlung. Vorhandene Dateien wiederverwenden, fehlende herunterladen und danach zuordnen. Die YuE2-Musikanbindung verwendet diese Dateien über audio.cpp. Speichern startet kein Modell; anschließend Musik → Testen verwenden.') if video:return dict(supported=True,requirements=requirements,source='https://huggingface.co/'+model['repo']+'/blob/'+model['revision']+'/README.md',license=source['license'],runtime='ComfyUI · gemeinsame Bild- und Videolaufzeit',runtime_installed=bool(getattr(self,'video_runtime_ready',lambda:False)()),message='Geprüftes Dateirezept nach Herstellerdokumentation für die zweistufige Distilled-Pipeline mit fester Bildanzahl. Alle Komponenten stammen aus derselben Version wie dein Transformer. Kein beliebiger Gemma-Encoder; keine Comfy-INT8-Dateien. Optionaler Duration-Head, Temporal-Upsampling und DFR sind nicht Teil dieses Rezepts. Vorhandene ComfyUI-Laufzeit wiederverwenden; Videomodell im Bereich Video aktivieren. Vollständige Dateien sind keine Speicherzusage.') if not image_recipe(model):return dict(supported=True,requirements=requirements,source=source['url'],license=source['license'],runtime='audio.cpp · Paketdefinition',runtime_installed=bool(getattr(self,'music_runtime_ready',lambda:False)()),message='Zusatzdateien aus der installierten Laufzeit-Paketdefinition. Weitere modellabhängige Pakete können erforderlich sein; dies ist keine vollständige Worker-Zusage.') diff --git a/server.py b/server.py index 429e807..3ea9d7e 100644 --- a/server.py +++ b/server.py @@ -9,6 +9,7 @@ import secrets from http.cookies import SimpleCookie, CookieError from auth import CredentialStore, verify_password from catalog import Catalog +from voxcpm_runtime import VoxCPMRuntime from tts_runtime import TTSRuntime from stt import STT,read_upload from audio_policy import AudioPolicy @@ -84,6 +85,8 @@ class Server(ThreadingHTTPServer): self.image_tests.enhancer=self.prompt_enhancer self.profiles.prompt_enhancer_ready=lambda task:bool(self.prompt_enhancer.installed(task)) self.profiles.image_runtime_ready=lambda:self.image_tests.status()["runtime_installed"] + self.voxcpm_runtime=VoxCPMRuntime(self.catalog.root.parent/'voxcpm-runtime',self.image_runtime) + self.profiles.voxcpm_runtime_ready=lambda:self.voxcpm_runtime.status()['installed'] self.tts_runtime=TTSRuntime(self.catalog.root.parent/'tts-runtime') self.tts_tests=TTSTests(self.catalog.root.parent/'tts-tests',self.profiles,self.tts_runtime) self.profiles.tts_blockers=self.tts_tests.blockers @@ -308,7 +311,7 @@ class Handler(BaseHTTPRequestHandler): 'audio':self.server.tts_runtime.status().get('installed',False), 'video':video_runtime.status().get('installed',False), 'audio_cpp':self.server.audio_cpp_runtime.status()['installed'], - 'music_worker':True,'separator':self.server.separator_runtime.status()['installed']} + 'voxcpm':self.server.voxcpm_runtime.status()['installed'],'music_worker':True,'separator':self.server.separator_runtime.status()['installed']} def execution_setup(self, model, profile=None, installed=None): return execution_assess(model, self.execution_runtime_snapshot() if installed is None else installed, profile) @@ -324,7 +327,7 @@ class Handler(BaseHTTPRequestHandler): return self.respond({'error':'Anmeldung erforderlich.'},401) if not self.authenticated() and self.path == '/': return self.respond((ROOT/'login.html').read_bytes(), mime='text/html; charset=utf-8') - routes = {'/separator-ui.js':('separator-ui.js','text/javascript'),'/music-ui.js':('music-ui.js','text/javascript'),'/audio-cpp-ui.js':('audio-cpp-ui.js','text/javascript'),'/ltx-original-ui.js':('ltx-original-ui.js','text/javascript'),'/backup-ui.js':('backup-ui.js','text/javascript'),'/dashboard-ui.js':('dashboard-ui.js','text/javascript'),'/dashboard.css':('dashboard.css','text/css'),'/themes.css':('themes.css','text/css'),'/video-ui.js':('video-ui.js','text/javascript'),'/stt-ui.js':('stt-ui.js','text/javascript'),'/tts-ui.js':('tts-ui.js','text/javascript'),'/auto-test-ui.js':('auto-test-ui.js','text/javascript'),'/chat-test-ui.js':('chat-test-ui.js','text/javascript'),'/endpoint-ui.js':('endpoint-ui.js','text/javascript'),'/docker-ui.js': ('docker-ui.js','text/javascript'), '/': ('index.html', 'text/html; charset=utf-8'), '/app.js': ('app.js', 'text/javascript'), '/style.css': ('style.css', 'text/css'), '/network-ui.js': ('network-ui.js', 'text/javascript'), '/access-ui.js': ('access-ui.js', 'text/javascript'), '/studio.js': ('studio.js', 'text/javascript'), '/catalog-ui.js': ('catalog-ui.js','text/javascript'), '/runtime-ui.js': ('runtime-ui.js','text/javascript'), '/profiles-ui.js': ('profiles-ui.js','text/javascript'), '/image-test-ui.js': ('image-test-ui.js','text/javascript')} + routes = {'/voxcpm-ui.js':('voxcpm-ui.js','text/javascript'),'/separator-ui.js':('separator-ui.js','text/javascript'),'/music-ui.js':('music-ui.js','text/javascript'),'/audio-cpp-ui.js':('audio-cpp-ui.js','text/javascript'),'/ltx-original-ui.js':('ltx-original-ui.js','text/javascript'),'/backup-ui.js':('backup-ui.js','text/javascript'),'/dashboard-ui.js':('dashboard-ui.js','text/javascript'),'/dashboard.css':('dashboard.css','text/css'),'/themes.css':('themes.css','text/css'),'/video-ui.js':('video-ui.js','text/javascript'),'/stt-ui.js':('stt-ui.js','text/javascript'),'/tts-ui.js':('tts-ui.js','text/javascript'),'/auto-test-ui.js':('auto-test-ui.js','text/javascript'),'/chat-test-ui.js':('chat-test-ui.js','text/javascript'),'/endpoint-ui.js':('endpoint-ui.js','text/javascript'),'/docker-ui.js': ('docker-ui.js','text/javascript'), '/': ('index.html', 'text/html; charset=utf-8'), '/app.js': ('app.js', 'text/javascript'), '/style.css': ('style.css', 'text/css'), '/network-ui.js': ('network-ui.js', 'text/javascript'), '/access-ui.js': ('access-ui.js', 'text/javascript'), '/studio.js': ('studio.js', 'text/javascript'), '/catalog-ui.js': ('catalog-ui.js','text/javascript'), '/runtime-ui.js': ('runtime-ui.js','text/javascript'), '/profiles-ui.js': ('profiles-ui.js','text/javascript'), '/image-test-ui.js': ('image-test-ui.js','text/javascript')} if self.path in routes: name, mime = routes[self.path] return self.respond((ROOT/name).read_bytes(), mime=mime) @@ -347,6 +350,8 @@ class Handler(BaseHTTPRequestHandler): if urlsplit(self.path).path == '/api/v1/music/audio': try:return self.respond(self.server.music.audio(parse_qs(urlsplit(self.path).query).get('id',[''])[0]),mime='audio/wav') except (OSError,ValueError):return self.respond({'error':'Musik nicht verfügbar.'},404) + if self.path == '/api/v1/voxcpm-runtime':return self.respond(self.server.voxcpm_runtime.status()) + if self.path == '/api/v1/voxcpm-runtime/log':return self.respond(self.server.voxcpm_runtime.log()) if self.path == '/api/v1/separator-runtime':return self.respond(self.server.separator_runtime.status()) if self.path == '/api/v1/separator-runtime/log':return self.respond(self.server.separator_runtime.log()) if self.path == '/api/v1/separator-tests':return self.respond(self.server.separator_tests.status()) @@ -547,6 +552,11 @@ class Handler(BaseHTTPRequestHandler): if self.path == '/api/v1/separator-profiles/save': try:return self.respond(self.server.separator_tests.configure(self.read_json())) except (ValueError,OSError) as exc:return self.respond({'error':str(exc)},400) + if self.path in ('/api/v1/voxcpm-runtime/install','/api/v1/voxcpm-runtime/cancel'): + try: + if self.read_json():raise ValueError('Keine Parameter erwartet.') + return self.respond(self.server.voxcpm_runtime.stop() if self.path.endswith('/cancel') else self.server.voxcpm_runtime.start()) + except (ValueError,OSError) as exc:return self.respond({'error':str(exc)},400) if self.path in ('/api/v1/separator-runtime/install','/api/v1/separator-runtime/cancel','/api/v1/separator-runtime/download'): try: data=self.read_json() @@ -751,7 +761,7 @@ def main(): server.endpoint.close() server.stt.stop() server.tts_tests.stop() - server.tts_runtime.stop() + server.tts_runtime.stop();server.voxcpm_runtime.stop() server.image_runtime.stop() server.ltx_original_runtime.stop() server.separator_tests.stop();server.separator_runtime.stop();server.music.stop();server.audio_cpp_runtime.stop() diff --git a/studio.js b/studio.js index 1b5650b..db29cd1 100644 --- a/studio.js +++ b/studio.js @@ -8,10 +8,11 @@ const Studio=(()=>{ if(!force&&document.querySelector('#studio')?.dataset.page===page)return; if(category!==page){category=page;section='discover';} if(modelId)section='profiles'; - const body=page==='stt-runtime'?STTUI.html(true):page==='tts-runtime'?TTSUI.html(true):['docker-runtime','services'].includes(page)?DockerUI.html(page==='services'):page==='runtimes'?runtimeOverview():page==='image-runtime'?imageRuntime():page==='runtime'?RuntimeUI.html():`
ATHENA / MODELLVERWALTUNG

${labels[page]}

${audioDescriptions[page]||'Modelle entdecken, herunterladen und mit gespeicherten Profilen konfigurieren.'}

${page==='voice'?'

Audio Separator einrichten → · Audio-Trennung →

Audio-Trennung nutzt die unterstützten Pakete im Audio-Separator-Bereich. Andere Hub-Dateien und Voice-Profile benötigen weiterhin eine geprüfte Anbindung.

':''}${audioDescriptions[page]&&!['audio','stt','music','voice'].includes(page)?'

Entdecken und Downloads sind verfügbar. Für diese Audio-Bereiche ist noch keine ausführbare Laufzeit in Deck angebunden. Gespeicherte Profile sind vorbereitend.

':''}
${[['discover','Entdecken'],['library','Bibliothek'],['downloads','Downloads'],['profiles','Profile'],...(['audio','stt'].includes(page)?[['setup','Einrichten']]:[]),...(['image','chat','audio','stt','music'].includes(page)?[['test','Testen'],...(page==='chat'?[['auto','Auto-Test']]:[])]:[])].map(([id,label])=>``).join('')}
${section==='setup'?(page==='stt'?STTUI.html(true):TTSUI.html(true)):['test','running'].includes(section)&&page==='music'?MusicUI.html():['test','running'].includes(section)&&page==='stt'?STTUI.html():['test','running'].includes(section)&&page==='audio'?TTSUI.html():section==='auto'?AutoTestUI.html():section==='test'?(page==='chat'?ChatTestUI.html():ImageTestUI.html()):section==='running'&&page==='image'?ImageTestUI.html(true):section==='profiles'?ProfilesUI.html():section==='running'&&page==='chat'?EndpointUI.runningHTML():section==='running'?'

Keine von Deck gestarteten Modelle

Profile werden auf Athena gespeichert. Für diesen Bereich ist noch kein Modellworker angebunden. Fehlende Komponenten stehen beim jeweiligen Profil.

llama.cpp-Builds verwalten →
':CatalogUI.html(section==='library',section==='downloads')}
`; + const body=page==='voxcpm-runtime'?VoxCPMUI.html():page==='stt-runtime'?STTUI.html(true):page==='tts-runtime'?TTSUI.html(true):['docker-runtime','services'].includes(page)?DockerUI.html(page==='services'):page==='runtimes'?runtimeOverview():page==='image-runtime'?imageRuntime():page==='runtime'?RuntimeUI.html():`
ATHENA / MODELLVERWALTUNG

${labels[page]}

${audioDescriptions[page]||'Modelle entdecken, herunterladen und mit gespeicherten Profilen konfigurieren.'}

${page==='voice'?'

Audio Separator einrichten → · Audio-Trennung →

Audio-Trennung nutzt die unterstützten Pakete im Audio-Separator-Bereich. Andere Hub-Dateien und Voice-Profile benötigen weiterhin eine geprüfte Anbindung.

':''}${audioDescriptions[page]&&!['audio','stt','music','voice'].includes(page)?'

Entdecken und Downloads sind verfügbar. Für diese Audio-Bereiche ist noch keine ausführbare Laufzeit in Deck angebunden. Gespeicherte Profile sind vorbereitend.

':''}
${[['discover','Entdecken'],['library','Bibliothek'],['downloads','Downloads'],['profiles','Profile'],...(['audio','stt'].includes(page)?[['setup','Einrichten']]:[]),...(['image','chat','audio','stt','music'].includes(page)?[['test','Testen'],...(page==='chat'?[['auto','Auto-Test']]:[])]:[])].map(([id,label])=>``).join('')}
${section==='setup'?(page==='stt'?STTUI.html(true):TTSUI.html(true)):['test','running'].includes(section)&&page==='music'?MusicUI.html():['test','running'].includes(section)&&page==='stt'?STTUI.html():['test','running'].includes(section)&&page==='audio'?TTSUI.html():section==='auto'?AutoTestUI.html():section==='test'?(page==='chat'?ChatTestUI.html():ImageTestUI.html()):section==='running'&&page==='image'?ImageTestUI.html(true):section==='profiles'?ProfilesUI.html():section==='running'&&page==='chat'?EndpointUI.runningHTML():section==='running'?'

Keine von Deck gestarteten Modelle

Profile werden auf Athena gespeichert. Für diesen Bereich ist noch kein Modellworker angebunden. Fehlende Komponenten stehen beim jeweiligen Profil.

llama.cpp-Builds verwalten →
':CatalogUI.html(section==='library',section==='downloads')}
`; document.querySelector('#view').innerHTML=`
${body}
`; if(['docker-runtime','services'].includes(page)){DockerUI.bind(page==='services');return;} if(page==='stt-runtime'){STTUI.bind(true);return;} + if(page==='voxcpm-runtime'){VoxCPMUI.bind();return;} if(page==='tts-runtime'){TTSUI.bind(true);return;} if(page==='runtime'){RuntimeUI.bind();return;} if(page==='image-runtime'){ImageTestUI.bind(true);ImageTestUI.bindInstaller();return;} @@ -36,6 +37,7 @@ const Studio=(()=>{ ['audio.cpp','Musik & Audio','audio-cpp-runtime','/api/v1/audio-cpp-runtime','CUDA-Build einmal erstellen. YuE2 ist angebunden; andere Modellfamilien brauchen eine geprüfte Worker-Anbindung.'], ['LTX Original','Video','ltx-original-runtime','/api/v1/ltx-original-runtime','Originales LTX-Backend für LTX DeskWEB. Modelle und Zusatzdateien bleiben in der Bibliothek.'], ['Docker Engine','Weitere Dienste','docker-runtime','/api/v1/docker','Optionale Container-Umgebung. Erstinstallation über den Systemhelfer; vorhandenes Docker wird nicht automatisch aktualisiert.'], + ['VoxCPM','Stimmwerkzeuge','voxcpm-runtime','/api/v1/voxcpm-runtime','Originale VoxCPM-Python-Laufzeit installieren. Vorhandene CUDA-Pakete wiederverwenden; Modellkomponenten separat. Deck-Generierungsworker noch nicht angebunden.'], ['Audio Separator','Audio-Trennung','separator-runtime','/api/v1/separator-runtime','Native CUDA-Laufzeit für BS-Roformer, MelBand und Demucs FT/6s. Vollständige Modellpakete mit überprüften Originalquellen und Prüfsummen.'] ]; const runtimeEscape=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); diff --git a/test_backup.py b/test_backup.py index 5302259..cad34f1 100644 --- a/test_backup.py +++ b/test_backup.py @@ -51,6 +51,19 @@ class RestoreTests(unittest.TestCase): doc=open_backup(self.server.backup.export('backup secure password'),'backup secure password') self.assertNotIn('PRIVATE-CHAT-LOG',json.dumps(doc));self.assertNotIn('model-weights-not-in-backup',json.dumps(doc));self.assertIn('history',doc);validate(doc) self.assertEqual(doc['models'][0]['id'],ident);self.assertEqual(len(doc['settings']['profiles.json']),1) + def test_voxcpm_runtime_export_and_restore(self): + from voxcpm_runtime import VERSION + runtime=Mock();runtime.status.return_value={'installed':True,'version':VERSION,'job':{'state':'complete'}} + self.server.voxcpm_runtime=runtime + doc=self.server.backup.snapshot();validate(doc) + self.assertTrue(doc['runtimes']['voxcpm']);self.assertEqual(doc['runtime_versions']['voxcpm'],VERSION) + bad=copy.deepcopy(doc);bad['runtimes']['voxcpm']='yes' + with self.assertRaises(ValueError):validate(bad) + runtime.status.return_value={'installed':False,'version':VERSION,'job':{'state':'complete'}} + summary=self.server.backup.inspect(seal(doc,'backup secure password'),'backup secure password') + self.assertFalse(summary['blockers']) + self.server.backup.start(dict(id=summary['id'],services=[],confirm=True,restore_credentials=False));job=self.wait() + self.assertEqual(job['state'],'complete',job);runtime.start.assert_called_once() def test_separator_restore_installs_runtime_and_redownloads_package(self): from separator_runtime import MODELS doc=self.server.backup.snapshot();name=next(iter(MODELS));doc['runtimes']['separator']={'installed':True,'models':[name]} diff --git a/test_voxcpm_components.py b/test_voxcpm_components.py index badeb70..d27330a 100644 --- a/test_voxcpm_components.py +++ b/test_voxcpm_components.py @@ -31,4 +31,4 @@ class VoxCPMComponentsTests(unittest.TestCase): self.assertEqual([f['name'] for f in c.files('other/VoxCPM2')['files']],['model.safetensors']) def test_execution_does_not_claim_installed_audio_cpp_can_run_vox(self): result=assess(dict(kind='voice',repo='openbmb/VoxCPM2',file='model.safetensors'),dict(audio_cpp=True)) - self.assertEqual(result['family'],'VoxCPM2');self.assertEqual(result['state'],'adapter_missing');self.assertEqual(result['candidates'][0]['id'],'voxcpm');self.assertFalse(result['candidates'][0]['installation_available']) + self.assertEqual(result['family'],'VoxCPM2');self.assertEqual(result['state'],'adapter_missing');self.assertEqual(result['candidates'][0]['id'],'voxcpm');self.assertTrue(result['candidates'][0]['installation_available']);self.assertEqual(result['candidates'][0]['route'],'#voxcpm-runtime') diff --git a/test_voxcpm_runtime.py b/test_voxcpm_runtime.py new file mode 100644 index 0000000..be19b12 --- /dev/null +++ b/test_voxcpm_runtime.py @@ -0,0 +1,28 @@ +import json,tempfile,unittest +from pathlib import Path +from unittest.mock import Mock,patch +from voxcpm_runtime import VoxCPMRuntime,VERSION +class VoxCPMRuntimeTests(unittest.TestCase): + def test_requires_verified_marker_and_valid_owned_environment(self): + with tempfile.TemporaryDirectory() as d: + r=VoxCPMRuntime(d,None);self.assertFalse(r.status()['installed']) + base=Path(d)/('a'*32);(base/'python/bin').mkdir(parents=True);(base/'python/bin/python').touch();(Path(d)/'active.json').write_text(json.dumps(dict(id=base.name))) + (base/'ready').write_text('wrong');self.assertFalse(r.status()['installed']) + (base/'ready').write_text(VERSION);self.assertTrue(r.status()['installed']);self.assertFalse(r.status()['worker_available']) + def test_success_and_failure_do_not_touch_shared_runtime_or_load_weights(self): + for fail in (False,True): + with tempfile.TemporaryDirectory() as d: + shared=Mock();shared.paths.return_value=(Path('/does/not/exist'),Path('/does/not/exist')) + r=VoxCPMRuntime(d,shared);r.job=dict(id='b'*32,state='running',phase='test') + calls=[] + def run(args,env=None): + calls.append([str(a) for a in args]) + if fail:raise RuntimeError('synthetic install failure') + r._command=run;r._run();self.assertEqual(r.job['state'],'failed' if fail else 'complete') + if fail:self.assertFalse((Path(d)/'active.json').exists());self.assertFalse((Path(d)/('b'*32)).exists()) + else: + flat=' '.join(' '.join(x) for x in calls);self.assertIn('voxcpm=='+VERSION,flat);self.assertIn('pip check',flat);self.assertNotIn('snapshot_download',flat);self.assertNotIn('model.generate(',flat) + def test_cancel_does_not_activate_partial_environment(self): + with tempfile.TemporaryDirectory() as d: + r=VoxCPMRuntime(d,None);r.job=dict(id='c'*32,state='running',phase='test');r._command=Mock(side_effect=InterruptedError());r._run() + self.assertEqual(r.job['state'],'cancelled');self.assertFalse((Path(d)/'active.json').exists()) diff --git a/voxcpm-ui.js b/voxcpm-ui.js new file mode 100644 index 0000000..d1fa866 --- /dev/null +++ b/voxcpm-ui.js @@ -0,0 +1,11 @@ +const VoxCPMUI=(()=>{ + const e=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); + function html(){return `
EINSTELLUNGEN / LAUFZEITEN

VoxCPM

VoxCPM2 einrichten

Diese Modellfamilie benötigt die originale VoxCPM-Python-Laufzeit. Eine eigene Umgebung schützt bestehende Laufzeiten; vorhandene CUDA-Pakete werden nach Möglichkeit wiederverwendet.

Status wird geprüft …

Dieser Schritt installiert die Laufzeit. Er lädt keine Modellgewichte und startet kein Modell. Die Deck-Anbindung für Generierung und Stimmklonen fehlt noch; das Profil bleibt deshalb gesperrt.

Installationsausgabe
Zurück zu Stimmwerkzeugen →
`;} + function bind(){const root=document.querySelector('#voxcpm-panel');if(!root)return;const el=id=>root.querySelector('#'+id);let busy=false; + async function api(path='',body){const r=await fetch('/api/v1/voxcpm-runtime'+path,body===undefined?{}:{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});const d=await r.json();if(!r.ok)throw Error(d.error||'Einrichtung fehlgeschlagen');return d;} + async function refresh(){try{const d=await api();if(!root.isConnected)return;el('voxcpm-status').textContent=d.job?.phase||(d.installed?'VoxCPM-Laufzeit installiert.':'Passende VoxCPM-Laufzeit fehlt.');el('voxcpm-details').textContent=`Version ${d.version} · mindestens ${d.required_disk_gib} GiB freier Speicher · ${d.shared_cuda?'CUDA-Pakete können wiederverwendet werden':'CUDA-Pakete werden mitinstalliert'}`;el('voxcpm-progress').hidden=d.job?.state!=='running';el('voxcpm-install').disabled=busy||d.installed||d.job?.state==='running';el('voxcpm-cancel').disabled=busy||d.job?.state!=='running';}catch(err){if(root.isConnected)el('voxcpm-error').textContent=err.message;}finally{if(root.isConnected)setTimeout(refresh,2000);}} + async function action(path){busy=true;el('voxcpm-install').disabled=true;el('voxcpm-error').textContent='';try{await api(path,{});}catch(err){el('voxcpm-error').textContent=err.message;}finally{busy=false;}} + el('voxcpm-install').onclick=()=>action('/install');el('voxcpm-cancel').onclick=()=>action('/cancel');el('voxcpm-log').onclick=async()=>{try{const d=await api('/log');el('voxcpm-output').textContent=d.text;}catch(err){el('voxcpm-error').textContent=err.message;}};refresh(); + } + return {html,bind}; +})(); diff --git a/voxcpm_runtime.py b/voxcpm_runtime.py new file mode 100644 index 0000000..2859f9f --- /dev/null +++ b/voxcpm_runtime.py @@ -0,0 +1,51 @@ +"""Owned, GUI-installable VoxCPM runtime; never downloads or loads models.""" +import json,os,shutil,sys,time +from pathlib import Path +from ltx_original_runtime import LTXOriginalRuntime +VERSION='2.0.3' +class VoxCPMRuntime(LTXOriginalRuntime): + name='VoxCPM' + def paths(self): + try: + ident=json.loads((self.root/'active.json').read_text())['id'] + if len(ident)==32 and all(c in '0123456789abcdef' for c in ident): + base=self.root/ident + if (base/'ready').read_text().strip()==VERSION:return base/'python/bin/python',base + except (OSError,ValueError,KeyError):pass + return self.root/'missing-python',self.root/'missing-runtime' + def status(self): + python,base=self.paths();shared=self.image_runtime.paths()[0].is_file() if self.image_runtime else False + return dict(installed=python.is_file() and (base/'ready').is_file(),version=VERSION,job=dict(self.job) if self.job else None,required_disk_gib=8 if shared else 25,shared_cuda=shared,worker_available=False,origin='OpenBMB/VoxCPM · offizielles Python-Paket',message='Laufzeitinstallation lädt keine Gewichte. Für die Generierung fehlt noch der Deck-Worker.') + def start(self): + if not shutil.which('ffmpeg'):raise ValueError('FFmpeg fehlt. Systemvoraussetzungen über den Deck-Installer einrichten.') + return super().start() + def _run(self): + base=self.root/self.job['id'];state='failed';phase='VoxCPM-Einrichtung fehlgeschlagen.' + try: + base.mkdir(mode=0o700);tmp=base/'tmp';tmp.mkdir() + # A new environment may reference existing CUDA packages but never alters them. + env={k:v for k,v in os.environ.items() if not k.startswith(('HF_','DECK_','LLAMA_'))} + env.update(TMPDIR=str(tmp),PIP_NO_INPUT='1',PIP_DISABLE_PIP_VERSION_CHECK='1',HF_HUB_OFFLINE='1',OMP_NUM_THREADS='2') + shared=self.image_runtime.paths()[0] if self.image_runtime else Path('/missing') + bootstrap=shared if shared.is_file() else Path(sys.executable) + self._phase('Eigene VoxCPM-Python-Umgebung erstellen');self._command([bootstrap,'-m','venv',base/'python'],env) + python=base/'python/bin/python' + self._command([python,'-c','import sys; assert (3,10)<=sys.version_info[:2]<(3,13), "VoxCPM benötigt Python 3.10 bis 3.12"'],env) + if shared.is_file(): + self._phase('Vorhandene CUDA-Pakete lesend wiederverwenden') + self._command([python,'-c','import json,site,subprocess,sys;from pathlib import Path;p=json.loads(subprocess.check_output([sys.argv[1],"-c","import json,site;print(json.dumps(site.getsitepackages()))"],text=True));Path(site.getsitepackages()[0],"deck-shared-cuda.pth").write_text("\\n".join(p)+"\\n")',shared],env) + self._phase('CUDA-PyTorch prüfen / installieren') + 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('VoxCPM '+VERSION+' und Abhängigkeiten installieren') + self._command([python,'-m','pip','install','--no-cache-dir','-c',Path(__file__).parent/'deploy/voxcpm-requirements.lock','voxcpm=='+VERSION],env) + self._phase('Paketkonsistenz und VoxCPM2-Unterstützung prüfen') + self._command([python,'-m','pip','check'],env) + self._command([python,'-c','import torch,soundfile;from voxcpm import VoxCPM;from voxcpm.model.voxcpm2 import VoxCPM2Model;assert torch.version.cuda;assert callable(VoxCPM.from_pretrained)'],env) + if self.cancel.is_set():raise InterruptedError() + (base/'ready').write_text(VERSION);marker=self.root/'active.tmp';marker.write_text(json.dumps(dict(id=base.name)));marker.replace(self.root/'active.json') + state='complete';phase='VoxCPM-Laufzeit installiert · noch kein Deck-Worker und kein Modell geladen' + except InterruptedError:state='cancelled';phase='VoxCPM-Einrichtung abgebrochen.' + except Exception as exc:phase=str(exc) + finally: + if 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()