From 52b1116bdba690f40e36084fc0977b9c4a1c658b Mon Sep 17 00:00:00 2001
From: Mikei386 <44135113+Mikei386@users.noreply.github.com>
Date: Mon, 28 Sep 2026 23:28:33 +0200
Subject: [PATCH] Separate audio discovery into TTS STT music and voice
categories
---
STUDIO.md | 6 ++++++
app.js | 2 +-
catalog-ui.js | 2 +-
catalog.py | 2 +-
index.html | 2 +-
profiles-ui.js | 2 +-
profiles.py | 1 +
studio.js | 5 +++--
test_catalog.py | 6 ++++++
9 files changed, 21 insertions(+), 7 deletions(-)
diff --git a/STUDIO.md b/STUDIO.md
index 86854ba..c456c5d 100644
--- a/STUDIO.md
+++ b/STUDIO.md
@@ -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.
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.
diff --git a/app.js b/app.js
index 5b5d24f..00a7e17 100644
--- a/app.js
+++ b/app.js
@@ -8,7 +8,7 @@ const metric=(title,value)=>`
${title}${value}v==null?'':``;
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;
-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==='network'){await networkPage();return;}
if(page==='home'||page==='endpoint'){await EndpointUI.render(page==='endpoint');return;
diff --git a/catalog-ui.js b/catalog-ui.js
index e8f0814..53a09d0 100644
--- a/catalog-ui.js
+++ b/catalog-ui.js
@@ -45,7 +45,7 @@ window.CatalogUI = (() => {
}
async function search(q) {
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); }
}
async function enrich(sequence){
diff --git a/catalog.py b/catalog.py
index 7e2d923..cbc9aad 100644
--- a/catalog.py
+++ b/catalog.py
@@ -11,7 +11,7 @@ import uuid
import urllib.parse
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):
p=urllib.parse.urlsplit(url)
diff --git a/index.html b/index.html
index 619ee64..2e7a2f4 100644
--- a/index.html
+++ b/index.html
@@ -1 +1 @@
-Athena DeckATHENA CONTROL SURFACEv0.7 · Router
+Athena DeckATHENA CONTROL SURFACEv0.7 · Router
diff --git a/profiles-ui.js b/profiles-ui.js
index 7714f8a..cea5861 100644
--- a/profiles-ui.js
+++ b/profiles-ui.js
@@ -8,7 +8,7 @@ window.ProfilesUI=(()=>{
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;
el('profile-list').innerHTML=rows.map(p=>`GESPEICHERT AUF ATHENA
${e(p.name)}
${e(p.model?.file||'Modelldatei nicht verfügbar')}
${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.
${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.
`);
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-components]').forEach(b=>b.onclick=()=>components(rows.find(p=>p.id===b.dataset.components)));
diff --git a/profiles.py b/profiles.py
index e578d8d..29ebeaa 100644
--- a/profiles.py
+++ b/profiles.py
@@ -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)},
'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)},
+ '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)}
}
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'}
diff --git a/studio.js b/studio.js
index d3a365c..202df2c 100644
--- a/studio.js
+++ b/studio.js
@@ -1,12 +1,13 @@
/* Server-backed model management. Legacy browser drafts remain untouched. */
const Studio=(()=>{
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){
if(!force&&document.querySelector('#studio')?.dataset.page===page)return;
if(category!==page){category=page;section='discover';}
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():`
ATHENA / MODELLVERWALTUNG
${labels[page]}
Modelle entdecken, herunterladen und mit gespeicherten Profilen konfigurieren.
${audioDescriptions[page]||'Modelle entdecken, herunterladen und mit gespeicherten Profilen konfigurieren.'}
${audioDescriptions[page]?'
Entdecken und Downloads sind verfügbar. Für diese Audio-Bereiche ist noch keine ausführbare Laufzeit in Deck angebunden. Gespeicherte Profile sind vorbereitend.
`;
if(['docker-runtime','services'].includes(page)){DockerUI.bind(page==='services');return;}
if(page==='runtime'){RuntimeUI.bind();return;}
diff --git a/test_catalog.py b/test_catalog.py
index 27e1f46..ea40486 100644
--- a/test_catalog.py
+++ b/test_catalog.py
@@ -91,6 +91,12 @@ class CatalogTests(unittest.TestCase):
with tempfile.TemporaryDirectory() as d:
c=Catalog(d);c.pending=[('sentinel',)];c.stop(shutdown=True)
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):
with tempfile.TemporaryDirectory() as directory:
c=Catalog(directory);c.job=dict(state='downloading');c.stop()