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 DeckATHENA CONTROL SURFACEv0.7 · Router
+Athena DeckATHENA 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='
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.
'}`:''}`;
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.'}
Audio-Trennung nutzt die unterstützten Pakete im Audio-Separator-Bereich. Andere Hub-Dateien und Voice-Profile benötigen weiterhin eine geprüfte Anbindung.
Entdecken und Downloads sind verfügbar. Für diese Audio-Bereiche ist noch keine ausführbare Laufzeit in Deck angebunden. Gespeicherte Profile sind vorbereitend.
Audio-Trennung nutzt die unterstützten Pakete im Audio-Separator-Bereich. Andere Hub-Dateien und Voice-Profile benötigen weiterhin eine geprüfte Anbindung.
Entdecken und Downloads sind verfügbar. Für diese Audio-Bereiche ist noch keine ausführbare Laufzeit in Deck angebunden. Gespeicherte Profile sind vorbereitend.
`;
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.
InstallationsausgabeZurü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()