Fix VoxCPM2 component recipe and clarify missing execution adapter

This commit is contained in:
Mikei386 committed 2026-10-02 11:43:47 +02:00
1 parent 3cd9fa5750
commit 1f0fc42344
7 files changed
+102 -12

No files matched your search

+32
View File
@@ -0,0 +1,32 @@
# VoxCPM2: Komponentenpaket und Ausführungsstatus
Für `openbmb/VoxCPM2/model.safetensors` besitzt Deck ein eigenes Komponentenrezept:
- `audiovae.pth`: Audio-VAE-Gewichte.
- `config.json`: Modellkonfiguration einschließlich Audio-VAE-Architektur.
- `tokenizer.json`: Text-Tokenizer/Wortschatz.
- `tokenizer_config.json`: Einstellungen des Text-Tokenizers.
- `special_tokens_map.json`: Spezialtokens.
Diese Dateien müssen dieselbe feste Repository-Revision wie die Hauptgewichte
verwenden. Vorhandene Downloads werden wiederverwendet. Die zwei Tokenizer-Dateien
sind verschiedene Teile eines Pakets, keine doppelten Modelle. Separater Textencoder
und Vision-Projektor werden nicht benötigt. Der optionale externe Denoiser ist
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
llama.cpp-Installation ersetzt diese Anbindung nicht. Das Profil bleibt gesperrt.
Der Katalog erlaubt `.pth` ausschließlich für die exakt benannte offizielle
Audio-VAE-Datei dieses Repositorys. Der Download führt keine Datei aus. Das spätere
Laden muss wie im Original mit `weights_only=True` erfolgen. Kein automatisches
Ausführen von Python-Dateien aus Modell-Repositories.
Die Bibliothek gruppiert Text-Tokenizer gesondert; sie erscheinen nicht zusätzlich
als allgemeine Konfiguration. „Alle fehlenden Dateien herunterladen“ prüft zunächst,
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.
+1 -1
View File
@@ -33,7 +33,7 @@ window.CatalogUI = (() => {
function renderLibrary() { function renderLibrary() {
const openRows=new Set([...root.querySelectorAll('.detail-row[open]')].map(x=>x.dataset.rowId)); const openRows=new Set([...root.querySelectorAll('.detail-row[open]')].map(x=>x.dataset.rowId));
const entries = state.entries.filter(x => x.kind === kind); const entries = state.entries.filter(x => x.kind === kind);
const groups = [['model', kind==='chat'?'Sprachmodelle · Hauptgewichte':'Modelle · Hauptgewichte'],['vision_projector','Vision-Projektoren'],['audio_projector','Audio-Projektoren'],['text_encoder','Textencoder'],['vae','VAE · Decoder'],['configuration','Konfigurationen'],['auxiliary','Tokenizer und weitere Zusatzkomponenten']]; const groups = [['model', kind==='chat'?'Sprachmodelle · Hauptgewichte':'Modelle · Hauptgewichte'],['vision_projector','Vision-Projektoren'],['audio_projector','Audio-Projektoren'],['text_encoder','Textencoder'],['vae','VAE · Decoder'],['tokenizer','Tokenizer-Dateien'],['configuration','Konfigurationen'],['auxiliary','Weitere Zusatzkomponenten']];
const row = x => `<details class="detail-row" data-row-id="${escape(x.id)}" ${openRows.has(x.id)?'open':''}><summary class="detail-summary"><strong class="hub-filename">${escape(x.file)}</strong><span>${escape(x.role_label)}</span><span>${size(x.size)}</span><span>${x.used_by_profiles?.length?`${x.used_by_profiles.length} Profil(e)`:'Nicht zugeordnet'}</span></summary><div class="detail-body"><p class="hub-filename">Quelle: ${escape(x.download_source?.repo||x.repo)}</p>${x.download_source?`<p class="small">Lokal übernommen · Originaldatei: ${escape(x.download_source.file)} · Quellrevision: ${escape(x.download_source.revision)}</p>`:""}<p>${x.profile_eligible?'Modelldatei · über ein Profil verwenden':'Zusatzdatei · wird einem Modellprofil unter Komponenten zugeordnet'}</p><details><summary>Version und Integrität</summary><p class="hub-filename">Revision ${escape(x.revision)}</p><p>${x.upstream_hash_verified ? 'SHA-256 mit Quelle geprüft' : 'SHA-256 lokal erfasst; Größe mit Quelle geprüft'}</p></details>${executionHTML(x.execution)}${x.used_by_profiles?.length?`<p class="warning">Verwendet von Profil: ${x.used_by_profiles.map(p=>escape(p.name)).join(", ")}. Bitte zuerst das Profil löschen.</p>`:""}<div class="card-actions">${x.profile_eligible===true?`<button data-profile-model="${x.id}">${kind==='video'?'Als Videomodell verwenden':'Profil anlegen'}</button>`:""}<button type="button" class="secondary" data-delete-model="${x.id}" ${x.used_by_profiles?.length?"disabled":""}>Datei löschen</button></div></div></details>`; const row = x => `<details class="detail-row" data-row-id="${escape(x.id)}" ${openRows.has(x.id)?'open':''}><summary class="detail-summary"><strong class="hub-filename">${escape(x.file)}</strong><span>${escape(x.role_label)}</span><span>${size(x.size)}</span><span>${x.used_by_profiles?.length?`${x.used_by_profiles.length} Profil(e)`:'Nicht zugeordnet'}</span></summary><div class="detail-body"><p class="hub-filename">Quelle: ${escape(x.download_source?.repo||x.repo)}</p>${x.download_source?`<p class="small">Lokal übernommen · Originaldatei: ${escape(x.download_source.file)} · Quellrevision: ${escape(x.download_source.revision)}</p>`:""}<p>${x.profile_eligible?'Modelldatei · über ein Profil verwenden':'Zusatzdatei · wird einem Modellprofil unter Komponenten zugeordnet'}</p><details><summary>Version und Integrität</summary><p class="hub-filename">Revision ${escape(x.revision)}</p><p>${x.upstream_hash_verified ? 'SHA-256 mit Quelle geprüft' : 'SHA-256 lokal erfasst; Größe mit Quelle geprüft'}</p></details>${executionHTML(x.execution)}${x.used_by_profiles?.length?`<p class="warning">Verwendet von Profil: ${x.used_by_profiles.map(p=>escape(p.name)).join(", ")}. Bitte zuerst das Profil löschen.</p>`:""}<div class="card-actions">${x.profile_eligible===true?`<button data-profile-model="${x.id}">${kind==='video'?'Als Videomodell verwenden':'Profil anlegen'}</button>`:""}<button type="button" class="secondary" data-delete-model="${x.id}" ${x.used_by_profiles?.length?"disabled":""}>Datei löschen</button></div></div></details>`;
const known = new Set(groups.map(([role])=>role)); const known = new Set(groups.map(([role])=>role));
const sections = [...groups, ['other','Weitere Dateien · Zuordnung unbekannt']]; const sections = [...groups, ['other','Weitere Dateien · Zuordnung unbekannt']];
+5 -2
View File
@@ -56,12 +56,14 @@ def file_role(filename):
role,label='auxiliary','Sprach-Tokenizer' role,label='auxiliary','Sprach-Tokenizer'
elif any(p in ('text_encoder','text_encoders') for p in parts) or re.search(r'(?:^|[/_-])(?:text.encoder|qwen3vl|t5xxl|clip_l|clip_g)(?:[/_.-]|$)',name): elif any(p in ('text_encoder','text_encoders') for p in parts) or re.search(r'(?:^|[/_-])(?:text.encoder|qwen3vl|t5xxl|clip_l|clip_g)(?:[/_.-]|$)',name):
role,label='text_encoder','Textencoder' role,label='text_encoder','Textencoder'
elif 'vae' in parts or re.search(r'(?:^|[/_-])vae(?:[/_.-]|$)',name): elif PurePosixPath(name).name in ('audiovae.pth','audiovae.safetensors') or 'vae' in parts or re.search(r'(?:^|[/_-])vae(?:[/_.-]|$)',name):
role,label='vae','VAE' role,label='vae','VAE'
elif any(p in ('latent_upscale_models','model_patches','loras') for p in parts): elif any(p in ('latent_upscale_models','model_patches','loras') for p in parts):
role,label='auxiliary','Zusatzkomponente' role,label='auxiliary','Zusatzkomponente'
elif re.search(r'(?:^|[/_-])(?:lora|adapter)(?:[/_.-]|$)',name): elif re.search(r'(?:^|[/_-])(?:lora|adapter)(?:[/_.-]|$)',name):
role,label='auxiliary','Zusatzkomponente' role,label='auxiliary','Zusatzkomponente'
elif 'tokenizer' in PurePosixPath(name).name or name.endswith('.tiktoken'):
role,label='tokenizer','Tokenizer-Datei'
elif name.endswith(('.gguf','.safetensors')): elif name.endswith(('.gguf','.safetensors')):
role,label='model','Modelldatei' role,label='model','Modelldatei'
else: else:
@@ -201,7 +203,8 @@ class Catalog:
files=[] files=[]
for f in data.get('siblings',[]): for f in data.get('siblings',[]):
name=f['rfilename'];p=PurePosixPath(name) name=f['rfilename'];p=PurePosixPath(name)
if p.is_absolute() or '..' in p.parts or p.suffix.lower() not in ('.gguf','.safetensors','.json','.tiktoken'):continue allowed=p.suffix.lower() in ('.gguf','.safetensors','.json','.tiktoken') or (repo=='openbmb/VoxCPM2' and name=='audiovae.pth')
if p.is_absolute() or '..' in p.parts or not allowed:continue
size=f.get('size',f.get('lfs',{}).get('size')) size=f.get('size',f.get('lfs',{}).get('size'))
if not isinstance(size,int) or size<1:continue if not isinstance(size,int) or size<1:continue
files.append(dict(name=name,size=size,sha256=f.get('lfs',{}).get('sha256'))) files.append(dict(name=name,size=size,sha256=f.get('lfs',{}).get('sha256')))
+5 -1
View File
@@ -1,7 +1,7 @@
"""Read-only, conservative mapping to Deck's maintained execution adapters.""" """Read-only, conservative mapping to Deck's maintained execution adapters."""
from catalog import file_role from catalog import file_role
from stt import supported as stt_supported from stt import supported as stt_supported
from profiles import image_recipe, image_adapter, video_recipe from profiles import image_recipe, image_adapter, video_recipe, voice_recipe
from tts_runtime import REPO, REVISION from tts_runtime import REPO, REVISION
def _assess(model, installed, profile=None): def _assess(model, installed, profile=None):
@@ -21,6 +21,8 @@ def _assess(model, installed, profile=None):
elif kind=='audio' and repo==REPO and filename=='model.safetensors' and model.get('revision')==REVISION: elif kind=='audio' and repo==REPO and filename=='model.safetensors' and model.get('revision')==REVISION:
result['family']='Qwen3-TTS CustomVoice' result['family']='Qwen3-TTS CustomVoice'
result.update(runtime='Qwen3-TTS CustomVoice',route='#tts-runtime',components=['Vollständiges Modellpaket und Sprach-Tokenizer (im Einrichtungsassistenten enthalten)']) 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.')
elif kind=='chat' and filename.lower().endswith('.gguf'): 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.') 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')): 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')):
@@ -56,6 +58,8 @@ def assess(model, installed, profile=None):
meta=model.get('runtime_metadata') or {};family=str(meta.get('architecture') or '').lower() meta=model.get('runtime_metadata') or {};family=str(meta.get('architecture') or '').lower()
def add(id,label,key,route,reason,maintained=True): 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)) 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)
if result.get('runtime'): if result.get('runtime'):
add('current',result['runtime'],kind,result['route'],'Vorhandene Deck-Zuordnung') 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'): if kind=='music' and repo=='audio-cpp/yue2-3b-gguf' and name.startswith('yue2-3b-') and name.endswith('.gguf'):
+3 -3
View File
@@ -38,11 +38,11 @@ window.ProfilesUI=(()=>{
try{ try{
const data=await api('profiles/components?'+new URLSearchParams({model_id:profile.model_id,...(extraRepo?{repo:extraRepo}:{})}));if(!root.isConnected||sequence!==panelSequence)return; 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=`<section class="card editor"><div class="card-top"><h2>Komponenten · ${e(profile.name)}</h2><button class="secondary" id="components-close">Schließen</button></div><p>${e(data.message)}</p><details><summary>Andere Komponentenquelle auswählen (ungeprüft)</summary><label>Hugging-Face-Link oder Repository<input id="component-source" placeholder="https://huggingface.co/Organisation/Modell"></label><button id="component-source-open" type="button">Dateien anzeigen</button></details>${data.supported?`<p>Quelle: <a class="link" href="${e(data.source)}" target="_blank" rel="noopener noreferrer">Modellkarte und Lizenz ↗</a> · ${e(data.license||'Lizenz nicht angegeben')}</p><p>Laufzeit: ${e(data.runtime)} · ${data.manual?'Zuordnung ungeprüft':data.runtime_installed?'installiert':'noch nicht eingerichtet'}</p>${!data.manual?'<button type="button" id="components-download-all">Alle fehlenden Dateien herunterladen</button>':'<p>Nur benötigte Dateien einzeln auswählen; diese Liste ist kein automatischer Downloadplan.</p>'}<form id="components-form">${data.requirements.map(r=>`<section class="card component-card"><h3>${e(r.label)}</h3><label>Datei aus der Bibliothek<select name="${r.role}"><option value="">Noch nicht zugeordnet</option>${r.available.map(f=>`<option value="${f.id}" ${(profile.components?.[r.role]===f.id||(!data.manual&&!profile.components?.[r.role]&&r.available.length===1))?'selected':''}>${e(f.file)} · ${(f.size/1024**3).toFixed(2)} GiB</option>`).join('')}</select></label>${r.candidates.map((f,i)=>`<div class="note"><p data-download-feedback="${r.role}:${i}" role="status"></p><p class="hub-filename">${e(f.name)} · ${(f.size/1024**3).toFixed(2)} GiB · Version ${e(f.revision.slice(0,12))}</p>${f.gated&&!f.auth_configured?'<p>Hugging-Face-Zugang unter <a href="#access">Einstellungen → Zugang</a> hinterlegen und Modellfreigabe prüfen.</p>':''}<button type="button" class="secondary" data-component-download="${r.role}:${i}" ${(f.gated&&!f.auth_configured)||r.available.some(x=>x.file===f.name&&x.revision===f.revision)?'disabled':''}>${r.available.some(x=>x.file===f.name&&x.revision===f.revision)?'Bereits in Bibliothek':'Datei herunterladen'}</button></div>`).join('')||'<p>Keine passende Quelldatei verfügbar.</p>'}</section>`).join('')}<div class="card-actions"><button id="components-save">Zuordnung auf Athena speichern</button><button type="button" class="secondary" id="components-refresh">Bibliothek aktualisieren</button></div><p class="small">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.</p></form>`:''}<p id="components-message" role="status"></p></section>`; el('profile-editor').innerHTML=`<section class="card editor"><div class="card-top"><h2>Komponenten · ${e(profile.name)}</h2><button class="secondary" id="components-close">Schließen</button></div><p>${e(data.message)}</p><details><summary>Andere Komponentenquelle auswählen (ungeprüft)</summary><label>Hugging-Face-Link oder Repository<input id="component-source" placeholder="https://huggingface.co/Organisation/Modell"></label><button id="component-source-open" type="button">Dateien anzeigen</button></details>${data.supported?`<p>Quelle: <a class="link" href="${e(data.source)}" target="_blank" rel="noopener noreferrer">Modellkarte und Lizenz ↗</a> · ${e(data.license||'Lizenz nicht angegeben')}</p><p>Laufzeit: ${e(data.runtime)} · ${data.manual?'Zuordnung ungeprüft':data.runtime_installed?'installiert':'noch nicht eingerichtet'}</p>${!data.manual?'<button type="button" id="components-download-all">Alle fehlenden Dateien herunterladen</button>':'<p>Nur benötigte Dateien einzeln auswählen; diese Liste ist kein automatischer Downloadplan.</p>'}<form id="components-form">${data.requirements.map(r=>`<section class="card component-card"><h3>${e(r.label)}</h3><label>Datei aus der Bibliothek<select name="${r.role}"><option value="">Noch nicht zugeordnet</option>${r.available.map(f=>`<option value="${f.id}" ${(profile.components?.[r.role]===f.id||(!data.manual&&!profile.components?.[r.role]&&r.available.length===1))?'selected':''}>${e(f.file)} · ${(f.size/1024**3).toFixed(2)} GiB</option>`).join('')}</select></label>${r.candidates.map((f,i)=>`<div class="note"><p data-download-feedback="${r.role}:${i}" role="status"></p><p class="hub-filename">${e(f.name)} · ${(f.size/1024**3).toFixed(2)} GiB · Version ${e(f.revision.slice(0,12))}</p>${f.gated&&!f.auth_configured?'<p>Hugging-Face-Zugang unter <a href="#access">Einstellungen → Zugang</a> hinterlegen und Modellfreigabe prüfen.</p>':''}<button type="button" class="secondary" data-component-download="${r.role}:${i}" ${(f.gated&&!f.auth_configured)||r.available.some(x=>x.file===f.name&&x.revision===f.revision)?'disabled':''}>${r.available.some(x=>x.file===f.name&&x.revision===f.revision)?'Bereits in Bibliothek':'Datei herunterladen'}</button></div>`).join('')||'<p>Keine passende Quelldatei verfügbar.</p>'}</section>`).join('')}<div class="card-actions"><button id="components-save">Zuordnung auf Athena speichern</button><button type="button" class="secondary" id="components-refresh">Bibliothek aktualisieren</button></div><p class="small">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.</p></form>`:''}<p id="components-message" role="status"></p></section>`;
const missing=data.requirements?.filter(r=>!r.available.length)||[]; 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()=>{ 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; const b=el('components-download-all');b.disabled=true;let count=0;
try{for(const r of missing){const f=r.candidates.find(f=>!f.gated||f.auth_configured);if(!f)throw Error('Keine herunterladbare Datei; HF-Zugang oder Quellversion prüfen für '+r.label);const catalog=await api('catalog');if((catalog.downloads||[]).some(j=>['queued','downloading'].includes(j.state)&&j.repo===f.repo&&j.file===f.name&&j.revision===f.revision))continue;await api('catalog/download',{repo:f.repo,filename:f.name,revision:f.revision,kind:profile.kind});count++;}if(el('components-message'))el('components-message').textContent='Dateien sind im Downloadplan. Nach Abschluss Bibliothek aktualisieren und Zuordnung speichern; passende Dateien werden vorausgewählt.';} try{const plan=missing.map(r=>{const f=r.candidates.find(f=>!f.gated||f.auth_configured);if(!f)throw Error('Downloadplan unvollständig: '+r.label+'. Quelle oder HF-Zugang prüfen. Es wurde noch kein Download gestartet.');return f;});for(const f of plan){const catalog=await api('catalog');if((catalog.downloads||[]).some(j=>['queued','downloading'].includes(j.state)&&j.repo===f.repo&&j.file===f.name&&j.revision===f.revision))continue;await api('catalog/download',{repo:f.repo,filename:f.name,revision:f.revision,kind:profile.kind});count++;}if(el('components-message'))el('components-message').textContent='Dateien sind im Downloadplan. Nach Abschluss Bibliothek aktualisieren und Zuordnung speichern; passende Dateien werden vorausgewählt.';}
catch(error){if(el('components-message'))el('components-message').textContent=error.message;b.disabled=false;} catch(error){if(el('components-message'))el('components-message').textContent=(count?count+' Datei(en) bereits im Downloadplan. ':'')+error.message;b.disabled=false;}
};} };}
el('component-source-open').onclick=()=>{const value=el('component-source').value.trim();if(value)components(profile,value);}; el('component-source-open').onclick=()=>{const value=el('component-source').value.trim();if(value)components(profile,value);};
el('components-close').onclick=()=>el('profile-editor').replaceChildren(); el('components-close').onclick=()=>el('profile-editor').replaceChildren();
+22 -5
View File
@@ -109,8 +109,19 @@ YUE2_COMPONENTS={
def music_recipe(model): def music_recipe(model):
return YUE2_COMPONENTS if model.get('kind')=='music' and model.get('repo')==YUE2_REPO and model.get('file') in ('yue2-3b-bf16.gguf','yue2-3b-q8_0.gguf','yue2-3b-q4_0.gguf') else None return YUE2_COMPONENTS if model.get('kind')=='music' and model.get('repo')==YUE2_REPO and model.get('file') in ('yue2-3b-bf16.gguf','yue2-3b-q8_0.gguf','yue2-3b-q4_0.gguf') else None
VOXCPM2_REPO='openbmb/VoxCPM2'
VOXCPM2_COMPONENTS={
'vae':dict(label='Audio-VAE · erzeugt die Audiodatei',files=['audiovae.pth']),
'model_config':dict(label='Modellkonfiguration',files=['config.json']),
'tokenizer':dict(label='Text-Tokenizer · Wortschatz',files=['tokenizer.json']),
'tokenizer_config':dict(label='Tokenizer-Einstellungen · gehört zum Wortschatz',files=['tokenizer_config.json']),
'special_tokens':dict(label='Spezialtokens · Steuerzeichen des Tokenizers',files=['special_tokens_map.json'])
}
def voice_recipe(model):
return VOXCPM2_COMPONENTS if model.get('kind') in ('voice','audio') and model.get('repo')==VOXCPM2_REPO and model.get('file')=='model.safetensors' else None
def component_recipe(model): def component_recipe(model):
return music_recipe(model) or video_recipe(model) or image_recipe(model) return voice_recipe(model) or music_recipe(model) or video_recipe(model) or image_recipe(model)
MANUAL_ROLES={'manual_text_encoder':'Textencoder (ungeprüft)','manual_vae':'VAE (ungeprüft)','manual_tokenizer':'Tokenizer (ungeprüft)','manual_configuration':'Konfiguration (ungeprüft)','manual_projector':'Projektor (ungeprüft)','manual_auxiliary':'Weitere Komponente (ungeprüft)'} MANUAL_ROLES={'manual_text_encoder':'Textencoder (ungeprüft)','manual_vae':'VAE (ungeprüft)','manual_tokenizer':'Tokenizer (ungeprüft)','manual_configuration':'Konfiguration (ungeprüft)','manual_projector':'Projektor (ungeprüft)','manual_auxiliary':'Weitere Komponente (ungeprüft)'}
@@ -161,8 +172,13 @@ class Profiles:
for role,info in music_recipe(p['model']).items(): for role,info in music_recipe(p['model']).items():
try:self._component(p['model'],role,p.get('components',{}).get(role)) try:self._component(p['model'],role,p.get('components',{}).get(role))
except ValueError:p['blockers'].append(info['label']+' fehlt oder ist noch nicht zugeordnet.') except ValueError:p['blockers'].append(info['label']+' fehlt oder ist noch nicht zugeordnet.')
if p.get('model') and voice_recipe(p['model']):
p['blockers']=['VoxCPM2-Ausführung fehlt: Die VoxCPM-Laufzeit und der Deck-Worker sind noch nicht angebunden.']
for role,info in voice_recipe(p['model']).items():
try:self._component(p['model'],role,p.get('components',{}).get(role))
except ValueError:p['blockers'].append(info['label']+' fehlt oder ist noch nicht zugeordnet.')
if p['kind']=='video' and self.video_blockers and p.get('model'):p['blockers']=self.video_blockers(p) if p['kind']=='video' and self.video_blockers and p.get('model'):p['blockers']=self.video_blockers(p)
if p['kind']=='audio' and self.tts_blockers and p.get('model'):p['blockers']=self.tts_blockers(p) if p['kind']=='audio' and self.tts_blockers and p.get('model') and not voice_recipe(p['model']):p['blockers']=self.tts_blockers(p)
if p['kind']=='music' and getattr(self,'music_blockers',None) and p.get('model'):p['blockers']=self.music_blockers(p) if p['kind']=='music' and getattr(self,'music_blockers',None) and p.get('model'):p['blockers']=self.music_blockers(p)
if p['kind']=='stt' and self.stt_blockers and p.get('model'):p['blockers']=self.stt_blockers(p) if p['kind']=='stt' and self.stt_blockers and p.get('model'):p['blockers']=self.stt_blockers(p)
if p['kind']=='chat' and self.chat_blockers and p.get('model'):p['blockers']=self.chat_blockers(p) if p['kind']=='chat' and self.chat_blockers and p.get('model'):p['blockers']=self.chat_blockers(p)
@@ -247,12 +263,12 @@ class Profiles:
if not recipe or role not in recipe:raise ValueError('Für dieses Modell ist kein geprüftes Komponentenrezept hinterlegt.') if not recipe or role not in recipe:raise ValueError('Für dieses Modell ist kein geprüftes Komponentenrezept hinterlegt.')
item=self.catalog.entry(ident) item=self.catalog.entry(ident)
if item['repo']!=recipe[role].get('repo',model['repo']) or item['kind']!=model['kind'] or item['file'] not in recipe[role]['files']:raise ValueError('Die Datei gehört nicht zu dieser Komponente des Modells.') if item['repo']!=recipe[role].get('repo',model['repo']) or item['kind']!=model['kind'] or item['file'] not in recipe[role]['files']:raise ValueError('Die Datei gehört nicht zu dieser Komponente des Modells.')
if (video_recipe(model) or music_recipe(model)) and item['revision']!=model['revision']:raise ValueError('Komponente muss dieselbe Quellversion wie das Hauptmodell verwenden.') if (video_recipe(model) or music_recipe(model) or voice_recipe(model)) and item['revision']!=model['revision']:raise ValueError('Komponente muss dieselbe Quellversion wie das Hauptmodell verwenden.')
return item return item
def components(self,model_id,extra_repo=None): def components(self,model_id,extra_repo=None):
model=self.catalog.entry(model_id) model=self.catalog.entry(model_id)
if extra_repo or not self._recipe(model):return self.manual_components(model,extra_repo) if extra_repo or not self._recipe(model):return self.manual_components(model,extra_repo)
recipe=self._recipe(model);video=bool(video_recipe(model));source=self.catalog.files(model['repo'],model['revision']) if video or music_recipe(model) else self.catalog.files(next(iter(recipe.values())).get('repo',model['repo']));entries=self.catalog.status()['entries'];requirements=[] recipe=self._recipe(model);video=bool(video_recipe(model));source=self.catalog.files(model['repo'],model['revision']) if video or music_recipe(model) or voice_recipe(model) else self.catalog.files(next(iter(recipe.values())).get('repo',model['repo']));entries=self.catalog.status()['entries'];requirements=[]
for role,info in recipe.items(): for role,info in recipe.items():
available=[] available=[]
for item in entries: for item in entries:
@@ -261,6 +277,7 @@ class Profiles:
except ValueError:pass 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] 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)) 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 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 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 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.') 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.')
@@ -270,7 +287,7 @@ class Profiles:
source=self.catalog.files(repo or model['repo'],None if repo else model['revision']) source=self.catalog.files(repo or model['repo'],None if repo else model['revision'])
entries=self.catalog.status()['entries'];requirements=[] entries=self.catalog.status()['entries'];requirements=[]
for role,label in MANUAL_ROLES.items(): for role,label in MANUAL_ROLES.items():
match={'manual_text_encoder':('text_encoder',),'manual_vae':('vae',),'manual_projector':('vision_projector','audio_projector'),'manual_configuration':('configuration',),'manual_auxiliary':('auxiliary',),'manual_tokenizer':('configuration','auxiliary')}[role] match={'manual_text_encoder':('text_encoder',),'manual_vae':('vae',),'manual_projector':('vision_projector','audio_projector'),'manual_configuration':('configuration',),'manual_auxiliary':('auxiliary',),'manual_tokenizer':('tokenizer','configuration','auxiliary')}[role]
def matches(name):return name.endswith('.tiktoken') or 'tokenizer' in name.lower() if role=='manual_tokenizer' else file_role(name)['role'] in match def matches(name):return name.endswith('.tiktoken') or 'tokenizer' in name.lower() if role=='manual_tokenizer' else file_role(name)['role'] in match
candidates=[dict(f,repo=source['repo'],revision=source['revision'],gated=source['gated'],auth_configured=source.get('auth_configured',False)) for f in source['files'] if f['name']!=model['file'] and matches(f['name'])] candidates=[dict(f,repo=source['repo'],revision=source['revision'],gated=source['gated'],auth_configured=source.get('auth_configured',False)) for f in source['files'] if f['name']!=model['file'] and matches(f['name'])]
available=[x for x in entries if x['kind']==model['kind'] and x['id']!=model['id'] and matches(x['file'])] available=[x for x in entries if x['kind']==model['kind'] and x['id']!=model['id'] and matches(x['file'])]
+34
View File
@@ -0,0 +1,34 @@
import tempfile,unittest
from pathlib import Path
from unittest.mock import Mock,patch
from catalog import Catalog,file_role
from profiles import Profiles,voice_recipe,VOXCPM2_COMPONENTS
from execution_setup import assess
class VoxCPMComponentsTests(unittest.TestCase):
def test_recipe_uses_existing_files_and_requires_matching_revision(self):
model=dict(id='main',kind='voice',repo='openbmb/VoxCPM2',file='model.safetensors',revision='a'*40,size=1)
entries=[dict(id=role,kind='voice',repo=model['repo'],file=info['files'][0],revision=model['revision'],size=1) for role,info in VOXCPM2_COMPONENTS.items()]
c=Mock();lookup={x['id']:x for x in [model]+entries};c.entry.side_effect=lambda id:lookup[id]
c.status.return_value=dict(entries=entries);c.files.return_value=dict(repo=model['repo'],revision=model['revision'],files=[dict(name=x['file'],size=1) for x in entries],gated=False,license='apache-2.0')
with tempfile.TemporaryDirectory() as d:
p=Profiles(Path(d)/'profiles.json',c);p.rows=[dict(id='p',revision=1,name='Vox',kind='voice',model_id='main',parameters={})]
data=p.components('main');c.files.assert_called_once_with(model['repo'],model['revision'])
self.assertEqual(len(data['requirements']),5);self.assertFalse(data['worker_available']);self.assertTrue(all(len(x['available'])==1 for x in data['requirements']))
p.assign(dict(id='p',revision=1,components={x['id']:x['id'] for x in entries}))
status=p.status()['profiles'][0];self.assertFalse(status['runnable']);self.assertEqual(len(status['blockers']),1);self.assertIn('VoxCPM',status['blockers'][0])
entries[0]['revision']='b'*40
with self.assertRaises(ValueError):p._component(model,'vae','vae')
def test_not_applied_to_arbitrary_voices_or_finetunes(self):
for repo,file in [('other/VoxCPM2','model.safetensors'),('openbmb/VoxCPM2','other.safetensors')]:self.assertIsNone(voice_recipe(dict(kind='voice',repo=repo,file=file)))
def test_roles_do_not_offer_auxiliary_files_as_main_models(self):
self.assertEqual(file_role('audiovae.pth')['role'],'vae')
for name in ('tokenizer.json','tokenizer_config.json'):
self.assertEqual(file_role(name)['role'],'tokenizer');self.assertFalse(file_role(name)['profile_eligible'])
def test_catalog_only_allows_reviewed_pth_source(self):
metadata=dict(sha='a'*40,siblings=[dict(rfilename=name,size=10) for name in ['model.safetensors','audiovae.pth','arbitrary.pth','untrusted.py']],cardData={})
with tempfile.TemporaryDirectory() as d,patch('catalog.metadata',return_value=metadata):
c=Catalog(d);self.assertEqual([f['name'] for f in c.files('openbmb/VoxCPM2')['files']],['model.safetensors','audiovae.pth'])
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'])