Fix VoxCPM2 component recipe and clarify missing execution adapter
This commit is contained in:
1 parent
3cd9fa5750
commit
1f0fc42344
7 files changed
+102
-12
No files matched your search
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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'])]
|
||||||
|
|||||||
@@ -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'])
|
||||||
Reference in new issue
Block a user