diff --git a/ideas/native_ui_config_mode_tracking.md b/ideas/native_ui_config_mode_tracking.md deleted file mode 100644 index e793f6b7..00000000 --- a/ideas/native_ui_config_mode_tracking.md +++ /dev/null @@ -1,28 +0,0 @@ -# Native UI Config Mode Tracking - -Issue: https://github.com/0xShug0/audio.cpp/issues/234 - -This note tracks native UI gaps seen when `audiocpp_server` is launched with a -server config but without `--ui-management`. - -## Bugs - -- The Studio model picker is built from the embedded model catalog, not from the - configured `/v1/models` response. In config mode this exposes models that the - server was not configured to serve. -- The UI uses catalog IDs as request model IDs. Configured server model IDs are - deployment-local, so a resident configured model may not be found if its ID - differs from the catalog ID. -- Management-only endpoints such as path inspection are correctly forbidden - when `ui_management=false`, but the UI still probes them and presents unclear - state. -- Package choice defaults are precision-centric. This can hide installed BF16 - packages behind a Q8 default and cannot represent multi-axis variants such as - ACE-Step base vs turbo. - -## Intended Direction - -- Add a config-mode UI path where selectable models come from `/v1/models`. -- Keep catalog/package-management behavior behind `--ui-management`. -- Make loaded/resident state use the actual server model ID. -- Represent package variants without assuming precision is the only choice axis. diff --git a/webui/configs/model_params.json b/webui/configs/model_params.json index d4f9a707..6214ce46 100644 --- a/webui/configs/model_params.json +++ b/webui/configs/model_params.json @@ -107,7 +107,7 @@ ], "ace_step": [ - {"name": "task_route", "type": "choice", "label": "task_route(操作类型)", "default": "text2music", "choices": ["text2music", "complete", "lego", "extract", "cover", "cover-nofsq", "repaint", "remix"], "info": "cover/remix=换词翻唱,非 text2music 需上传源音频;详见 webui/README.md"}, + {"name": "route", "type": "choice", "label": "route(操作类型)", "default": "text2music", "choices": ["text2music", "complete", "lego", "extract", "cover", "cover-nofsq", "repaint", "remix"], "info": "cover/remix=换词翻唱,非 text2music 需上传源音频;详见 webui/README.md"}, {"name": "num_inference_steps", "type": "number", "label": "num_inference_steps", "default": 8, "minimum": 1, "maximum": 20, "step": 1, "precision": 0, "info": "扩散步数(turbo 上限 20);remix 路由不填时默认 16,其他路由默认 8"}, {"name": "shift", "type": "slider", "label": "shift(时间步弯曲)", "default": 3.0, "minimum": 1.0, "maximum": 5.0, "step": 0.5, "info": "原版 turbo 默认 3.0;1.0 会明显劣化 remix 换词咬字"}, {"name": "guidance_scale", "type": "slider", "label": "guidance_scale", "default": 1.0, "minimum": 0.0, "maximum": 5.0, "step": 0.1}, diff --git a/webui/native/dist/index.html b/webui/native/dist/index.html index dcbb7910..22d18e38 100644 --- a/webui/native/dist/index.html +++ b/webui/native/dist/index.html @@ -13,20 +13,20 @@
diff --git a/webui/native/src/lib/api.ts b/webui/native/src/lib/api.ts index 8c944173..46399a89 100644 --- a/webui/native/src/lib/api.ts +++ b/webui/native/src/lib/api.ts @@ -156,12 +156,11 @@ export async function availableVoices(model = ''): Promise { return response.voices; } -export async function uploadWav(blob: Blob, filename: string, signal?: AbortSignal): Promise { +export async function uploadWav(blob: Blob, signal?: AbortSignal): Promise { const response = await jsonRequest<{ path: string }>('/v1/ui/upload', { method: 'POST', headers: { - 'Content-Type': 'audio/wav', - 'X-AudioCPP-Filename': filename + 'Content-Type': 'audio/wav' }, body: blob }, signal); diff --git a/webui/native/src/routes/+page.svelte b/webui/native/src/routes/+page.svelte index 9394325e..fb4c5088 100644 --- a/webui/native/src/routes/+page.svelte +++ b/webui/native/src/routes/+page.svelte @@ -183,10 +183,6 @@ } } - function loadedModelName(model: LoadedModel) { - return catalog.find((entry) => entry.id === model.id)?.display_name || model.id; - } - const workflowTabs = [ { id: 'tts', label: 'Text to speech', filterLabel: 'TTS', tasks: ['tts', 'clon'] }, { id: 'asr', label: 'ASR / Transcription', filterLabel: 'ASR', tasks: ['asr'] }, @@ -215,13 +211,53 @@ seed_vc: 'Seed-VC' }; + function pathVariantLabel(path: string) { + const normalized = path.replace(/\\/g, '/'); + const filename = normalized.split('/').filter(Boolean).pop() || ''; + const match = filename.match(/(?:^|[-_])(\d+(?:\.\d+)?[bm])(?:[-_]|$)/i); + return match ? match[1].toUpperCase() : ''; + } + + function catalogPathMatches(expectedPath: string, actualPath: string) { + const actual = comparablePath(actualPath); + const expected = comparablePath(resolveCatalogPath(expectedPath)); + if (actual === expected) return true; + const relative = comparablePath(expectedPath).replace(/^models\//, ''); + return actual === relative || actual.endsWith(`/${relative}`); + } + + function catalogEntryMatchesLoadedModel(entry: CatalogEntry, model: LoadedModel) { + if (entry.family !== model.family || entry.task !== model.task) return false; + if (catalogPathMatches(entry.path, model.path)) return true; + return Boolean((entry.install_packages || []).some((choice) => + catalogPathMatches(choice.path, model.path))); + } + + function loadedCatalogEntry(model: LoadedModel) { + const exact = catalog.find((entry) => entry.id === model.id); + if (exact && catalogEntryMatchesLoadedModel(exact, model)) return exact; + return catalog.find((entry) => catalogEntryMatchesLoadedModel(entry, model)); + } + + function inferredLoadedModelName(model: LoadedModel, base?: CatalogEntry) { + const variant = pathVariantLabel(model.path); + const familyName = familyLabels[model.family] || base?.display_name || model.family; + return variant && !familyName.toLowerCase().includes(variant.toLowerCase()) + ? `${familyName} ${variant}` + : familyName; + } + + function loadedModelName(model: LoadedModel) { + return loadedCatalogEntry(model)?.display_name || inferredLoadedModelName(model); + } + function compareModelNames(left: string, right: string) { return left.localeCompare(right, 'en', { sensitivity: 'base', numeric: true }); } function configuredCatalogEntries() { return loadedModels.map((model) => { - const exact = catalog.find((entry) => entry.id === model.id); + const exact = loadedCatalogEntry(model); const familyMatch = catalog.find((entry) => entry.family === model.family && entry.task === model.task); const base = exact || familyMatch; @@ -235,7 +271,7 @@ mode: model.mode }), id: model.id, - display_name: exact ? exact.display_name : base ? `${base.display_name} (${model.id})` : model.id, + display_name: exact ? exact.display_name : inferredLoadedModelName(model, base), display_name_en: exact ? exact.display_name_en : base?.display_name_en, family: model.family, path: model.path, @@ -353,11 +389,7 @@ } function packagePathMatches(choice: InstallPackageChoice, path: string) { - const actual = comparablePath(path); - const expected = comparablePath(resolveCatalogPath(choice.path)); - if (actual === expected) return true; - const relative = comparablePath(choice.path).replace(/^models\//, ''); - return actual === relative || actual.endsWith(`/${relative}`); + return catalogPathMatches(choice.path, path); } function residentModel(entry: CatalogEntry, models = loadedModels) { @@ -1017,7 +1049,7 @@ ? 44100 : ['asr', 'vad', 'diar', 'align', 'midi'].includes(selected.task) ? 16000 : undefined; const wav = await browserDecodeToWav(file, targetSampleRate); - return uploadWav(wav, file.name.replace(/\.[^.]+$/, '') + '.wav', aborter?.signal); + return uploadWav(wav, aborter?.signal); } function requestOptions() { @@ -1205,7 +1237,7 @@ if (!blob.size) return; const file = new File([blob], `live-${liveChunkNumber}.webm`, { type: blob.type }); const wav = await browserDecodeToWav(file, 16000); - const audio = await uploadWav(wav, `live-${liveChunkNumber}.wav`); + const audio = await uploadWav(wav); const result = await transcription({ model: selected.id, audio,