Separate audio discovery into TTS STT music and voice categories

This commit is contained in:
Mikei386 committed 2026-09-28 23:28:33 +02:00
1 parent 9fecb30a8f
commit 52b1116bdb
9 files changed
+21 -7

No files matched your search

+6
View File
@@ -217,3 +217,9 @@ Bildprofil anlegen → Komponenten → Alle fehlenden Dateien herunterladen →
Neue FLUX-Profile starten mit 4 Schritten und Guidance 1. Bestehende Profile behalten ihre Werte. Pipeline: ComfyUI UNETLoader, Qwen3-FLUX2-Textencoder auf zweiter GPU, EmptyFlux2LatentImage, Flux2Scheduler und Euler, VAE. Der bestehende Low-VRAM-Modus lagert Teile des 16,9-GiB-Modells in den System-RAM aus; es wird nicht vollständige GPU-Belegung versprochen. Beide GPUs müssen frei sein; fremde Prozesse werden nicht gestoppt. Zunächst Text-zu-Bild bis 1024×1024; kein Image-to-Image-Editor für diese Integration. Neue FLUX-Profile starten mit 4 Schritten und Guidance 1. Bestehende Profile behalten ihre Werte. Pipeline: ComfyUI UNETLoader, Qwen3-FLUX2-Textencoder auf zweiter GPU, EmptyFlux2LatentImage, Flux2Scheduler und Euler, VAE. Der bestehende Low-VRAM-Modus lagert Teile des 16,9-GiB-Modells in den System-RAM aus; es wird nicht vollständige GPU-Belegung versprochen. Beide GPUs müssen frei sein; fremde Prozesse werden nicht gestoppt. Zunächst Text-zu-Bild bis 1024×1024; kein Image-to-Image-Editor für diese Integration.
Verifikation auf Athena: eigene ungespeicherte Testprofilkopie, 512×512, 4 Schritte, Guidance 1, Seed 123, neutrales Fahrradmotiv. Vollständiger Job circa 43 Sekunden einschließlich Start und Entladen. PNG visuell geprüft. 150 automatisierte Tests erfolgreich. Gespeicherte Benutzerprofile und produktiver Router nicht verändert. Verifikation auf Athena: eigene ungespeicherte Testprofilkopie, 512×512, 4 Schritte, Guidance 1, Seed 123, neutrales Fahrradmotiv. Vollständiger Job circa 43 Sekunden einschließlich Start und Entladen. PNG visuell geprüft. 150 automatisierte Tests erfolgreich. Gespeicherte Benutzerprofile und produktiver Router nicht verändert.
### Audio-Bereiche
Audio ist in Text-to-Speech (`audio`, bestehende Dateien und Links bleiben erhalten), Spracherkennung (`stt`), Musik (`music`) und Stimme / Voice (`voice`) aufgeteilt. Jede Kategorie hat eigene Suchergebnisse, Bibliothek, Downloads und vorbereitende Profile. Hub-Filter: text-to-speech, automatic-speech-recognition, text-to-audio, audio-to-audio. Musik umfasst gemäß Hub-Task auch Geräuscherzeugung; Voice umfasst auch Audiotrennung. Direkte Repository-Links umgehen wie bisher den Suchfilter.
Neue Downloads behalten die gewählte Kategorie. Alte Audio-Dateien bleiben bei TTS; es erfolgt keine ungesicherte automatische Umklassifizierung. Noch keine Audio-Laufzeit oder Endpunktfreigabe; die neuen Profile speichern zunächst nur Modell und Namen. Eigenständige Studio-Oberflächen bleiben Weitere Dienste.
+1 -1
View File
@@ -8,7 +8,7 @@ const metric=(title,value)=>`<div><small>${title}</small><strong>${value}</stron
const bar=v=>v==null?'':`<progress max="100" value="${Number(v)}" aria-label="Auslastung"></progress>`; const bar=v=>v==null?'':`<progress max="100" value="${Number(v)}" aria-label="Auslastung"></progress>`;
const placeholders={chat:['Sprachmodelle / Chat','Modellprofile, Laufzeit und Chat','Hier werden später Modellprofile und native llama.cpp-Prozesse verwaltet.'],image:['Bildgenerierung','Worker und Aufträge','Hier entsteht später die Übersicht für Bild-Worker und deren Zustand.'],audio:['Audio','Spracheingabe und Sprachausgabe','Hier werden später getrennte Worker für Erkennung und Ausgabe eingebunden.'],services:['Weitere Dienste','Erweiterbare Dienste','Hier finden später zusätzliche Worker und ihre Schnittstellen Platz.']}; const placeholders={chat:['Sprachmodelle / Chat','Modellprofile, Laufzeit und Chat','Hier werden später Modellprofile und native llama.cpp-Prozesse verwaltet.'],image:['Bildgenerierung','Worker und Aufträge','Hier entsteht später die Übersicht für Bild-Worker und deren Zustand.'],audio:['Audio','Spracheingabe und Sprachausgabe','Hier werden später getrennte Worker für Erkennung und Ausgabe eingebunden.'],services:['Weitere Dienste','Erweiterbare Dienste','Hier finden später zusätzliche Worker und ihre Schnittstellen Platz.']};
async function refresh(){if(refreshing)return;refreshing=true;const page=location.hash.slice(1)||'home';document.querySelectorAll('nav a').forEach(a=>a.classList.toggle('active',a.hash==='#'+page));try{error.textContent='';let html; async function refresh(){if(refreshing)return;refreshing=true;const page=location.hash.slice(1)||'home';document.querySelectorAll('nav a').forEach(a=>a.classList.toggle('active',a.hash==='#'+page));try{error.textContent='';let html;
if(['chat','image','audio','video','runtime','runtimes','image-runtime','docker-runtime','services'].includes(page)){Studio.render(page);return;} if(['chat','image','audio','stt','music','voice','video','runtime','runtimes','image-runtime','docker-runtime','services'].includes(page)){Studio.render(page);return;}
if(page==='access'){await accessPage();return;} if(page==='access'){await accessPage();return;}
if(page==='network'){await networkPage();return;} if(page==='network'){await networkPage();return;}
if(page==='home'||page==='endpoint'){await EndpointUI.render(page==='endpoint');return; if(page==='home'||page==='endpoint'){await EndpointUI.render(page==='endpoint');return;
+1 -1
View File
@@ -45,7 +45,7 @@ window.CatalogUI = (() => {
} }
async function search(q) { async function search(q) {
const sequence = ++searchSequence; message('Modelle werden gesucht …'); const sequence = ++searchSequence; message('Modelle werden gesucht …');
try { const result = await api('/search?'+new URLSearchParams({q,kind,sort:el('hub-sort').value,base_only:String(el('hub-base-only').checked)})); if (!root.isConnected || sequence !== searchSequence) return; models = result.models; renderModels(); enrich(sequence); message(result.notice || (result.base_only ? `Basismodellfilter aktiv · ${result.scanned} Hub-Treffer geprüft, höchstens 20 angezeigt. Bei fehlenden Treffern Suche eingrenzen oder Filter ausschalten.` : kind === 'audio' ? 'Audio-Suche: derzeit Text-to-Speech-Modelle.' : el('hub-sort').value==='newest'?'Neueste Hub-Repositories zuerst (Erstellungsdatum, nicht letztes Update).':'Wähle ein Modell für die Dateiauswahl. Name A–Z sortiert die angezeigten Treffer.')); } try { const result = await api('/search?'+new URLSearchParams({q,kind,sort:el('hub-sort').value,base_only:String(el('hub-base-only').checked)})); if (!root.isConnected || sequence !== searchSequence) return; models = result.models; renderModels(); enrich(sequence); message(result.notice || (result.base_only ? `Basismodellfilter aktiv · ${result.scanned} Hub-Treffer geprüft, höchstens 20 angezeigt. Bei fehlenden Treffern Suche eingrenzen oder Filter ausschalten.` : kind === 'audio' ? 'Text-to-Speech: Modelle für Sprachausgabe.' : el('hub-sort').value==='newest'?'Neueste Hub-Repositories zuerst (Erstellungsdatum, nicht letztes Update).':'Wähle ein Modell für die Dateiauswahl. Name A–Z sortiert die angezeigten Treffer.')); }
catch(error) { if (sequence === searchSequence) message(error.message); } catch(error) { if (sequence === searchSequence) message(error.message); }
} }
async function enrich(sequence){ async function enrich(sequence){
+1 -1
View File
@@ -11,7 +11,7 @@ import uuid
import urllib.parse import urllib.parse
import urllib.request import urllib.request
KINDS={'chat':'text-generation','image':'text-to-image','audio':'text-to-speech','video':'text-to-video'} KINDS={'chat':'text-generation','image':'text-to-image','audio':'text-to-speech','stt':'automatic-speech-recognition','music':'text-to-audio','voice':'audio-to-audio','video':'text-to-video'}
def safe_url(url): def safe_url(url):
p=urllib.parse.urlsplit(url) p=urllib.parse.urlsplit(url)
+1 -1
View File
@@ -1 +1 @@
<!doctype html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Athena Deck</title><link rel="stylesheet" href="/style.css"><script src="/network-ui.js" defer></script><script src="/access-ui.js" defer></script><script src="/catalog-ui.js" defer></script><script src="/runtime-ui.js" defer></script><script src="/profiles-ui.js" defer></script><script src="/auto-test-ui.js" defer></script><script src="/chat-test-ui.js" defer></script><script src="/image-test-ui.js" defer></script><script src="/docker-ui.js" defer></script><script src="/studio.js" defer></script><script src="/endpoint-ui.js" defer></script><script src="/app.js" defer></script></head><body><aside><a class="brand" href="#home"><span>Α</span> ATHENA <b>DECK</b></a><p class="eyebrow">WORKSPACE / ATHENA</p><nav aria-label="Hauptnavigation"><a href="#home">Übersicht <span>01</span></a><a href="#chat">Sprachmodelle / Chat <span>02</span></a><a href="#image">Bildgenerierung <span>03</span></a><a href="#audio">Audio <span>04</span></a><a href="#video">Video <span>05</span></a><a href="#services">Weitere Dienste <span>06</span></a><a href="#hardware">Hardware <span>07</span></a><p class="nav-heading">EINSTELLUNGEN</p><a href="#runtimes">Laufzeiten <span>↗</span></a><a class="nav-sub" href="#runtime">llama.cpp</a><a class="nav-sub" href="#image-runtime">Bildgenerierung</a><a class="nav-sub" href="#docker-runtime">Docker</a><a href="#endpoint">Endpunkt &amp; Profile <span>↗</span></a><a href="#network">Netzwerk <span>↗</span></a><a href="#access">Zugang &amp; API <span>↗</span></a></nav><footer><i></i> Isolierte Entwicklungsumgebung<p>Eigener Modell-Router<br>Hardware · nur lesend</p></footer></aside><main><header><span>ATHENA CONTROL SURFACE</span><span class="pill">v0.7 · Router</span></header><div id="view"></div><p id="error" role="alert"></p></main></body></html> <!doctype html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Athena Deck</title><link rel="stylesheet" href="/style.css"><script src="/network-ui.js" defer></script><script src="/access-ui.js" defer></script><script src="/catalog-ui.js" defer></script><script src="/runtime-ui.js" defer></script><script src="/profiles-ui.js" defer></script><script src="/auto-test-ui.js" defer></script><script src="/chat-test-ui.js" defer></script><script src="/image-test-ui.js" defer></script><script src="/docker-ui.js" defer></script><script src="/studio.js" defer></script><script src="/endpoint-ui.js" defer></script><script src="/app.js" defer></script></head><body><aside><a class="brand" href="#home"><span>Α</span> ATHENA <b>DECK</b></a><p class="eyebrow">WORKSPACE / ATHENA</p><nav aria-label="Hauptnavigation"><a href="#home">Übersicht <span>01</span></a><a href="#chat">Sprachmodelle / Chat <span>02</span></a><a href="#image">Bildgenerierung <span>03</span></a><p class="nav-heading">AUDIO</p><a class="nav-sub" href="#audio">Text-to-Speech (TTS)</a><a class="nav-sub" href="#stt">Spracherkennung (STT)</a><a class="nav-sub" href="#music">Musik</a><a class="nav-sub" href="#voice">Stimme / Voice</a><a href="#video">Video <span>05</span></a><a href="#services">Weitere Dienste <span>06</span></a><a href="#hardware">Hardware <span>07</span></a><p class="nav-heading">EINSTELLUNGEN</p><a href="#runtimes">Laufzeiten <span>↗</span></a><a class="nav-sub" href="#runtime">llama.cpp</a><a class="nav-sub" href="#image-runtime">Bildgenerierung</a><a class="nav-sub" href="#docker-runtime">Docker</a><a href="#endpoint">Endpunkt &amp; Profile <span>↗</span></a><a href="#network">Netzwerk <span>↗</span></a><a href="#access">Zugang &amp; API <span>↗</span></a></nav><footer><i></i> Isolierte Entwicklungsumgebung<p>Eigener Modell-Router<br>Hardware · nur lesend</p></footer></aside><main><header><span>ATHENA CONTROL SURFACE</span><span class="pill">v0.7 · Router</span></header><div id="view"></div><p id="error" role="alert"></p></main></body></html>
+1 -1
View File
@@ -8,7 +8,7 @@ window.ProfilesUI=(()=>{
const message=t=>{if(root.isConnected)el('profile-message').textContent=t;}; const message=t=>{if(root.isConnected)el('profile-message').textContent=t;};
async function load(){try{const [p,c,h,ep]=await Promise.all([api('profiles'),api('catalog'),kind==='chat'?api('hardware').catch(()=>({gpus:[]})):Promise.resolve({gpus:[]}),api('endpoint')]);projectors=c.entries.filter(x=>x.role==='vision_projector'&&x.file.endsWith('.gguf'));enabled=ep.profiles.filter(x=>x.enabled).map(x=>x.id);gpus=h.gpus||[];if(!root.isConnected)return;rows=p.profiles.filter(x=>x.kind===kind);models=c.entries.filter(x=>x.kind===kind&&x.profile_eligible===true);schema=p.schemas[kind];el('profile-new').disabled=!models.length; async function load(){try{const [p,c,h,ep]=await Promise.all([api('profiles'),api('catalog'),kind==='chat'?api('hardware').catch(()=>({gpus:[]})):Promise.resolve({gpus:[]}),api('endpoint')]);projectors=c.entries.filter(x=>x.role==='vision_projector'&&x.file.endsWith('.gguf'));enabled=ep.profiles.filter(x=>x.enabled).map(x=>x.id);gpus=h.gpus||[];if(!root.isConnected)return;rows=p.profiles.filter(x=>x.kind===kind);models=c.entries.filter(x=>x.kind===kind&&x.profile_eligible===true);schema=p.schemas[kind];el('profile-new').disabled=!models.length;
el('profile-list').innerHTML=rows.map(p=>`<article class="card"><span class="pill">GESPEICHERT AUF ATHENA</span><h3>${e(p.name)}</h3><p class="hub-filename">${e(p.model?.file||'Modelldatei nicht verfügbar')}</p><p>${Object.entries(p.parameters).map(([k,v])=>`${e(labels[k]||k)}: ${e(k==='gpu_reserve_mode'?({auto:'Automatisch',manual:'Manuell',none:'Keine zusätzliche Reserve'}[v]||v):k==='gpu_reserve_mib'?(Object.entries(v).map(([id,n])=>`${gpus.find(g=>g.uuid===id)?.name||id}: ${n} MiB`).join(', ')||'gemäß Reservemodus'):k==='gpu_devices'?(v.length?v.map((id,i)=>`CUDA${i}: ${gpus.find(g=>g.uuid===id)?.name||id}`).join(', '):'Automatisch'):Array.isArray(v)?(v.join(', ')||'Automatisch'):v)}`).join(' · ')}</p><details><summary>${p.runnable?'Laufzeit bereit · lädt bei Anfrage':'Was fehlt zur Ausführung?'}</summary>${p.blockers.map(t=>`<p>${e(t)}</p>`).join('')}</details><div class="card-actions"><label class="endpoint-choice"><input type="checkbox" data-publish="${p.id}" ${enabled.includes(p.id)?'checked':''} ${!p.runnable&&!enabled.includes(p.id)?'disabled':''}> Am API-Endpunkt freigegeben</label><button class="secondary" data-edit="${p.id}">Bearbeiten</button><button class="secondary" data-copy="${p.id}">Duplizieren</button><button class="secondary" data-delete="${p.id}">Löschen</button>${kind==='image'?`<button class="secondary" data-components="${p.id}">Komponenten</button>`:''}</div></article>`).join('')||`<section class="empty"><h3>Noch keine Profile</h3><p>${models.length?'Wähle eine heruntergeladene Modelldatei und lege dein erstes Profil an.':'Lade zuerst eine Gewichtsdatei unter Entdecken herunter.'}</p></section>`; el('profile-list').innerHTML=rows.map(p=>`<article class="card"><span class="pill">GESPEICHERT AUF ATHENA</span><h3>${e(p.name)}</h3><p class="hub-filename">${e(p.model?.file||'Modelldatei nicht verfügbar')}</p><p>${Object.entries(p.parameters).map(([k,v])=>`${e(labels[k]||k)}: ${e(k==='gpu_reserve_mode'?({auto:'Automatisch',manual:'Manuell',none:'Keine zusätzliche Reserve'}[v]||v):k==='gpu_reserve_mib'?(Object.entries(v).map(([id,n])=>`${gpus.find(g=>g.uuid===id)?.name||id}: ${n} MiB`).join(', ')||'gemäß Reservemodus'):k==='gpu_devices'?(v.length?v.map((id,i)=>`CUDA${i}: ${gpus.find(g=>g.uuid===id)?.name||id}`).join(', '):'Automatisch'):Array.isArray(v)?(v.join(', ')||'Automatisch'):v)}`).join(' · ')}</p><details><summary>${p.runnable?'Laufzeit bereit · lädt bei Anfrage':'Was fehlt zur Ausführung?'}</summary>${p.blockers.map(t=>`<p>${e(t)}</p>`).join('')}</details><div class="card-actions"><label class="endpoint-choice"><input type="checkbox" data-publish="${p.id}" ${enabled.includes(p.id)?'checked':''} ${!p.runnable&&!enabled.includes(p.id)?'disabled':''}> Am API-Endpunkt freigegeben</label><button class="secondary" data-edit="${p.id}">Bearbeiten</button><button class="secondary" data-copy="${p.id}">Duplizieren</button><button class="secondary" data-delete="${p.id}">Löschen</button>${kind==='image'?`<button class="secondary" data-components="${p.id}">Komponenten</button>`:''}</div></article>`).join('')||`<section class="empty"><h3>Noch keine Profile</h3><p>${models.length?'Wähle eine heruntergeladene Modelldatei und lege dein erstes Profil an.':'Lade zuerst eine Gewichtsdatei unter Entdecken herunter.'}</p></section>`;
el('profile-list').insertAdjacentHTML('afterbegin',`<p>${kind==='image'?'Höchstens ein Bildprofil ist am API-Endpunkt freigegeben. Ein neuer Haken ersetzt die bisherige Auswahl.':'Mehrere Sprachmodellprofile können gleichzeitig am API-Endpunkt freigegeben sein.'} Freigegeben bedeutet nicht bereits in den GPU-Speicher geladen.</p>`); el('profile-list').insertAdjacentHTML('afterbegin',`<p>${kind==='image'?'Höchstens ein Bildprofil ist am API-Endpunkt freigegeben. Ein neuer Haken ersetzt die bisherige Auswahl.':kind==='chat'?'Mehrere Sprachmodellprofile können gleichzeitig am API-Endpunkt freigegeben sein.':'Für diesen Bereich ist noch keine ausführbare Laufzeit am Endpunkt angebunden.'} Freigegeben bedeutet nicht bereits in den GPU-Speicher geladen.</p>`);
root.querySelectorAll('[data-publish]').forEach(b=>b.onchange=async()=>{const checked=b.checked;root.querySelectorAll('[data-publish]').forEach(x=>x.disabled=true);try{await api('endpoint/profile',{id:b.dataset.publish,enabled:checked});await load();message('Endpunkt-Freigabe gespeichert. Geladen wird erst bei einer API-Anfrage.');}catch(error){await load();message(error.message);}}); root.querySelectorAll('[data-publish]').forEach(b=>b.onchange=async()=>{const checked=b.checked;root.querySelectorAll('[data-publish]').forEach(x=>x.disabled=true);try{await api('endpoint/profile',{id:b.dataset.publish,enabled:checked});await load();message('Endpunkt-Freigabe gespeichert. Geladen wird erst bei einer API-Anfrage.');}catch(error){await load();message(error.message);}});
root.querySelectorAll('[data-delete]').forEach(b=>b.onclick=async()=>{const p=rows.find(p=>p.id===b.dataset.delete);if(!confirm(`Profil „${p.name}“ löschen? Modelldateien und Komponenten bleiben in der Bibliothek.`))return;b.disabled=true;try{await api('profiles/delete',{id:p.id,revision:p.revision});panelSequence++;el('profile-editor').replaceChildren();await load();message('Profil gelöscht. Modelldateien und Komponenten bleiben erhalten.');}catch(error){message(error.message);b.disabled=false;}}); root.querySelectorAll('[data-delete]').forEach(b=>b.onclick=async()=>{const p=rows.find(p=>p.id===b.dataset.delete);if(!confirm(`Profil „${p.name}“ löschen? Modelldateien und Komponenten bleiben in der Bibliothek.`))return;b.disabled=true;try{await api('profiles/delete',{id:p.id,revision:p.revision});panelSequence++;el('profile-editor').replaceChildren();await load();message('Profil gelöscht. Modelldateien und Komponenten bleiben erhalten.');}catch(error){message(error.message);b.disabled=false;}});
root.querySelectorAll('[data-components]').forEach(b=>b.onclick=()=>components(rows.find(p=>p.id===b.dataset.components))); root.querySelectorAll('[data-components]').forEach(b=>b.onclick=()=>components(rows.find(p=>p.id===b.dataset.components)));
+1
View File
@@ -10,6 +10,7 @@ SCHEMAS={
'chat':{'context':(512,2097152,8192),'slots':(1,16,1),'threads':(1,256,6),'batch':(1,8192,512),'ubatch':(1,8192,128),'temperature':(0,5,.8),'top_p':(0,1,.95),'top_k':(0,1000,40),'mtp_tokens':(1,8,2),'mtp_min_p':(0,1,.05)}, 'chat':{'context':(512,2097152,8192),'slots':(1,16,1),'threads':(1,256,6),'batch':(1,8192,512),'ubatch':(1,8192,128),'temperature':(0,5,.8),'top_p':(0,1,.95),'top_k':(0,1000,40),'mtp_tokens':(1,8,2),'mtp_min_p':(0,1,.05)},
'image':{'width':(256,2048,1024),'height':(256,2048,1024),'steps':(1,100,25),'seed':(-1,2147483647,-1),'guidance':(0,30,1)}, 'image':{'width':(256,2048,1024),'height':(256,2048,1024),'steps':(1,100,25),'seed':(-1,2147483647,-1),'guidance':(0,30,1)},
'audio':{'speed':(.25,4,1)}, 'audio':{'speed':(.25,4,1)},
'stt':{},'music':{},'voice':{},
'video':{'width':(256,1920,768),'height':(256,1088,512),'frames':(1,241,33),'fps':(1,60,24),'steps':(1,100,20),'seed':(-1,2147483647,-1)} 'video':{'width':(256,1920,768),'height':(256,1088,512),'frames':(1,241,33),'fps':(1,60,24),'steps':(1,100,20),'seed':(-1,2147483647,-1)}
} }
CHAT_GPU_DEFAULTS={'gpu_devices':[], 'split_mode':'none', 'tensor_split':[], 'mtp':False,'gpu_offload':'auto','gpu_reserve_mode':'auto','gpu_reserve_mib':{},'vision_projector':None,'vision_device':'cpu'} CHAT_GPU_DEFAULTS={'gpu_devices':[], 'split_mode':'none', 'tensor_split':[], 'mtp':False,'gpu_offload':'auto','gpu_reserve_mode':'auto','gpu_reserve_mib':{},'vision_projector':None,'vision_device':'cpu'}
+3 -2
View File
@@ -1,12 +1,13 @@
/* Server-backed model management. Legacy browser drafts remain untouched. */ /* Server-backed model management. Legacy browser drafts remain untouched. */
const Studio=(()=>{ const Studio=(()=>{
let section='discover',category=''; let section='discover',category='';
const labels={chat:'Sprachmodelle / Chat',image:'Bildgenerierung',audio:'Audio',video:'Video'}; const labels={chat:'Sprachmodelle / Chat',image:'Bildgenerierung',audio:'Text-to-Speech (TTS)',stt:'Spracherkennung (STT)',music:'Musik',voice:'Stimme / Voice',video:'Video'};
const audioDescriptions={audio:'Geschriebenen Text vorlesen lassen. Die Suche zeigt Text-to-Speech-Modelle.',stt:'Gesprochene Sprache in Text umwandeln. Die Suche zeigt Spracherkennungsmodelle.',music:'Musik und Klänge aus Text erzeugen. Die Hub-Kategorie Text-to-Audio umfasst neben Musik auch Geräusche und andere Audioerzeugung.',voice:'Vorhandenes Audio bearbeiten: Stimmumwandlung, Audiotrennung und weitere Audio-to-Audio-Modelle. Eigenständige Studio-Oberflächen gehören unter Weitere Dienste.'};
function render(page,force=false,modelId=null){ function render(page,force=false,modelId=null){
if(!force&&document.querySelector('#studio')?.dataset.page===page)return; if(!force&&document.querySelector('#studio')?.dataset.page===page)return;
if(category!==page){category=page;section='discover';} if(category!==page){category=page;section='discover';}
if(modelId)section='profiles'; if(modelId)section='profiles';
const body=['docker-runtime','services'].includes(page)?DockerUI.html(page==='services'):page==='runtimes'?runtimeOverview():page==='image-runtime'?imageRuntime():page==='runtime'?RuntimeUI.html():`<div class="kicker">ATHENA / MODELLVERWALTUNG</div><h1>${labels[page]}</h1><p>Modelle entdecken, herunterladen und mit gespeicherten Profilen konfigurieren.</p><div class="studio-tabs" role="tablist" aria-label="Modellverwaltung">${[['discover','Entdecken'],['library','Bibliothek'],['downloads','Downloads'],['profiles','Profile'],...(['image','chat'].includes(page)?[['test','Testen'],...(page==='chat'?[['auto','Auto-Test']]:[])]:[]),['running','Laufend']].map(([id,label])=>`<button role="tab" aria-selected="${section===id}" class="${section===id?'chosen':'secondary'}" data-section="${id}">${label}</button>`).join('')}</div><div role="tabpanel">${section==='auto'?AutoTestUI.html():section==='test'?(page==='chat'?ChatTestUI.html():ImageTestUI.html()):section==='running'&&page==='image'?ImageTestUI.html(true):section==='profiles'?ProfilesUI.html():section==='running'&&page==='chat'?EndpointUI.runningHTML():section==='running'?'<section class="card"><h2>Keine von Deck gestarteten Modelle</h2><p>Profile werden auf Athena gespeichert. Für diesen Bereich ist noch kein Modellworker angebunden. Fehlende Komponenten stehen beim jeweiligen Profil.</p><a class="link" href="#runtime">llama.cpp-Builds verwalten →</a></section>':CatalogUI.html(section==='library',section==='downloads')}</div>`; const body=['docker-runtime','services'].includes(page)?DockerUI.html(page==='services'):page==='runtimes'?runtimeOverview():page==='image-runtime'?imageRuntime():page==='runtime'?RuntimeUI.html():`<div class="kicker">ATHENA / MODELLVERWALTUNG</div><h1>${labels[page]}</h1><p>${audioDescriptions[page]||'Modelle entdecken, herunterladen und mit gespeicherten Profilen konfigurieren.'}</p>${audioDescriptions[page]?'<p class="note">Entdecken und Downloads sind verfügbar. Für diese Audio-Bereiche ist noch keine ausführbare Laufzeit in Deck angebunden. Gespeicherte Profile sind vorbereitend.</p>':''}<div class="studio-tabs" role="tablist" aria-label="Modellverwaltung">${[['discover','Entdecken'],['library','Bibliothek'],['downloads','Downloads'],['profiles','Profile'],...(['image','chat'].includes(page)?[['test','Testen'],...(page==='chat'?[['auto','Auto-Test']]:[])]:[]),['running','Laufend']].map(([id,label])=>`<button role="tab" aria-selected="${section===id}" class="${section===id?'chosen':'secondary'}" data-section="${id}">${label}</button>`).join('')}</div><div role="tabpanel">${section==='auto'?AutoTestUI.html():section==='test'?(page==='chat'?ChatTestUI.html():ImageTestUI.html()):section==='running'&&page==='image'?ImageTestUI.html(true):section==='profiles'?ProfilesUI.html():section==='running'&&page==='chat'?EndpointUI.runningHTML():section==='running'?'<section class="card"><h2>Keine von Deck gestarteten Modelle</h2><p>Profile werden auf Athena gespeichert. Für diesen Bereich ist noch kein Modellworker angebunden. Fehlende Komponenten stehen beim jeweiligen Profil.</p><a class="link" href="#runtime">llama.cpp-Builds verwalten →</a></section>':CatalogUI.html(section==='library',section==='downloads')}</div>`;
document.querySelector('#view').innerHTML=`<div id="studio" data-page="${page}">${body}</div>`; document.querySelector('#view').innerHTML=`<div id="studio" data-page="${page}">${body}</div>`;
if(['docker-runtime','services'].includes(page)){DockerUI.bind(page==='services');return;} if(['docker-runtime','services'].includes(page)){DockerUI.bind(page==='services');return;}
if(page==='runtime'){RuntimeUI.bind();return;} if(page==='runtime'){RuntimeUI.bind();return;}
+6
View File
@@ -91,6 +91,12 @@ class CatalogTests(unittest.TestCase):
with tempfile.TemporaryDirectory() as d: with tempfile.TemporaryDirectory() as d:
c=Catalog(d);c.pending=[('sentinel',)];c.stop(shutdown=True) c=Catalog(d);c.pending=[('sentinel',)];c.stop(shutdown=True)
with patch('catalog.threading.Thread') as thread:c._next();thread.assert_not_called() with patch('catalog.threading.Thread') as thread:c._next();thread.assert_not_called()
def test_audio_categories_use_separate_tasks(self):
from urllib.parse import urlsplit,parse_qs
with tempfile.TemporaryDirectory() as d,patch('catalog.metadata',return_value=[]) as fetch:
for kind,task in [('audio','text-to-speech'),('stt','automatic-speech-recognition'),('music','text-to-audio'),('voice','audio-to-audio')]:
Catalog(d).search('',kind)
self.assertEqual(parse_qs(urlsplit(fetch.call_args.args[0]).query)['filter'],[task])
def test_cancellation(self): def test_cancellation(self):
with tempfile.TemporaryDirectory() as directory: with tempfile.TemporaryDirectory() as directory:
c=Catalog(directory);c.job=dict(state='downloading');c.stop() c=Catalog(directory);c.job=dict(state='downloading');c.stop()