From 7891e7425b5d78a8ff53c6492d23e0acaed1f4e5 Mon Sep 17 00:00:00 2001 From: Warren B Date: Mon, 31 Aug 2026 01:05:46 +0100 Subject: [PATCH 1/5] model_specs: correct declarations that disagree with the code Sixteen specs described a request surface the engine does not implement, or failed to describe one it does. Each change below was checked against the C++ that reads the option. Declarations that were wrong - voxcpm1: num_inference_steps 50 -> 10, max_tokens 1024 -> 4096, min_tokens 0 -> 2, retry_badcase_max_times 2 -> 3, per voxcpm1/types.h:13-19. - fish_audio, outetts: text_chunk_mode default "word_budget" -> "default". The shared parser in framework/text/chunking.cpp throws on "word_budget", so the documented default was un-passable. Both move from the text_chunk_mode_full preset to explicit values, because "default" is not currently in that preset; a separate change corrects the preset itself. - midashenglm_gen: seed min -1 -> 0. parse_u32_option rejects any leading minus, so a spec-legal -1 was a runtime error, and the "-1 selects a random seed" claim went with it. - dramabox: five descriptions claimed their default came from config.json. dramabox/assets.cpp reads config.json for architecture only. - irodori_tts: text_chunk_mode values widened to the four the shared parser accepts. Declarations for options nothing reads - granite5asr: language. The session hard-codes "en" and never reads it. - firered_audio: session option firered_audio.helper_graph_arena_mb. Only the fireredtts3-prefixed key exists, and FireRedAudio validates session options strictly, so this one actively lied. Options the engine reads and no spec declared - minimax_music3: the five options added with the performance pack -- flow_uncond_interval, flow_uncond_warmup, ensemble_takes, ensemble_prefix_frames, flow_chunk_hop_frames -- plus the minimax_music3.pipeline_overlap session option. ensemble_takes returns several named outputs and had no way to be requested. - minimax_h3: first_block_cache_start_percent and _end_percent, the primary window whose _sigma override was already declared. - sense_asr: "vad" added to the audio_chunk_mode enum; the session implements it. - parakeet_tdt: language. Every other strict ASR family declares it, and it is a field of the OpenAI-compatible transcription contract, so a per-model rejection of it is a protocol violation. Documented as accepted and ignored, since Parakeet reads only keep_language_tags. - qwen3_forced_aligner, miocodec: options.session filled in with the seven and six prefixed session options their sessions actually read. Required flags that lie - heartmula: lyrics and tags marked required. heartmula/session.cpp:437-442 throws when either is empty, but the specs said optional, so a client had no way to know before the engine did. Deliberately not changed, with reasons recorded in the descriptions: fish_audio and outetts require reference_text only when reference audio is present, and the schema's `required` is a plain bool with no conditional form, so marking them required would break plain TTS with a built-in voice. mms_forced_aligner keeps return_timestamps: it is never read, but the family validates strictly and the CLI's --words-out injects the key, so deleting it would break forced alignment from the command line. The WebUI bundle is regenerated because catalog.ts inlines model_specs at frontend build time, so a spec edit does not reach users until it is rebuilt. Validation: python3 tools/check_loader_catalog_sync.py # ok, in sync cd webui/native && npm run build ctest -R model_spec_system_test # passes Backend tested: Metal (Apple M4 Max). Known limitations: the corrected declarations were verified by reading the option-parsing code, not by running each of the sixteen families -- most have no package installed here. No spec gained schema_version: the five that declare options without it would need dependencies and load blocks they do not have, and switching them to full v1 validation is not verifiable without running every affected model. --- model_specs/dramabox.json | 10 ++--- model_specs/firered_audio.json | 8 ---- model_specs/fish_audio.json | 13 ++++-- model_specs/granite5asr.json | 7 ---- model_specs/heartmula.json | 4 +- model_specs/irodori_tts.json | 2 + model_specs/midashenglm_gen.json | 4 +- model_specs/minimax_h3.json | 18 +++++++++ model_specs/minimax_music3.json | 48 ++++++++++++++++++++++ model_specs/miocodec.json | 54 +++++++++++++++++++++++++ model_specs/mms_forced_aligner.json | 2 +- model_specs/outetts.json | 13 ++++-- model_specs/parakeet_tdt.json | 6 +++ model_specs/qwen3_forced_aligner.json | 57 +++++++++++++++++++++++++++ model_specs/sense_asr.json | 3 +- model_specs/voxcpm1.json | 8 ++-- webui/native/dist/index.html | 18 ++++----- 17 files changed, 228 insertions(+), 47 deletions(-) diff --git a/model_specs/dramabox.json b/model_specs/dramabox.json index ed494c8d3..3ecbe701b 100644 --- a/model_specs/dramabox.json +++ b/model_specs/dramabox.json @@ -57,7 +57,7 @@ { "name": "num_inference_steps", "type": "int", - "description": "Diffusion sampling step count; default comes from config.json, 30 in the current package.", + "description": "Diffusion sampling step count; default 30.", "required": false, "min": 1, "default": 30 @@ -65,7 +65,7 @@ { "name": "guidance_scale", "type": "float", - "description": "Classifier-free guidance scale; default comes from config.json, 2.5 in the current package. Values greater than 1 enable CFG.", + "description": "Classifier-free guidance scale; default 2.5. Values greater than 1 enable CFG.", "required": false, "min": 0.0, "default": 2.5 @@ -73,7 +73,7 @@ { "name": "spatio_temporal_guidance_scale", "type": "float", - "description": "Spatio-temporal guidance scale; default comes from config.json, 1.5 in the current package. Values greater than 0 enable STG.", + "description": "Spatio-temporal guidance scale; default 1.5. Values greater than 0 enable STG.", "required": false, "min": 0.0, "default": 1.5 @@ -81,7 +81,7 @@ { "name": "duration_scale", "type": "float", - "description": "Multiplier applied to the estimated prompt duration when duration_sec is 0; default comes from config.json, 1.1 in the current package.", + "description": "Multiplier applied to the estimated prompt duration when duration_sec is 0; default 1.1.", "required": false, "min": 0.0, "default": 1.1 @@ -89,7 +89,7 @@ { "name": "reference_duration_sec", "type": "float", - "description": "Reference voice crop/repeat duration in seconds; default comes from config.json, 10.0 in the current package.", + "description": "Reference voice crop/repeat duration in seconds; default 10.0.", "required": false, "min": 0.0, "default": 10.0 diff --git a/model_specs/firered_audio.json b/model_specs/firered_audio.json index 7e54daf4b..32f05f856 100644 --- a/model_specs/firered_audio.json +++ b/model_specs/firered_audio.json @@ -182,14 +182,6 @@ "min": 1, "default": 1024 }, - { - "name": "helper_graph_arena_mb", - "type": "int", - "description": "Helper graph arena size in MiB; default 256.", - "required": false, - "min": 1, - "default": 256 - }, { "name": "weight_context_mb", "type": "int", diff --git a/model_specs/fish_audio.json b/model_specs/fish_audio.json index 9a15337c7..8530e14d2 100644 --- a/model_specs/fish_audio.json +++ b/model_specs/fish_audio.json @@ -29,7 +29,7 @@ { "name": "reference_text", "type": "string", - "description": "Reference transcript used with speaker reference audio.", + "description": "Reference transcript used with speaker reference audio. Required whenever the request carries inline reference audio; the model rejects the request without it.", "required": false }, { @@ -57,10 +57,15 @@ { "name": "text_chunk_mode", "type": "enum", - "description": "Framework text chunking mode; default word_budget.", - "preset": "text_chunk_mode_full", + "description": "Framework text chunking mode; default default.", + "values": [ + "default", + "tag_aware", + "japanese", + "endline" + ], "required": false, - "default": "word_budget" + "default": "default" }, { "name": "top_p", diff --git a/model_specs/granite5asr.json b/model_specs/granite5asr.json index f576999fb..ae85c315d 100644 --- a/model_specs/granite5asr.json +++ b/model_specs/granite5asr.json @@ -22,13 +22,6 @@ }, "options": { "request": [ - { - "name": "language", - "type": "string", - "description": "Recognition language (currently English).", - "required": false, - "default": "en" - }, { "name": "audio_chunk_mode", "type": "enum", diff --git a/model_specs/heartmula.json b/model_specs/heartmula.json index 0392f868e..d6f87fdec 100644 --- a/model_specs/heartmula.json +++ b/model_specs/heartmula.json @@ -26,13 +26,13 @@ "name": "lyrics", "type": "string", "description": "Lyrics text.", - "required": false + "required": true }, { "name": "tags", "type": "string", "description": "Comma-separated music tags.", - "required": false + "required": true }, { "name": "duration_sec", diff --git a/model_specs/irodori_tts.json b/model_specs/irodori_tts.json index 4da584fe6..ccebb6390 100644 --- a/model_specs/irodori_tts.json +++ b/model_specs/irodori_tts.json @@ -74,6 +74,8 @@ "type": "enum", "description": "Text chunking mode; default endline.", "values": [ + "default", + "tag_aware", "japanese", "endline" ], diff --git a/model_specs/midashenglm_gen.json b/model_specs/midashenglm_gen.json index 5704bf86f..0f92dbec0 100644 --- a/model_specs/midashenglm_gen.json +++ b/model_specs/midashenglm_gen.json @@ -67,9 +67,9 @@ { "name": "seed", "type": "int", - "description": "Generation seed; -1 selects a random seed.", + "description": "Generation seed; parsed as an unsigned 32-bit value, so negative seeds are rejected.", "required": false, - "min": -1, + "min": 0, "max": 2147483647, "default": 0 } diff --git a/model_specs/minimax_h3.json b/model_specs/minimax_h3.json index 9ac47500a..d4a38a686 100644 --- a/model_specs/minimax_h3.json +++ b/model_specs/minimax_h3.json @@ -192,6 +192,24 @@ "min": 0.0, "default": 0.1 }, + { + "name": "first_block_cache_start_percent", + "type": "float", + "description": "Start of the denoise window where the first-block cache may be used, as a fraction of the schedule.", + "required": false, + "min": 0.0, + "max": 1.0, + "default": 0.1 + }, + { + "name": "first_block_cache_end_percent", + "type": "float", + "description": "End of the denoise window where the first-block cache may be used, as a fraction of the schedule; must be greater than first_block_cache_start_percent.", + "required": false, + "min": 0.0, + "max": 1.0, + "default": 0.95 + }, { "name": "first_block_cache_start_sigma", "type": "float", diff --git a/model_specs/minimax_music3.json b/model_specs/minimax_music3.json index 97c165638..4da4d6aae 100644 --- a/model_specs/minimax_music3.json +++ b/model_specs/minimax_music3.json @@ -80,6 +80,47 @@ "required": false, "min": 0, "default": 0 + }, + { + "name": "flow_uncond_interval", + "type": "int", + "description": "Evaluate the flow unconditional CFG branch only every N-th step and reuse the cached guidance delta in between; 1 keeps the exact reference trajectory.", + "required": false, + "min": 1, + "default": 1 + }, + { + "name": "flow_uncond_warmup", + "type": "int", + "description": "Number of initial flow steps that always evaluate both CFG branches when delta reuse is enabled.", + "required": false, + "min": 0, + "default": 2 + }, + { + "name": "ensemble_takes", + "type": "int", + "description": "Decode N independent takes of the same prompt in one batched AR pass (seeds seed..seed+N-1); outputs are returned as named audio take_01..take_NN.", + "required": false, + "min": 1, + "max": 16, + "default": 1 + }, + { + "name": "ensemble_prefix_frames", + "type": "int", + "description": "Intro-lock for ensembles: decode the first N AR frames once as a shared master trajectory (~25 frames per second), then fork the takes; 0 disables.", + "required": false, + "min": 0, + "default": 0 + }, + { + "name": "flow_chunk_hop_frames", + "type": "int", + "description": "Flow chunk hop in AR frames (~25/sec); 0 keeps the model config (100, 50% chunk overlap).", + "required": false, + "min": 0, + "default": 0 } ], "session": [ @@ -141,6 +182,13 @@ "description": "Load large generation stages only while they are needed to reduce peak VRAM.", "required": false, "default": true + }, + { + "name": "pipeline_overlap", + "type": "bool", + "description": "Overlap AR decoding with per-chunk condition/flow/vocoder work on a second backend stream. Requires mem_saver=false; falls back to the sequential pipeline otherwise.", + "required": false, + "default": false } ], "load": [] diff --git a/model_specs/miocodec.json b/model_specs/miocodec.json index 8cb71e543..c94907a99 100644 --- a/model_specs/miocodec.json +++ b/model_specs/miocodec.json @@ -26,6 +26,60 @@ "speaker_reference" ] }, + "options": { + "request": [], + "session": [ + { + "name": "weight_type", + "type": "enum", + "description": "Codec weight storage type; default f32.", + "preset": "weight_type_full", + "required": false, + "default": "f32" + }, + { + "name": "weight_context_mb", + "type": "int", + "description": "Codec weight descriptor context size in MiB; default 256.", + "required": false, + "min": 1, + "default": 256 + }, + { + "name": "constant_context_mb", + "type": "int", + "description": "Reusable constant context size in MiB; default 256.", + "required": false, + "min": 1, + "default": 256 + }, + { + "name": "content_graph_arena_mb", + "type": "int", + "description": "Content encoder graph arena size in MiB; default 512.", + "required": false, + "min": 1, + "default": 512 + }, + { + "name": "global_graph_arena_mb", + "type": "int", + "description": "Global encoder graph arena size in MiB; default 256.", + "required": false, + "min": 1, + "default": 256 + }, + { + "name": "wave_graph_arena_mb", + "type": "int", + "description": "Wave decoder graph arena size in MiB; default 512.", + "required": false, + "min": 1, + "default": 512 + } + ], + "load": [] + }, "runtime": { "tags": [ "gguf" diff --git a/model_specs/mms_forced_aligner.json b/model_specs/mms_forced_aligner.json index 9862dee61..793ad37b7 100644 --- a/model_specs/mms_forced_aligner.json +++ b/model_specs/mms_forced_aligner.json @@ -61,7 +61,7 @@ { "name": "return_timestamps", "type": "bool", - "description": "Request word timestamps in the result; set automatically by --words-out.", + "description": "Accepted for cross-model compatibility (--words-out sets it) and ignored: the forced aligner always returns word timestamps.", "required": false, "default": true } diff --git a/model_specs/outetts.json b/model_specs/outetts.json index fc8e7dedc..4eae1b8bc 100644 --- a/model_specs/outetts.json +++ b/model_specs/outetts.json @@ -107,7 +107,7 @@ { "name": "reference_text", "type": "string", - "description": "Transcript matching the reference voice audio for voice cloning.", + "description": "Transcript matching the reference voice audio for voice cloning. Required whenever reference audio is supplied; the model rejects voice cloning without it.", "required": false }, { @@ -128,10 +128,15 @@ { "name": "text_chunk_mode", "type": "enum", - "description": "Framework long-form text chunking mode; default word_budget.", - "preset": "text_chunk_mode_full", + "description": "Framework long-form text chunking mode; default default.", + "values": [ + "default", + "tag_aware", + "japanese", + "endline" + ], "required": false, - "default": "word_budget" + "default": "default" } ], "session": [ diff --git a/model_specs/parakeet_tdt.json b/model_specs/parakeet_tdt.json index 7d9910dd8..96be20dfa 100644 --- a/model_specs/parakeet_tdt.json +++ b/model_specs/parakeet_tdt.json @@ -54,6 +54,12 @@ }, "options": { "request": [ + { + "name": "language", + "type": "string", + "description": "Transcription language from the OpenAI-compatible request field. Accepted for API compatibility; Parakeet TDT detects the language itself and does not use this value (see keep_language_tags).", + "required": false + }, { "name": "max_tokens", "type": "int", diff --git a/model_specs/qwen3_forced_aligner.json b/model_specs/qwen3_forced_aligner.json index 060d45db8..a5083531e 100644 --- a/model_specs/qwen3_forced_aligner.json +++ b/model_specs/qwen3_forced_aligner.json @@ -37,6 +37,63 @@ "required": false, "default": false } + ], + "session": [ + { + "name": "weight_type", + "type": "enum", + "description": "Fallback weight storage type for the thinker; default native.", + "preset": "weight_type_full", + "required": false, + "default": "native" + }, + { + "name": "thinker_weight_type", + "type": "enum", + "description": "Thinker matmul weight storage type; defaults to weight_type when set, otherwise native.", + "preset": "weight_type_full", + "required": false + }, + { + "name": "audio_encoder_weight_type", + "type": "enum", + "description": "Audio encoder weight storage type; default native.", + "preset": "weight_type_conv", + "required": false, + "default": "native" + }, + { + "name": "audio_encoder_graph_arena_mb", + "type": "int", + "description": "Audio encoder graph arena size in MiB; default 128.", + "required": false, + "min": 1, + "default": 128 + }, + { + "name": "thinker_prefill_graph_arena_mb", + "type": "int", + "description": "Thinker prefill graph arena size in MiB; default 256.", + "required": false, + "min": 1, + "default": 256 + }, + { + "name": "thinker_decode_graph_arena_mb", + "type": "int", + "description": "Thinker decode graph arena size in MiB; default 256.", + "required": false, + "min": 1, + "default": 256 + }, + { + "name": "thinker_weight_context_mb", + "type": "int", + "description": "Thinker weight descriptor context size in MiB; default 64.", + "required": false, + "min": 1, + "default": 64 + } ] }, "runtime": { diff --git a/model_specs/sense_asr.json b/model_specs/sense_asr.json index 868b896a3..2cc6102e2 100644 --- a/model_specs/sense_asr.json +++ b/model_specs/sense_asr.json @@ -69,10 +69,11 @@ { "name": "audio_chunk_mode", "type": "enum", - "description": "Audio chunking mode: auto, fixed, or none.", + "description": "Audio chunking mode: auto, fixed, vad, or none.", "values": [ "auto", "fixed", + "vad", "none" ], "required": false, diff --git a/model_specs/voxcpm1.json b/model_specs/voxcpm1.json index 66376a33c..423545b32 100644 --- a/model_specs/voxcpm1.json +++ b/model_specs/voxcpm1.json @@ -46,21 +46,21 @@ "type": "int", "description": "Maximum MiniCPM output tokens.", "required": false, - "default": 1024 + "default": 4096 }, { "name": "min_tokens", "type": "int", "description": "Minimum MiniCPM output tokens before an EOS stop is honored.", "required": false, - "default": 0 + "default": 2 }, { "name": "num_inference_steps", "type": "int", "description": "CFM diffusion sampling steps.", "required": false, - "default": 50 + "default": 10 }, { "name": "guidance_scale", @@ -81,7 +81,7 @@ "type": "int", "description": "Maximum bad-case retry count.", "required": false, - "default": 2 + "default": 3 }, { "name": "retry_badcase_ratio_threshold", diff --git a/webui/native/dist/index.html b/webui/native/dist/index.html index c30ce5757..fdcfa4be9 100644 --- a/webui/native/dist/index.html +++ b/webui/native/dist/index.html @@ -31,20 +31,20 @@
From 7e301745565646501b3ea74203d2b35fff38097a Mon Sep 17 00:00:00 2001 From: Warren B Date: Mon, 31 Aug 2026 01:06:34 +0100 Subject: [PATCH 2/5] webui: make every supported model reachable and correctly driven A third of the shipped packages could not be installed from the native WebUI, four model families were invisible, and several controls sent values the engine never asked for. Measured by executing catalog.ts against the real specs: 128 of 188 packages were reachable before, 161 after, with none lost. Package resolution (catalog.ts, types.ts) - Group install choices by target_directory instead of a hardcoded six-family allowlist. Packages that are precision variants of one model share a directory; packages that are different models do not. The allowlist had to be edited by hand for every new family and left every other family with at most one q8 and one fp16 choice. - Extend the precision-suffix list to q2_k..q6_k, q4_0, q5_0 and f32. It covered only q8/f16/bf16/safetensors/orig, so an entry whose download_id ended in an uncovered suffix narrowed to that single package and hid its siblings -- which is why PersonaPlex offered only Q4_K while its own spec marks Q8_0 default. - Honour ui.recommended_package. Every spec has one, all 64 resolve to a real package, and nothing read the field. - Label packages so that no two buttons of one entry read alike, and so that the text fits the button. The build alone ("Q8_0", "Q4_K ConvRot") is enough for nearly every entry; where several models share a target directory and collide on precision, the label keeps what the display names do not share ("Turbo BF16", "XL SFT BF16"), and falls back to the full display name if even that is ambiguous. The full name stays on the title and the aria-label, and both button lines clamp inside the border. - Match relatedness on target_directory instead of an id prefix. The prefix test pulled the IndexTTS2.5 packages into the IndexTTS2 entry, whose fp16 slot was then decided by spec-file array order. - An entry with no installable package is kept and flagged instead of silently vanishing from the whole UI. Coverage (models_catalog.json, 84 -> 98 entries) - Add moss_voicegen, which has an Apache-2.0 licence, an active loader, a published GGUF package and a matching UI tab, and no way to be selected. - Add f5_tts (Habibi unified plus seven Arabic dialect checkpoints), with its non-commercial licence named in the input hint. - Add the four non-English PocketTTS checkpoints and DotTTS Edit, all shipped and none reachable. - Point personaplex and supertonic at the packages their own specs mark default and recommended. - Rewrite every download_id to an exact packages[].id. 46 named a value that was not a package id and resolved only through a compatibility shim. - Regenerate every path from the resolved package. 40 named pre-GGUF directories no package installs; granite5asr was missing the models/ prefix entirely. - Retask the five entries that throw without reference audio from tts to clon, so the form requires the reference the engine demands. Request wiring (+page.svelte, Arena.svelte) - Stop sending a fixed max_tokens of 1024 on every request. Engine ceilings range from 300 to 4096; this halved Qwen3-TTS, quartered MOSS-TTS-Local and capped VibeVoice, whose default is no explicit cap. Blank now means the model's own limit. - Default the seed to -1, which the field's own label already documents as random. A fixed 1234 made fourteen families whose engine default is a fresh random seed return identical audio every run. - Render segments, speaker turns and word timings as a table with SRT and VTT export. This data was already produced and discarded. - Offer built-in voices from the spec and per-model voices from the server; both were plumbed and never read. - Hide the lyrics and duration controls for models that reject them, require a voice description for voice design and a transcript plus language for forced alignment, and send a seed for the conversion and analysis tasks that declare one. - Fix a hero subtitle that rendered the raw lookup key on the Voice conversion and Source separation tabs, and a double-encoded ellipsis in the models-folder status line. Validation: cd webui/native && npx svelte-check --tsconfig ./tsconfig.json # 0 errors cd webui/native && npm run build Reachability measured by loading the built catalog.ts through Vite's SSR loader and counting install choices, before and after. Backend tested: Metal (Apple M4 Max). Verified end to end against a live server: OmniVoice renders from a request shaped like the UI's, and Parakeet transcribes it back verbatim with nine word timings. Known limitations: min_vram_gb for the fourteen new entries is copied from architecturally comparable existing entries, not measured -- no spec field carries it and every existing value is hand-authored. f5_tts and moss_voicegen have no package installed here, so their entries are verified by resolution rather than by download and run. The 22 safetensors packages remain uninstallable from the UI, unchanged and deliberate. --- webui/configs/models_catalog.json | 178 ++++++++------- webui/native/dist/index.html | 30 ++- webui/native/src/app.css | 34 ++- webui/native/src/lib/catalog.ts | 312 +++++++++++++++++---------- webui/native/src/lib/i18n.ts | 14 ++ webui/native/src/lib/types.ts | 19 ++ webui/native/src/routes/+page.svelte | 224 ++++++++++++++----- webui/native/src/routes/Arena.svelte | 4 +- 8 files changed, 549 insertions(+), 266 deletions(-) diff --git a/webui/configs/models_catalog.json b/webui/configs/models_catalog.json index 924c2fd99..27dccaf58 100644 --- a/webui/configs/models_catalog.json +++ b/webui/configs/models_catalog.json @@ -3,34 +3,42 @@ "port": 8088, "device": 0, "threads": 1, - "_comment": "Native WebUI catalog for model families enabled in registry.cpp. The embedded server loads one selected model or precision at a time and resolves paths relative to the configured models root. Missing packages appear as not installed; the Models page invokes the repository-level model_manager_v2.py through the server to install spec-backed GGUF packages in the background. 'task' must be one of: tts, asr, vad, diar, sep, gen, clon, vc, s2s, align, vdes, spk, svc. 'download_id' is a model_specs package id and is omitted for bundled assets such as silero_vad. Optional per-entry keys include input_hint, default_options, session_options, and min_vram_gb. min_vram_gb is an estimated minimum CUDA VRAM value used only for UI guidance.", + "_comment": "Native WebUI catalog for model families enabled in registry.cpp. The embedded server loads one selected model or precision at a time and resolves paths relative to the configured models root. Missing packages appear as not installed; the Models page invokes the repository-level model_manager_v2.py through the server to install spec-backed GGUF packages in the background. 'task' must be one of: tts, asr, vad, diar, sep, gen, clon, vc, s2s, align, vdes, spk, svc, midi. 'download_id' must be the exact model_specs packages[].id this entry installs and loads -- not a family name or a legacy alias -- and 'path' must be that package's install location (models//). Both are omitted for bundled assets such as silero_vad and marblenet_vad. Optional per-entry keys include input_hint, default_options, session_options, and min_vram_gb. min_vram_gb is an estimated minimum CUDA VRAM value used only for UI guidance.", "models": [ - { "id": "omnivoice", "display_name": "OmniVoice (tts)", "family": "omnivoice", "path": "models/OmniVoice", "task": "tts", "mode": "offline", "download_id": "omnivoice", "min_vram_gb": 10 }, - { "id": "pocket-tts", "display_name": "Pocket TTS (tts)", "family": "pocket_tts", "path": "models/pocket-tts", "task": "tts", "mode": "offline", "download_id": "pocket_tts", "min_vram_gb": 2 }, - { "id": "dots-tts-soar", "display_name": "DotTTS SOAR (tts + clone)", "family": "dots_tts", "path": "models/DotTTS-SOAR-GGUF", "task": "tts", "mode": "offline", "download_id": "dots_tts_soar_q8_0", "min_vram_gb": 8 }, - { "id": "dots-tts-meanflow", "display_name": "DotTTS MeanFlow (tts + clone)", "family": "dots_tts", "path": "models/DotTTS-MF-GGUF", "task": "tts", "mode": "offline", "download_id": "dots_tts_mf_q8_0", "min_vram_gb": 8 }, - { "id": "neutts-2e", "display_name": "NeuTTS 2E (tts, preset voices)", "family": "neutts", "path": "models/NeuTTS-2E-GGUF", "task": "tts", "mode": "offline", "download_id": "neutts_2e_orig", "min_vram_gb": 4 }, - { "id": "qwen3-tts", "display_name": "Qwen3-TTS 0.6B (tts)", "family": "qwen3_tts", "path": "models/Qwen3-TTS-12Hz-0.6B-Base", "task": "tts", "mode": "offline", "download_id": "qwen3_tts_0_6b_base", "min_vram_gb": 5 }, - { "id": "qwen3-tts-1.7b", "display_name": "Qwen3-TTS 1.7B Base (tts)", "family": "qwen3_tts", "path": "models/Qwen3-TTS-12Hz-1.7B-Base", "task": "tts", "mode": "offline", "download_id": "qwen3_tts_1_7b_base", "min_vram_gb": 8 }, - { "id": "qwen3-tts-1.7b-custom", "display_name": "Qwen3-TTS 1.7B CustomVoice (tts)", "family": "qwen3_tts", "path": "models/Qwen3-TTS-12Hz-1.7B-CustomVoice", "task": "tts", "mode": "offline", "download_id": "qwen3_tts_1_7b_custom_voice", "min_vram_gb": 8 }, - { "id": "miotts", "display_name": "MioTTS 1.7B (tts; needs MioCodec)", "family": "miotts", "path": "models/MioTTS-1.7B", "task": "tts", "mode": "offline", "download_id": "miotts_1_7b", "min_vram_gb": 8 }, - { "id": "soprano-tts", "display_name": "Soprano TTS (tts)", "family": "soprano_tts", "path": "models/Soprano-1.1-80M-GGUF", "task": "tts", "mode": "offline", "download_id": "soprano_1_1_80m_q8_0", "min_vram_gb": 1 }, - { "id": "voxcpm2", "display_name": "VoxCPM2 (tts)", "family": "voxcpm2", "path": "models/VoxCPM2", "task": "tts", "mode": "offline", "download_id": "voxcpm2", "session_options": { "voxcpm2.weight_type": "q8_0" }, "min_vram_gb": 6 }, - { "id": "voxcpm1", "display_name": "VoxCPM1 0.5B (tts + clone)", "family": "voxcpm1", "path": "models/VoxCPM1-GGUF", "task": "tts", "mode": "offline", "download_id": "voxcpm1_0.5b_q8_0", "min_vram_gb": 4 }, - { "id": "vibevoice", "display_name": "VibeVoice 1.5B/7B (tts, long-form/multi-speaker)", "family": "vibevoice", "path": "models/VibeVoice-1.5B", "task": "tts", "mode": "offline", "download_id": "vibevoice_1_5b", "min_vram_gb": 7 }, - { "id": "vibevoice-7b", "display_name": "VibeVoice 7B (tts, long-form/multi-speaker)", "family": "vibevoice", "path": "models/VibeVoice-7B-GGUF", "task": "tts", "mode": "offline", "download_id": "vibevoice_7b_q8_0", "min_vram_gb": 16 }, - { "id": "index-tts2", "display_name": "IndexTTS2 (tts 中英克隆+情感)", "display_name_en": "IndexTTS2 (tts, zh/en clone + emotion)", "family": "index_tts2", "path": "models/IndexTTS-2", "task": "tts", "mode": "offline", "download_id": "index_tts2", "min_vram_gb": 8 }, - { "id": "index-tts2.5", "display_name": "IndexTTS2.5 (tts 多语种克隆+情感, GGUF Q8)", "display_name_en": "IndexTTS2.5 (tts, zh/en/ja/es/ar clone + emotion, GGUF Q8)", "family": "index_tts2", "path": "models/IndexTTS2.5-GGUF", "task": "tts", "mode": "offline", "download_id": "index_tts2_5_q8_0", "min_vram_gb": 8, + { "id": "omnivoice", "display_name": "OmniVoice (tts)", "family": "omnivoice", "path": "models/OmniVoice-GGUF/omnivoice-q8_0.gguf", "task": "tts", "mode": "offline", "download_id": "omnivoice_q8_0", "min_vram_gb": 10 }, + { "id": "pocket-tts", "display_name": "Pocket TTS (tts)", "family": "pocket_tts", "path": "models/PocketTTS-GGUF/english/pocket-tts-english-q8_0.gguf", "task": "tts", "mode": "offline", "download_id": "pocket_tts_english_q8_0", "min_vram_gb": 2 }, + { "id": "pocket-tts-german", "display_name": "Pocket TTS German (voice clone)", "family": "pocket_tts", "path": "models/PocketTTS-GGUF/german/pocket-tts-german-q8_0.gguf", "task": "clon", "mode": "offline", "download_id": "pocket_tts_german_q8_0", "min_vram_gb": 2, + "input_hint_en": "**Pocket TTS German**: German offline speech. Unlike the English package this checkpoint ships no packaged voice embedding, so a reference voice upload is required." }, + { "id": "pocket-tts-italian", "display_name": "Pocket TTS Italian (voice clone)", "family": "pocket_tts", "path": "models/PocketTTS-GGUF/italian/pocket-tts-italian-q8_0.gguf", "task": "clon", "mode": "offline", "download_id": "pocket_tts_italian_q8_0", "min_vram_gb": 2, + "input_hint_en": "**Pocket TTS Italian**: Italian offline speech. Unlike the English package this checkpoint ships no packaged voice embedding, so a reference voice upload is required." }, + { "id": "pocket-tts-portuguese", "display_name": "Pocket TTS Portuguese (voice clone)", "family": "pocket_tts", "path": "models/PocketTTS-GGUF/portuguese/pocket-tts-portuguese-q8_0.gguf", "task": "clon", "mode": "offline", "download_id": "pocket_tts_portuguese_q8_0", "min_vram_gb": 2, + "input_hint_en": "**Pocket TTS Portuguese**: Portuguese offline speech. Unlike the English package this checkpoint ships no packaged voice embedding, so a reference voice upload is required." }, + { "id": "pocket-tts-spanish", "display_name": "Pocket TTS Spanish (voice clone)", "family": "pocket_tts", "path": "models/PocketTTS-GGUF/spanish/pocket-tts-spanish-q8_0.gguf", "task": "clon", "mode": "offline", "download_id": "pocket_tts_spanish_q8_0", "min_vram_gb": 2, + "input_hint_en": "**Pocket TTS Spanish**: Spanish offline speech. Unlike the English package this checkpoint ships no packaged voice embedding, so a reference voice upload is required." }, + { "id": "dots-tts-soar", "display_name": "DotTTS SOAR (tts + clone)", "family": "dots_tts", "path": "models/DotTTS-SOAR-GGUF/dots-tts-soar-q8_0.gguf", "task": "tts", "mode": "offline", "download_id": "dots_tts_soar_q8_0", "min_vram_gb": 8 }, + { "id": "dots-tts-meanflow", "display_name": "DotTTS MeanFlow (tts + clone)", "family": "dots_tts", "path": "models/DotTTS-MF-GGUF/dots-tts-mf-q8_0.gguf", "task": "tts", "mode": "offline", "download_id": "dots_tts_mf_q8_0", "min_vram_gb": 8 }, + { "id": "neutts-2e", "display_name": "NeuTTS 2E (tts, preset voices)", "family": "neutts", "path": "models/NeuTTS-2E-GGUF/neutts-2e-orig.gguf", "task": "tts", "mode": "offline", "download_id": "neutts_2e_orig", "min_vram_gb": 4 }, + { "id": "qwen3-tts", "display_name": "Qwen3-TTS 0.6B (tts)", "family": "qwen3_tts", "path": "models/Qwen3-TTS-12Hz-0.6B-Base-GGUF/qwen3-tts-12hz-0.6b-base-q8_0.gguf", "task": "tts", "mode": "offline", "download_id": "qwen3_tts_0_6b_base_q8_0", "min_vram_gb": 5 }, + { "id": "qwen3-tts-1.7b", "display_name": "Qwen3-TTS 1.7B Base (tts)", "family": "qwen3_tts", "path": "models/Qwen3-TTS-12Hz-1.7B-Base-GGUF/qwen3-tts-12hz-1.7b-base-q8_0_v2.gguf", "task": "tts", "mode": "offline", "download_id": "qwen3_tts_1_7b_base_q8_0", "min_vram_gb": 8 }, + { "id": "qwen3-tts-1.7b-custom", "display_name": "Qwen3-TTS 1.7B CustomVoice (tts)", "family": "qwen3_tts", "path": "models/Qwen3-TTS-12Hz-1.7B-CustomVoice-GGUF/qwen3-tts-12hz-1.7b-customvoice-q8_0.gguf", "task": "tts", "mode": "offline", "download_id": "qwen3_tts_1_7b_customvoice_q8_0", "min_vram_gb": 8 }, + { "id": "miotts", "display_name": "MioTTS 1.7B (tts; needs MioCodec)", "family": "miotts", "path": "models/MioTTS-1.7B-GGUF/miotts-1.7b-q8_0.gguf", "task": "clon", "mode": "offline", "download_id": "miotts_1_7b_q8_0", "min_vram_gb": 8 }, + { "id": "soprano-tts", "display_name": "Soprano TTS (tts)", "family": "soprano_tts", "path": "models/Soprano-1.1-80M-GGUF/soprano-1.1-80m-q8_0.gguf", "task": "tts", "mode": "offline", "download_id": "soprano_1_1_80m_q8_0", "min_vram_gb": 1 }, + { "id": "voxcpm2", "display_name": "VoxCPM2 (tts)", "family": "voxcpm2", "path": "models/VoxCPM2-GGUF/voxcpm2-q8_0.gguf", "task": "tts", "mode": "offline", "download_id": "voxcpm2_q8_0", "session_options": { "voxcpm2.weight_type": "q8_0" }, "min_vram_gb": 6 }, + { "id": "voxcpm1", "display_name": "VoxCPM1 0.5B (tts + clone)", "family": "voxcpm1", "path": "models/VoxCPM1-GGUF/voxcpm-0.5b-q8_0-audiovae-f16.gguf", "task": "tts", "mode": "offline", "download_id": "voxcpm1_0.5b_q8_0", "min_vram_gb": 4 }, + { "id": "vibevoice", "display_name": "VibeVoice 1.5B/7B (tts, long-form/multi-speaker)", "family": "vibevoice", "path": "models/VibeVoice-1.5B-GGUF/vibevoice-1.5b-q8_0.gguf", "task": "tts", "mode": "offline", "download_id": "vibevoice_1_5b_q8_0", "min_vram_gb": 7 }, + { "id": "vibevoice-7b", "display_name": "VibeVoice 7B (tts, long-form/multi-speaker)", "family": "vibevoice", "path": "models/VibeVoice-7B-GGUF/vibevoice-7b-q8_0.gguf", "task": "tts", "mode": "offline", "download_id": "vibevoice_7b_q8_0", "min_vram_gb": 16 }, + { "id": "index-tts2", "display_name": "IndexTTS2 (tts 中英克隆+情感)", "display_name_en": "IndexTTS2 (tts, zh/en clone + emotion)", "family": "index_tts2", "path": "models/IndexTTS2-GGUF/index-tts2-q8_0.gguf", "task": "clon", "mode": "offline", "download_id": "index_tts2_q8_0", "min_vram_gb": 8 }, + { "id": "index-tts2.5", "display_name": "IndexTTS2.5 (tts 多语种克隆+情感, GGUF Q8)", "display_name_en": "IndexTTS2.5 (tts, zh/en/ja/es/ar clone + emotion, GGUF Q8)", "family": "index_tts2", "path": "models/IndexTTS2.5-GGUF/index-tts2_5-q8_0.gguf", "task": "clon", "mode": "offline", "download_id": "index_tts2_5_q8_0", "min_vram_gb": 8, "input_hint": "**IndexTTS2.5**:中/英/日/西/阿零样本克隆;上传参考音色即克隆;可在『其它参数(JSON)』里传 `lang`(默认 auto:含汉字按中文,否则按英文)与情感选项。许可证为 bilibili Model Use License(非 OSI),商用前请确认条款。", "input_hint_en": "**IndexTTS2.5**: zero-shot cloning in zh/en/ja/es/ar. Upload a reference voice to clone; pass `lang` (default auto: zh when the text contains Han characters, otherwise en) and emotion options through the JSON box. Weights are under the bilibili Model Use License (not OSI-approved) — check terms before commercial use." }, - { "id": "irodori-tts", "display_name": "Irodori-TTS v4 Small (tts 日语, GGUF Q8)", "display_name_en": "Irodori-TTS v4 Small (ja tts, GGUF Q8)", "family": "irodori_tts", "path": "models/Irodori-TTS-v4-Small-GGUF", "task": "tts", "mode": "offline", "download_id": "irodori_tts_v4_small_q8_0", "min_vram_gb": 4, + { "id": "irodori-tts", "display_name": "Irodori-TTS v4 Small (tts 日语, GGUF Q8)", "display_name_en": "Irodori-TTS v4 Small (ja tts, GGUF Q8)", "family": "irodori_tts", "path": "models/Irodori-TTS-v4-Small-GGUF/irodori-tts-v4-small-q8_0.gguf", "task": "tts", "mode": "offline", "download_id": "irodori_tts_v4_small_q8_0", "min_vram_gb": 4, "input_hint": "**Irodori-TTS v4 Small**:日语 TTS;可不上传参考音色直接生成,也可上传参考音色进行克隆;可在声音设计页用日语 caption 描述音色。", "input_hint_en": "**Irodori-TTS v4 Small**: Japanese TTS. Generate without a reference voice, clone from an uploaded reference, or use the voice-design page with a Japanese voice caption." }, - { "id": "irodori-tts-v3-500m", "display_name": "Irodori-TTS 500M v3 (tts 日语)", "display_name_en": "Irodori-TTS 500M v3 (ja tts)", "family": "irodori_tts", "path": "models/Irodori-TTS-500M-v3-GGUF", "task": "tts", "mode": "offline", "download_id": "irodori_tts_500m_v3_q8_0", "min_vram_gb": 4 }, - { "id": "moss-tts-local", "display_name": "MOSS-TTS-Local v1.5 (tts)", "family": "moss_tts_local", "path": "models/MOSS-TTS-Local-Transformer-v1.5", "task": "tts", "mode": "offline", "download_id": "moss_tts_local_v1_5", "min_vram_gb": 8 }, - { "id": "moss-tts-nano", "display_name": "MOSS-TTS-Nano 100M (tts)", "family": "moss_tts_nano", "path": "models/MOSS-TTS-Nano-100M", "task": "tts", "mode": "offline", "download_id": "moss_tts_nano_100m", "min_vram_gb": 2 }, - { "id": "magpie-tts", "display_name": "MagpieTTS Multilingual 357M v2607 (tts, preset voices)", "display_name_en": "MagpieTTS Multilingual 357M v2607 (tts, preset voices)", "family": "magpie_tts", "path": "models/MagpieTTS-Multilingual-357M-GGUF", "task": "tts", "mode": "offline", "download_id": "magpie_tts_q8_0", "min_vram_gb": 4, + { "id": "irodori-tts-v3-500m", "display_name": "Irodori-TTS 500M v3 (tts 日语)", "display_name_en": "Irodori-TTS 500M v3 (ja tts)", "family": "irodori_tts", "path": "models/Irodori-TTS-500M-v3-GGUF/irodori-tts-500m-v3-q8_0.gguf", "task": "tts", "mode": "offline", "download_id": "irodori_tts_500m_v3_q8_0", "min_vram_gb": 4 }, + { "id": "moss-tts-local", "display_name": "MOSS-TTS-Local v1.5 (tts)", "family": "moss_tts_local", "path": "models/MOSS-TTS-Local-v1.5-GGUF/moss-tts-local-v1.5-q8_0.gguf", "task": "tts", "mode": "offline", "download_id": "moss_tts_local_v1_5_q8_0", "min_vram_gb": 8 }, + { "id": "moss-tts-nano", "display_name": "MOSS-TTS-Nano 100M (tts)", "family": "moss_tts_nano", "path": "models/MOSS-TTS-Nano-100M-GGUF/moss-tts-nano-100m-q8_0.gguf", "task": "tts", "mode": "offline", "download_id": "moss_tts_nano_100m_q8_0", "min_vram_gb": 2 }, + { "id": "magpie-tts", "display_name": "MagpieTTS Multilingual 357M v2607 (tts, preset voices)", "display_name_en": "MagpieTTS Multilingual 357M v2607 (tts, preset voices)", "family": "magpie_tts", "path": "models/MagpieTTS-Multilingual-357M-GGUF/magpie-tts-multilingual-357m-q8_0.gguf", "task": "tts", "mode": "offline", "download_id": "magpie_tts_q8_0", "min_vram_gb": 4, "input_hint": "**MagpieTTS**:多语种离线 TTS;使用打包 speaker map 里的 `voice_id`,不需要上传参考音频。当前 GGUF 包不包含日语 phoneme 表,因此日语路径不可用。", "input_hint_en": "**MagpieTTS**: multilingual offline TTS with packaged speaker prompts selected by `voice_id`; no reference upload is needed. The current GGUF package does not include the Japanese phoneme table, so Japanese is not available." }, { "id": "fireredtts3-instruct", "display_name": "FireRedTTS3 Instruct Clone", "family": "fireredtts3", "path": "models/FireRedTTS3-Instruct-GGUF/fireredtts3-instruct-q8_0.gguf", "task": "clon", "mode": "offline", "download_id": "fireredtts3_instruct_q8_0", "min_vram_gb": 8, @@ -39,44 +47,60 @@ "input_hint_en": "**FireRedTTS3 Base**: zero-shot voice cloning. Upload a reference voice and provide the matching reference transcript." }, { "id": "firered-audio-tts", "display_name": "FireRedAudio Clone", "family": "firered_audio", "path": "models/FireRedAudio-GGUF/firered-audio-q8_0.gguf", "task": "clon", "mode": "offline", "download_id": "firered_audio_q8_0", "min_vram_gb": 10, "input_hint_en": "**FireRedAudio Clone**: upload a reference voice and provide the matching reference transcript. For no-reference voice design, use the VoiceDesign entry." }, - { "id": "supertonic", "display_name": "Supertonic 3 (tts 预置音色/多语种)", "display_name_en": "Supertonic 3 (tts, preset voices)", "family": "supertonic", "path": "models/supertonic-3", "task": "tts", "mode": "offline", "download_id": "supertonic_3", "min_vram_gb": 2 }, - { "id": "higgs-audio-tts", "display_name": "Higgs Audio v3 TTS 4B (tts 克隆, GGUF Q8)", "display_name_en": "Higgs Audio v3 TTS 4B (tts + clone, GGUF Q8)", "family": "higgs_audio_tts", "path": "models/Higgs-Audio-v3-TTS-4B-GGUF", "task": "tts", "mode": "offline", "download_id": "higgs_audio_v3_tts_4b", "min_vram_gb": 6, + { "id": "supertonic", "display_name": "Supertonic 3 (tts 预置音色/多语种)", "display_name_en": "Supertonic 3 (tts, preset voices)", "family": "supertonic", "path": "models/Supertonic-3-GGUF/supertonic-3-orig.gguf", "task": "tts", "mode": "offline", "download_id": "supertonic_3_orig", "min_vram_gb": 2 }, + { "id": "higgs-audio-tts", "display_name": "Higgs Audio v3 TTS 4B (tts 克隆, GGUF Q8)", "display_name_en": "Higgs Audio v3 TTS 4B (tts + clone, GGUF Q8)", "family": "higgs_audio_tts", "path": "models/Higgs-Audio-v3-TTS-4B-GGUF/higgs-audio-v3-tts-4b-q8_0.gguf", "task": "tts", "mode": "offline", "download_id": "higgs_audio_tts_4b_q8_0", "min_vram_gb": 6, "input_hint": "**Higgs Audio v3 TTS**:Q8_0 GGUF 包(权重已量化,不用再设 weight_type);上传参考音色即声音克隆,留空用默认音色;长文本自动分段。", "input_hint_en": "**Higgs Audio v3 TTS**: Q8_0 GGUF package (already quantized — no weight_type needed). Upload a reference voice to clone, or leave it empty for the default voice; long text is chunked automatically." }, - { "id": "fish-audio-s2-pro", "display_name": "Fish Audio S2 Pro (tts 克隆/控制标记, GGUF Q8)", "display_name_en": "Fish Audio S2 Pro (tts + clone/control tags, GGUF Q8)", "family": "fish_audio", "path": "models/Fish-Audio-S2-Pro-GGUF", "task": "tts", "mode": "offline", "download_id": "fish_audio_s2_pro", "min_vram_gb": 8, + { "id": "fish-audio-s2-pro", "display_name": "Fish Audio S2 Pro (tts 克隆/控制标记, GGUF Q8)", "display_name_en": "Fish Audio S2 Pro (tts + clone/control tags, GGUF Q8)", "family": "fish_audio", "path": "models/Fish-Audio-S2-Pro-GGUF/fish-audio-s2-pro-q8_0.gguf", "task": "tts", "mode": "offline", "download_id": "fish_audio_s2_pro_q8_0", "min_vram_gb": 8, "input_hint": "**Fish Audio S2 Pro**:Q8_0 GGUF 包;中英+自动语种;上传参考音色即克隆;正文里可写行内控制标记(如 (laugh))。", "input_hint_en": "**Fish Audio S2 Pro**: Q8_0 GGUF package; English/Chinese plus auto language. Upload a reference voice to clone; inline control tags such as (laugh) can be written in the text." }, - { "id": "glm-tts", "display_name": "GLM-TTS (tts 克隆, 社区)", "display_name_en": "GLM-TTS (tts + clone, community)", "family": "glm_tts", "path": "models/GLM-TTS", "task": "tts", "mode": "offline", "download_id": "glm_tts", "min_vram_gb": 8, + { "id": "glm-tts", "display_name": "GLM-TTS (tts 克隆, 社区)", "display_name_en": "GLM-TTS (tts + clone, community)", "family": "glm_tts", "path": "models/GLM-TTS-Q8/Text to audio (TTS)/GLM-TTS_Q8.gguf", "task": "clon", "mode": "offline", "download_id": "glm_tts_q8_0", "min_vram_gb": 8, "input_hint": "**GLM-TTS**(社区模型):中英 TTS / voice clone;上传参考音色即克隆。", "input_hint_en": "**GLM-TTS** (community): Chinese/English TTS and voice clone. Upload a reference voice to clone." }, - { "id": "outetts", "display_name": "Llama-OuteTTS 1.0 1B (tts 克隆, 社区)", "display_name_en": "Llama-OuteTTS 1.0 1B (tts + clone, community)", "family": "outetts", "path": "models/Llama-OuteTTS-1.0-1B", "task": "tts", "mode": "offline", "download_id": "outetts_1_0_1b", "min_vram_gb": 4, + { "id": "outetts", "display_name": "Llama-OuteTTS 1.0 1B (tts 克隆, 社区)", "display_name_en": "Llama-OuteTTS 1.0 1B (tts + clone, community)", "family": "outetts", "path": "models/Llama-OuteTTS-1.0-1B_Q8/Text to audio (TTS)/Llama-OuteTTS-1.0-1B_Q8.gguf", "task": "tts", "mode": "offline", "download_id": "outetts_1_0_1b_q8_0", "min_vram_gb": 4, "input_hint": "**OuteTTS 1.0 1B**(社区模型):23 种语言,DAC 编解码;上传参考音色即克隆。", "input_hint_en": "**OuteTTS 1.0 1B** (community): 23 languages, IBM DAC codec. Upload a reference voice to clone." }, - { "id": "vietneu-tts", "display_name": "VieNeu-TTS v3 Turbo (tts 越南语, 社区)", "display_name_en": "VieNeu-TTS v3 Turbo (vi tts, community)", "family": "vietneu_tts", "path": "models/VieNeu-TTS-v3-Turbo", "task": "tts", "mode": "offline", "download_id": "vietneu_tts_v3_turbo", "min_vram_gb": 4, + { "id": "vietneu-tts", "display_name": "VieNeu-TTS v3 Turbo (tts 越南语, 社区)", "display_name_en": "VieNeu-TTS v3 Turbo (vi tts, community)", "family": "vietneu_tts", "path": "models/VieNeu-TTS-v3-Turbo-GGUF/model.gguf", "task": "clon", "mode": "offline", "download_id": "vietneu_tts_v3_turbo_q8_0", "min_vram_gb": 4, "input_hint": "**VieNeu-TTS v3 Turbo**(社区模型):越南语 / 英语;上传参考音色即克隆。", "input_hint_en": "**VieNeu-TTS v3 Turbo** (community): Vietnamese and English. Upload a reference voice to clone." }, - { "id": "inflect-v2", "display_name": "Inflect Micro v2 (tts 英语, 社区)", "display_name_en": "Inflect Micro v2 (en tts, community)", "family": "inflect_v2", "path": "models/Inflect-Micro-v2", "task": "tts", "mode": "offline", "download_id": "inflect_micro_v2", "min_vram_gb": 2, + { "id": "inflect-v2", "display_name": "Inflect Micro v2 (tts 英语, 社区)", "display_name_en": "Inflect Micro v2 (en tts, community)", "family": "inflect_v2", "path": "models/Inflect-Micro-v2-GGUF/inflect-micro-v2-orig.gguf", "task": "tts", "mode": "offline", "download_id": "inflect_micro_v2_orig", "min_vram_gb": 2, "input_hint": "**Inflect Micro v2**(社区模型):英语离线 TTS;Micro 是默认包,Nano 可通过模型管理器另装后手动选择路径。", "input_hint_en": "**Inflect Micro v2** (community): English offline TTS. Micro is the default package; Nano can be installed separately and selected manually." }, - { "id": "dramabox", "display_name": "DramaBox (tts 克隆, GGUF Q8)", "display_name_en": "DramaBox (tts + clone, GGUF Q8)", "family": "dramabox", "path": "models/DramaBox-GGUF", "task": "tts", "mode": "offline", "download_id": "dramabox_q8_0", "min_vram_gb": 16, + { "id": "dramabox", "display_name": "DramaBox (tts 克隆, GGUF Q8)", "display_name_en": "DramaBox (tts + clone, GGUF Q8)", "family": "dramabox", "path": "models/DramaBox-GGUF/dramabox-q8_0.gguf", "task": "tts", "mode": "offline", "download_id": "dramabox_q8_0", "min_vram_gb": 16, "input_hint": "**DramaBox**:英语 TTS / voice clone;上传参考音色可克隆,长文本建议写清楚说话人描述。", "input_hint_en": "**DramaBox**: English TTS and voice clone. Upload a reference voice to clone; for long text, keep speaker wording explicit." }, - { "id": "confucius4-tts", "display_name": "Confucius4-TTS (voice clone, GGUF)", "display_name_en": "Confucius4-TTS (voice clone, GGUF)", "family": "confucius4_tts", "path": "models/Confucius4-TTS-GGUF", "task": "clon", "mode": "offline", "download_id": "confucius4_tts_orig", "min_vram_gb": 8, + { "id": "confucius4-tts", "display_name": "Confucius4-TTS (voice clone, GGUF)", "display_name_en": "Confucius4-TTS (voice clone, GGUF)", "family": "confucius4_tts", "path": "models/Confucius4-TTS-GGUF/confucius4-tts-orig.gguf", "task": "clon", "mode": "offline", "download_id": "confucius4_tts_orig", "min_vram_gb": 8, "input_hint": "**Confucius4-TTS**:需要参考音色;当前中文/英语路径更可靠,非中英语种仍在验证中。", "input_hint_en": "**Confucius4-TTS**: requires a reference voice. Chinese/English are the most reliable paths; other languages are still being validated." }, - { "id": "echo-tts", "display_name": "Echo-TTS (voice clone)", "family": "echo_tts", "path": "models/Echo-TTS-GGUF", "task": "clon", "mode": "offline", "download_id": "echo_tts_q8_0", "min_vram_gb": 8, + { "id": "echo-tts", "display_name": "Echo-TTS (voice clone)", "family": "echo_tts", "path": "models/Echo-TTS-GGUF/echo-tts-q8_0.gguf", "task": "clon", "mode": "offline", "download_id": "echo_tts_q8_0", "min_vram_gb": 8, "input_hint_en": "**Echo-TTS**: English zero-shot cloning at 44.1 kHz. Upload a reference voice -- no transcript needed. Output is CC-BY-NC-SA and may not be used commercially." }, - { "id": "chatterbox", "display_name": "Chatterbox (voice clone)", "family": "chatterbox", "path": "models/chatterbox", "task": "clon", "mode": "offline", "download_id": "chatterbox", "min_vram_gb": 12 }, + { "id": "chatterbox", "display_name": "Chatterbox (voice clone)", "family": "chatterbox", "path": "models/Chatterbox-GGUF/chatterbox-q8_0.gguf", "task": "clon", "mode": "offline", "download_id": "chatterbox_q8_0", "min_vram_gb": 12 }, + { "id": "f5-tts-habibi", "display_name": "F5-TTS Habibi Unified (voice clone, ar)", "family": "f5_tts", "path": "models/Habibi-TTS/Unified/habibi-unified-orig.gguf", "task": "clon", "mode": "offline", "download_id": "habibi_unified", "min_vram_gb": 4, + "input_hint_en": "**F5-TTS Habibi Unified**: Arabic zero-shot cloning across dialects. Upload a reference voice and provide the matching reference transcript; select a dialect with `dialect` (MSA, SAU, UAE, ALG, IRQ, EGY, MAR, ...) in the JSON box. Weights are cc-by-nc-sa-4.0 (F5-TTS Base is cc-by-nc-4.0) -- non-commercial use only." }, + { "id": "f5-tts-habibi-alg", "display_name": "F5-TTS Habibi Algerian (voice clone, ar-ALG)", "family": "f5_tts", "path": "models/Habibi-TTS/Specialized/ALG/habibi-alg-orig.gguf", "task": "clon", "mode": "offline", "download_id": "habibi_alg", "min_vram_gb": 4, + "input_hint_en": "**F5-TTS Habibi Algerian**: Arabic zero-shot cloning specialised for the Algerian checkpoint. Upload a reference voice and provide the matching reference transcript. Weights are cc-by-nc-sa-4.0 (F5-TTS Base is cc-by-nc-4.0) -- non-commercial use only." }, + { "id": "f5-tts-habibi-egy", "display_name": "F5-TTS Habibi Egyptian (voice clone, ar-EGY)", "family": "f5_tts", "path": "models/Habibi-TTS/Specialized/EGY/habibi-egy-orig.gguf", "task": "clon", "mode": "offline", "download_id": "habibi_egy", "min_vram_gb": 4, + "input_hint_en": "**F5-TTS Habibi Egyptian**: Arabic zero-shot cloning specialised for the Egyptian checkpoint. Upload a reference voice and provide the matching reference transcript. Weights are cc-by-nc-sa-4.0 (F5-TTS Base is cc-by-nc-4.0) -- non-commercial use only." }, + { "id": "f5-tts-habibi-irq", "display_name": "F5-TTS Habibi Iraqi (voice clone, ar-IRQ)", "family": "f5_tts", "path": "models/Habibi-TTS/Specialized/IRQ/habibi-irq-orig.gguf", "task": "clon", "mode": "offline", "download_id": "habibi_irq", "min_vram_gb": 4, + "input_hint_en": "**F5-TTS Habibi Iraqi**: Arabic zero-shot cloning specialised for the Iraqi checkpoint. Upload a reference voice and provide the matching reference transcript. Weights are cc-by-nc-sa-4.0 (F5-TTS Base is cc-by-nc-4.0) -- non-commercial use only." }, + { "id": "f5-tts-habibi-mar", "display_name": "F5-TTS Habibi Moroccan (voice clone, ar-MAR)", "family": "f5_tts", "path": "models/Habibi-TTS/Specialized/MAR/habibi-mar-orig.gguf", "task": "clon", "mode": "offline", "download_id": "habibi_mar", "min_vram_gb": 4, + "input_hint_en": "**F5-TTS Habibi Moroccan**: Arabic zero-shot cloning specialised for the Moroccan checkpoint. Upload a reference voice and provide the matching reference transcript. Weights are cc-by-nc-sa-4.0 (F5-TTS Base is cc-by-nc-4.0) -- non-commercial use only." }, + { "id": "f5-tts-habibi-msa", "display_name": "F5-TTS Habibi Modern Standard Arabic (voice clone, ar-MSA)", "family": "f5_tts", "path": "models/Habibi-TTS/Specialized/MSA/habibi-msa-orig.gguf", "task": "clon", "mode": "offline", "download_id": "habibi_msa", "min_vram_gb": 4, + "input_hint_en": "**F5-TTS Habibi Modern Standard Arabic**: Arabic zero-shot cloning specialised for the Modern Standard Arabic checkpoint. Upload a reference voice and provide the matching reference transcript. Weights are cc-by-nc-sa-4.0 (F5-TTS Base is cc-by-nc-4.0) -- non-commercial use only." }, + { "id": "f5-tts-habibi-sau", "display_name": "F5-TTS Habibi Saudi (voice clone, ar-SAU)", "family": "f5_tts", "path": "models/Habibi-TTS/Specialized/SAU/habibi-sau-orig.gguf", "task": "clon", "mode": "offline", "download_id": "habibi_sau", "min_vram_gb": 4, + "input_hint_en": "**F5-TTS Habibi Saudi**: Arabic zero-shot cloning specialised for the Saudi checkpoint. Upload a reference voice and provide the matching reference transcript. Weights are cc-by-nc-sa-4.0 (F5-TTS Base is cc-by-nc-4.0) -- non-commercial use only." }, + { "id": "f5-tts-habibi-uae", "display_name": "F5-TTS Habibi Emirati (voice clone, ar-UAE)", "family": "f5_tts", "path": "models/Habibi-TTS/Specialized/UAE/habibi-uae-orig.gguf", "task": "clon", "mode": "offline", "download_id": "habibi_uae", "min_vram_gb": 4, + "input_hint_en": "**F5-TTS Habibi Emirati**: Arabic zero-shot cloning specialised for the Emirati checkpoint. Upload a reference voice and provide the matching reference transcript. Weights are cc-by-nc-sa-4.0 (F5-TTS Base is cc-by-nc-4.0) -- non-commercial use only." }, - { "id": "ace-step", "display_name": "ACE-Step 1.5 (music gen)", "family": "ace_step", "path": "models/Ace-Step1.5", "task": "gen", "mode": "offline", "download_id": "ace_step", "session_options": { "ace_step.mem_saver": "true", "ace_step.dit_weight_type": "q8_0", "ace_step.text_encoder_weight_type": "q8_0", "ace_step.planner_weight_type": "q8_0" }, "min_vram_gb": 8 }, - { "id": "minimax-music3", "display_name": "MiniMax-Music3 (song gen)", "family": "minimax_music3", "path": "models/MiniMax-Music3-GGUF", "task": "gen", "mode": "offline", "download_id": "minimax_music3_q4_0", "min_vram_gb": 12 }, - { "id": "stable-audio-small-music","display_name": "Stable Audio 3 Small Music (gen)", "family": "stable_audio", "path": "models/stable-audio-3-small-music", "task": "gen", "mode": "offline", "download_id": "stable_audio_3_small_music", "min_vram_gb": 4 }, - { "id": "stable-audio-small-sfx", "display_name": "Stable Audio 3 Small SFX (gen)", "family": "stable_audio", "path": "models/stable-audio-3-small-sfx", "task": "gen", "mode": "offline", "download_id": "stable_audio_3_small_sfx", "min_vram_gb": 4 }, - { "id": "stable-audio-medium", "display_name": "Stable Audio 3 Medium (gen)", "family": "stable_audio", "path": "models/stable-audio-3-medium", "task": "gen", "mode": "offline", "download_id": "stable_audio_3_medium", "session_options": { "stable_audio.mem_saver": "true" }, "min_vram_gb": 10 }, - { "id": "heartmula", "display_name": "HeartMuLa 3B (music gen)", "family": "heartmula", "path": "models/HeartMuLa", "task": "gen", "mode": "offline", "download_id": "heartmula", "session_options": { "heartmula.mem_saver": "true" }, "min_vram_gb": 24 }, - { "id": "minimax-h3", "display_name": "MiniMax-H3 Q4 (sound generation)", "family": "minimax_h3", "path": "models/MiniMax-H3-Q4-GGUF/dit.gguf", "task": "gen", "mode": "offline", "download_id": "minimax_h3", "min_vram_gb": 20, + { "id": "ace-step", "display_name": "ACE-Step 1.5 (music gen)", "family": "ace_step", "path": "models/ACE-Step1.5-GGUF/turbo/ace-step-1.5-turbo-bf16.gguf", "task": "gen", "mode": "offline", "download_id": "ace_step_turbo_bf16", "session_options": { "ace_step.mem_saver": "true", "ace_step.dit_weight_type": "q8_0", "ace_step.text_encoder_weight_type": "q8_0", "ace_step.planner_weight_type": "q8_0" }, "min_vram_gb": 8 }, + { "id": "minimax-music3", "display_name": "MiniMax-Music3 (song gen)", "family": "minimax_music3", "path": "models/MiniMax-Music3-GGUF", "task": "gen", "mode": "offline", "download_id": "minimax_music3_q4_0", "min_vram_gb": 12 }, + { "id": "stable-audio-small-music","display_name": "Stable Audio 3 Small Music (gen)", "family": "stable_audio", "path": "models/Stable-Audio-3-Small-Music-GGUF/stable-audio-3-small-music-q8_0.gguf", "task": "gen", "mode": "offline", "download_id": "stable_audio_3_small_music_q8_0", "min_vram_gb": 4 }, + { "id": "stable-audio-small-sfx", "display_name": "Stable Audio 3 Small SFX (gen)", "family": "stable_audio", "path": "models/Stable-Audio-3-Small-SFX-GGUF/stable-audio-3-small-sfx-q8_0.gguf", "task": "gen", "mode": "offline", "download_id": "stable_audio_3_small_sfx_q8_0", "min_vram_gb": 4 }, + { "id": "stable-audio-medium", "display_name": "Stable Audio 3 Medium (gen)", "family": "stable_audio", "path": "models/Stable-Audio-3-Medium-GGUF/stable-audio-3-medium-q8_0.gguf", "task": "gen", "mode": "offline", "download_id": "stable_audio_3_medium_q8_0", "session_options": { "stable_audio.mem_saver": "true" }, "min_vram_gb": 10 }, + { "id": "heartmula", "display_name": "HeartMuLa 3B (music gen)", "family": "heartmula", "path": "models/HeartMuLa-GGUF/heartmula-q8_0.gguf", "task": "gen", "mode": "offline", "download_id": "heartmula_q8_0", "session_options": { "heartmula.mem_saver": "true" }, "min_vram_gb": 24 }, + { "id": "minimax-h3", "display_name": "MiniMax-H3 Q4 (sound generation)", "family": "minimax_h3", "path": "models/MiniMax-H3-Q4-GGUF/dit.gguf", "task": "gen", "mode": "offline", "download_id": "minimax_h3_q4_k", "min_vram_gb": 20, "default_options": { "num_inference_steps": 12, "height": 32, "width": 32, "num_frames": 241, "guidance_scale": 1.0, "dit_acceleration": "none", "return_video": false }, "input_hint_en": "MiniMax-H3 uses a joint audio/video DiT. The default Q4 DiT is the quality-first choice; the optional CUDA-only INT8 ConvRot DiT trades slightly more VRAM for higher speed. Native Studio uses 12 denoising steps, a 32x32 latent canvas, quality-first full-DiT execution, and disables video decoding for practical audio-only generation on a 24 GB GPU." }, { "id": "midashenglm-gen", "display_name": "MiDashengLM-Gen (audio generation)", "family": "midashenglm_gen", "path": "models/MiDashengLM-Gen-GGUF/midashenglm-gen-q8_0.gguf", "task": "gen", "mode": "offline", "download_id": "midashenglm_gen_q8_0", "min_vram_gb": 8, @@ -89,81 +113,85 @@ { "id": "firered-audio-acoustic-edit", "display_name": "FireRedAudio Acoustic Edit", "family": "firered_audio", "path": "models/FireRedAudio-GGUF/firered-audio-q8_0.gguf", "task": "gen", "mode": "offline", "download_id": "firered_audio_q8_0", "min_vram_gb": 10, "input_hint_en": "**FireRedAudio Acoustic Edit**: upload source audio and use a trained acoustic instruction such as `shift the pitch by 3 steps`." }, - { "id": "qwen3-asr", "display_name": "Qwen3-ASR 0.6B (asr)", "family": "qwen3_asr", "path": "models/Qwen3-ASR-0.6B", "task": "asr", "mode": "offline", "download_id": "qwen3_asr_0_6b", "min_vram_gb": 3 }, - { "id": "qwen3-asr-1.7b", "display_name": "Qwen3-ASR 1.7B HF (asr)", "family": "qwen3_asr", "path": "models/Qwen3-ASR-1.7B-hf", "task": "asr", "mode": "offline", "download_id": "qwen3_asr_1_7b_hf", "min_vram_gb": 6, + { "id": "qwen3-asr", "display_name": "Qwen3-ASR 0.6B (asr)", "family": "qwen3_asr", "path": "models/Qwen3-ASR-0.6B-GGUF/qwen3-asr-0.6b-q8_0.gguf", "task": "asr", "mode": "offline", "download_id": "qwen3_asr_0_6b_q8_0", "min_vram_gb": 3 }, + { "id": "qwen3-asr-1.7b", "display_name": "Qwen3-ASR 1.7B HF (asr)", "family": "qwen3_asr", "path": "models/Qwen3-ASR-1.7B-GGUF/qwen3-asr-1.7b-q8_0.gguf", "task": "asr", "mode": "offline", "download_id": "qwen3_asr_1_7b_q8_0", "min_vram_gb": 6, "input_hint": "**Qwen3-ASR 1.7B**(HF 原生权重,免转换):精度高于 0.6B;长音频自动分段转写;8G 卡显存偏紧,长音频建议先短段试跑。", "input_hint_en": "**Qwen3-ASR 1.7B**: native Hugging Face weights with no conversion required. It is more accurate than the 0.6B model and automatically chunks long audio; test short clips first on an 8 GB GPU." }, - { "id": "citrinet-asr", "display_name": "Citrinet ASR (asr)", "family": "citrinet_asr", "path": "models/citrinet", "task": "asr", "mode": "offline", "download_id": "citrinet_asr", "min_vram_gb": 2 }, - { "id": "nemotron-asr", "display_name": "Nemotron 3.5 ASR 0.6B (asr, 100+语种)", "display_name_en": "Nemotron 3.5 ASR 0.6B (asr, 100+ languages)", "family": "nemotron_asr", "path": "models/nemotron-3.5-asr-streaming-0.6b", "task": "asr", "mode": "offline", "download_id": "nemotron_asr", "min_vram_gb": 4, + { "id": "citrinet-asr", "display_name": "Citrinet ASR (asr)", "family": "citrinet_asr", "path": "models/Citrinet-ASR-GGUF/citrinet-asr-q8_0.gguf", "task": "asr", "mode": "offline", "download_id": "citrinet_asr_q8_0", "min_vram_gb": 2 }, + { "id": "nemotron-asr", "display_name": "Nemotron 3.5 ASR 0.6B (asr, 100+语种)", "display_name_en": "Nemotron 3.5 ASR 0.6B (asr, 100+ languages)", "family": "nemotron_asr", "path": "models/Nemotron-3.5-ASR-Streaming-0.6B-GGUF/nemotron-3.5-asr-streaming-0.6b-q8_0.gguf", "task": "asr", "mode": "offline", "download_id": "nemotron_asr_q8_0", "min_vram_gb": 4, "input_hint": "**Nemotron ASR**:100+ 语种,语种码为 BCP-47(如 en-US / zh-CN),留空=auto;模型自带长音频处理。", "input_hint_en": "**Nemotron ASR**: supports more than 100 languages using BCP-47 codes such as en-US or zh-CN. Leave language blank for automatic detection; long audio is handled by the model." }, - { "id": "higgs-audio-stt", "display_name": "Higgs Audio v3 STT (asr, 英语)", "display_name_en": "Higgs Audio v3 STT (asr, English)", "family": "higgs_audio_stt", "path": "models/higgs-audio-v3-stt", "task": "asr", "mode": "offline", "download_id": "higgs_audio_stt", "min_vram_gb": 8, + { "id": "higgs-audio-stt", "display_name": "Higgs Audio v3 STT (asr, 英语)", "display_name_en": "Higgs Audio v3 STT (asr, English)", "family": "higgs_audio_stt", "path": "models/Higgs-Audio-v3-STT-GGUF/higgs-audio-v3-stt-q8_0.gguf", "task": "asr", "mode": "offline", "download_id": "higgs_audio_stt_q8_0", "min_vram_gb": 8, "input_hint": "**Higgs Audio STT**:英语转写;可在文本框填指令(默认相当于 Transcribe the speech.);离线模式自动切分长音频。", "input_hint_en": "**Higgs Audio STT**: English transcription. The text box accepts an instruction; offline mode automatically chunks long audio." }, - { "id": "hviske-asr", "display_name": "Hviske v5.3 (asr, 丹麦语)", "display_name_en": "Hviske v5.3 (asr, Danish)", "family": "hviske_asr", "path": "models/hviske-v5.3", "task": "asr", "mode": "offline", "download_id": "hviske_asr", "min_vram_gb": 6, + { "id": "hviske-asr", "display_name": "Hviske v5.3 (asr, 丹麦语)", "display_name_en": "Hviske v5.3 (asr, Danish)", "family": "hviske_asr", "path": "models/Hviske-v5.3-GGUF/hviske-v5.3_Q8.gguf", "task": "asr", "mode": "offline", "download_id": "hviske_asr_q8_0", "min_vram_gb": 6, "input_hint": "**Hviske ASR**:丹麦语专用;模型侧自动分段。", "input_hint_en": "**Hviske ASR**: dedicated Danish transcription with automatic model-side segmentation." }, - { "id": "vibevoice-asr", "display_name": "VibeVoice ASR (asr, 多语种+说话人分段)", "display_name_en": "VibeVoice ASR (asr, multilingual + speaker turns)", "family": "vibevoice_asr", "path": "models/VibeVoice-ASR", "task": "asr", "mode": "offline", "download_id": "vibevoice_asr", "min_vram_gb": 20, + { "id": "vibevoice-asr", "display_name": "VibeVoice ASR (asr, 多语种+说话人分段)", "display_name_en": "VibeVoice ASR (asr, multilingual + speaker turns)", "family": "vibevoice_asr", "path": "models/VibeVoice-ASR-GGUF/vibevoice-asr-q8_0.gguf", "task": "asr", "mode": "offline", "download_id": "vibevoice_asr_q8_0", "min_vram_gb": 20, "input_hint": "**VibeVoice ASR**:自动语种,可输出分段/说话人轮次;文本框可填上下文提示(如 The recording is a meeting conversation.)。权重 17.3G,8G 卡跑不动。", "input_hint_en": "**VibeVoice ASR**: automatic language detection with segment and speaker-turn output. The text box accepts a context prompt. Its 17.3 GB weights require substantially more than 8 GB VRAM." }, - { "id": "voxtral-realtime", "display_name": "Voxtral Mini 4B Realtime (asr, 自动语种+流式)", "display_name_en": "Voxtral Mini 4B Realtime (asr, auto + streaming)", "family": "voxtral_realtime", "path": "models/Voxtral-Mini-4B-Realtime-2602-GGUF", "task": "asr", "mode": "offline", "download_id": "voxtral_realtime", "min_vram_gb": 8 }, - { "id": "fun-asr-nano", "display_name": "Fun-ASR-Nano 2512 (asr, GGUF Q8)", "display_name_en": "Fun-ASR-Nano 2512 (asr, GGUF Q8)", "family": "fun_asr_nano", "path": "models/Fun-ASR-Nano-2512-GGUF", "task": "asr", "mode": "offline", "download_id": "fun_asr_nano_2512_q8_0", "min_vram_gb": 4, + { "id": "voxtral-realtime", "display_name": "Voxtral Mini 4B Realtime (asr, 自动语种+流式)", "display_name_en": "Voxtral Mini 4B Realtime (asr, auto + streaming)", "family": "voxtral_realtime", "path": "models/Voxtral-Mini-4B-Realtime-2602-GGUF/voxtral-mini-4b-realtime-2602-q8_0.gguf", "task": "asr", "mode": "offline", "download_id": "voxtral_realtime_q8_0", "min_vram_gb": 8 }, + { "id": "fun-asr-nano", "display_name": "Fun-ASR-Nano 2512 (asr, GGUF Q8)", "display_name_en": "Fun-ASR-Nano 2512 (asr, GGUF Q8)", "family": "fun_asr_nano", "path": "models/Fun-ASR-Nano-2512-GGUF/fun-asr-nano-2512-q8_0.gguf", "task": "asr", "mode": "offline", "download_id": "fun_asr_nano_2512_q8_0", "min_vram_gb": 4, "input_hint": "**Fun-ASR-Nano**:轻量离线 ASR;支持 auto/中文/英语/日语。", "input_hint_en": "**Fun-ASR-Nano**: lightweight offline ASR; supports auto, Chinese, English and Japanese." }, - { "id": "parakeet-tdt", "display_name": "Parakeet-TDT 0.6B v3 (asr, 流式)", "display_name_en": "Parakeet-TDT 0.6B v3 (asr + streaming)", "family": "parakeet_tdt", "path": "models/parakeet-tdt-0.6b-v3", "task": "asr", "mode": "offline", "download_id": "parakeet_tdt", "min_vram_gb": 4, + { "id": "parakeet-tdt", "display_name": "Parakeet-TDT 0.6B v3 (asr, 流式)", "display_name_en": "Parakeet-TDT 0.6B v3 (asr + streaming)", "family": "parakeet_tdt", "path": "models/Parakeet-TDT-0.6B-v3-GGUF/parakeet-tdt-0.6b-v3-q8_0.gguf", "task": "asr", "mode": "offline", "download_id": "parakeet_tdt_q8_0", "min_vram_gb": 4, "input_hint": "**Parakeet-TDT**:离线/长音频/流式 ASR;支持多种欧洲语言,留空=自动。", "input_hint_en": "**Parakeet-TDT**: offline, long-form and streaming ASR for many European languages; leave language empty for auto." }, - { "id": "kroko-asr", "display_name": "Kroko Community ASR (asr, GGUF Q8)", "display_name_en": "Kroko Community ASR (asr, GGUF Q8)", "family": "kroko_asr", "path": "models/Kroko-ASR-GGUF", "task": "asr", "mode": "offline", "download_id": "kroko_asr_community_q8_0", "min_vram_gb": 4, + { "id": "kroko-asr", "display_name": "Kroko Community ASR (asr, GGUF Q8)", "display_name_en": "Kroko Community ASR (asr, GGUF Q8)", "family": "kroko_asr", "path": "models/Kroko-ASR-GGUF/kroko-en-community-64-l-q8_0.gguf", "task": "asr", "mode": "offline", "download_id": "kroko_asr_community_q8_0", "min_vram_gb": 4, "input_hint": "**Kroko Community ASR**:GGUF Q8 包;离线转写,支持时间戳。", "input_hint_en": "**Kroko Community ASR**: GGUF Q8 package for offline transcription with timestamps." }, - { "id": "granite5asr", "display_name": "Granite Speech 5.0 470M TurboCTC (asr)", "display_name_en": "Granite Speech 5.0 470M TurboCTC (asr)", "family": "granite5asr", "path": "granite5asr", "task": "asr", "mode": "offline", "download_id": "granite5asr_q8_0", "min_vram_gb": 4, + { "id": "granite5asr", "display_name": "Granite Speech 5.0 470M TurboCTC (asr)", "display_name_en": "Granite Speech 5.0 470M TurboCTC (asr)", "family": "granite5asr", "path": "models/Granite-Speech-5.0-470M-TurboCTC-GGUF/granite-speech-5.0-470m-turboctc-q8_0.gguf", "task": "asr", "mode": "offline", "download_id": "granite5asr_q8_0", "min_vram_gb": 4, "input_hint": "**Granite Speech 5.0 TurboCTC**:IBM 470M 英语 ASR;超快 Conformer CTC 转写;支持长音频自动分段与流式模式。", "input_hint_en": "**Granite Speech 5.0 TurboCTC**: IBM 470M English ASR with ultra-fast Conformer CTC architecture, supporting long-form audio segmentation and streaming mode." }, - { "id": "sense-asr", "display_name": "SenseVoice-Small (asr, 流式, 社区)", "display_name_en": "SenseVoice-Small (asr + streaming, community)", "family": "sense_asr", "path": "models/SenseVoice-Small-GGUF", "task": "asr", "mode": "offline", "download_id": "sensevoice_small_q8", "min_vram_gb": 4, + { "id": "sense-asr", "display_name": "SenseVoice-Small (asr, 流式, 社区)", "display_name_en": "SenseVoice-Small (asr + streaming, community)", "family": "sense_asr", "path": "models/SenseVoice-Small-GGUF/sensevoice-small-q8-audiocpp-v1.gguf", "task": "asr", "mode": "offline", "download_id": "sensevoice_small_q8", "min_vram_gb": 4, "input_hint": "**SenseVoice-Small**(社区模型):多语种 ASR,事件/情感/语言标签,ITN 可开关;离线与流式模式。", "input_hint_en": "**SenseVoice-Small** (community): multilingual ASR with event/emotion/language tags, optional ITN; offline and streaming modes." }, { "id": "firered-audio-asr", "display_name": "FireRedAudio (ASR / audio QA)", "family": "firered_audio", "path": "models/FireRedAudio-GGUF/firered-audio-q8_0.gguf", "task": "asr", "mode": "offline", "download_id": "firered_audio_q8_0", "min_vram_gb": 10, "input_hint_en": "**FireRedAudio ASR**: upload audio, then use the text box as the transcription or audio-understanding instruction." }, - { "id": "chatterbox-vc", "display_name": "Chatterbox (vc 声音转换)", "display_name_en": "Chatterbox (voice conversion)", "family": "chatterbox", "path": "models/chatterbox", "task": "vc", "mode": "offline", "download_id": "chatterbox", "min_vram_gb": 12, + { "id": "chatterbox-vc", "display_name": "Chatterbox (vc 声音转换)", "display_name_en": "Chatterbox (voice conversion)", "family": "chatterbox", "path": "models/Chatterbox-GGUF/chatterbox-q8_0.gguf", "task": "vc", "mode": "offline", "download_id": "chatterbox_q8_0", "min_vram_gb": 12, "input_hint": "**Chatterbox VC**:上传源语音和目标音色参考;模型保留源语音内容,将说话人音色转换为目标音色,输出 24kHz 单声道。", "input_hint_en": "**Chatterbox VC**: upload source speech and a target-voice reference. It preserves the source content and converts the speaker identity; output is 24 kHz mono." }, - { "id": "meanvc2", "display_name": "MeanVC2 (voice conversion)", "display_name_en": "MeanVC2 (voice conversion)", "family": "meanvc2", "path": "models/MeanVC2-GGUF/meanvc2-120ms-40ms-fp32.gguf", "task": "vc", "mode": "offline", "download_id": "meanvc2_120ms_40ms_f32", "min_vram_gb": 6, + { "id": "meanvc2", "display_name": "MeanVC2 (voice conversion)", "display_name_en": "MeanVC2 (voice conversion)", "family": "meanvc2", "path": "models/MeanVC2-GGUF/meanvc2-120ms-40ms-fp32.gguf", "task": "vc", "mode": "offline", "download_id": "meanvc2_120ms_40ms_f32", "min_vram_gb": 6, "input_hint": "**MeanVC2**:上传源语音和目标音色参考;默认 120 ms / 40 ms checkpoint 使用 F32 GGUF。", "input_hint_en": "**MeanVC2**: upload source speech and a target-voice reference. The default 120 ms / 40 ms checkpoint uses F32 GGUF." }, - { "id": "vevo2", "display_name": "Vevo2 (vc 语音转换, GGUF Q8)", "display_name_en": "Vevo2 (voice conversion, GGUF Q8)", "family": "vevo2", "path": "models/Vevo2-GGUF", "task": "vc", "mode": "offline", "download_id": "vevo2_gguf", "min_vram_gb": 6 }, - { "id": "vevo2-svc", "display_name": "Vevo2 (svc 歌声转换, GGUF Q8)", "display_name_en": "Vevo2 (singing voice conversion, GGUF Q8)", "family": "vevo2", "path": "models/Vevo2-GGUF", "task": "svc", "mode": "offline", "download_id": "vevo2_gguf", "min_vram_gb": 6, + { "id": "vevo2", "display_name": "Vevo2 (vc 语音转换, GGUF Q8)", "display_name_en": "Vevo2 (voice conversion, GGUF Q8)", "family": "vevo2", "path": "models/Vevo2-GGUF/vevo2-q8_0.gguf", "task": "vc", "mode": "offline", "download_id": "vevo2_q8_0", "min_vram_gb": 6 }, + { "id": "vevo2-svc", "display_name": "Vevo2 (svc 歌声转换, GGUF Q8)", "display_name_en": "Vevo2 (singing voice conversion, GGUF Q8)", "family": "vevo2", "path": "models/Vevo2-GGUF/vevo2-q8_0.gguf", "task": "svc", "mode": "offline", "download_id": "vevo2_q8_0", "min_vram_gb": 6, "input_hint": "**Vevo2 歌声转换 (svc)**:上传源歌声 + 目标歌手参考音色,默认 route=style_preserved_svc。style_converted_svc / singing_style_conversion 等风格转换 route 需在『其它参数(JSON)』里补 `style_ref`(服务器本地 wav 路径)/ `style_ref_text` / `target_text`。", "input_hint_en": "**Vevo2 singing conversion**: upload source singing and a target-singer reference. The default route is style_preserved_svc; style-conversion routes also accept style_ref, style_ref_text and target_text in Additional options." }, - { "id": "vevo2-s2s", "display_name": "Vevo2 (s2s 语音编辑, GGUF Q8)", "display_name_en": "Vevo2 (speech editing, GGUF Q8)", "family": "vevo2", "path": "models/Vevo2-GGUF", "task": "s2s", "mode": "offline", "download_id": "vevo2_gguf", "min_vram_gb": 6, + { "id": "vevo2-s2s", "display_name": "Vevo2 (s2s 语音编辑, GGUF Q8)", "display_name_en": "Vevo2 (speech editing, GGUF Q8)", "family": "vevo2", "path": "models/Vevo2-GGUF/vevo2-q8_0.gguf", "task": "s2s", "mode": "offline", "download_id": "vevo2_q8_0", "min_vram_gb": 6, "input_hint": "**Vevo2 语音编辑 (s2s)**:上传要编辑的源语音,并在『其它参数(JSON)』里填 `{\"target_text\": \"替换后的完整句子\"}`(编辑保持原说话人音色,可不上传目标音色)。", "input_hint_en": "**Vevo2 speech editing**: upload source speech and set target_text to the complete replacement sentence in Additional options. Editing preserves the original speaker and does not require a target-voice reference." }, - { "id": "seed-vc", "display_name": "Seed-VC (vc 语音转换)", "display_name_en": "Seed-VC (voice conversion)", "family": "seed_vc", "path": "models/SeedVC-MLX", "task": "vc", "mode": "offline", "download_id": "seed_vc", "min_vram_gb": 4 }, - { "id": "seed-vc-svc", "display_name": "Seed-VC (svc 歌声转换)", "display_name_en": "Seed-VC (singing voice conversion)", "family": "seed_vc", "path": "models/SeedVC-MLX", "task": "svc", "mode": "offline", "download_id": "seed_vc", "min_vram_gb": 4, + { "id": "dots-tts-edit", "display_name": "DotTTS Edit (speech editing)", "family": "dots_tts", "path": "models/DotTTS-Edit-GGUF/dots-tts-edit-q8_0.gguf", "task": "s2s", "mode": "offline", "download_id": "dots_tts_edit_q8_0", "min_vram_gb": 8, "default_options": { "template_name": "edit" }, + "input_hint_en": "**DotTTS Edit**: upload the source speech and describe the edit in the text box. Optional `instruction`, `source_text`, `target_text` and `use_xvector` overrides can be passed through the JSON box." }, + { "id": "seed-vc", "display_name": "Seed-VC (vc 语音转换)", "display_name_en": "Seed-VC (voice conversion)", "family": "seed_vc", "path": "models/SeedVC-MLX-GGUF/seed-vc-mlx-q8_0.gguf", "task": "vc", "mode": "offline", "download_id": "seed_vc_mlx_q8_0", "min_vram_gb": 4 }, + { "id": "seed-vc-svc", "display_name": "Seed-VC (svc 歌声转换)", "display_name_en": "Seed-VC (singing voice conversion)", "family": "seed_vc", "path": "models/SeedVC-MLX-GGUF/seed-vc-mlx-q8_0.gguf", "task": "svc", "mode": "offline", "download_id": "seed_vc_mlx_q8_0", "min_vram_gb": 4, "input_hint": "**Seed-VC 歌声转换 (svc)**:上传源歌声 + 目标歌手参考音色,默认 route=v1_svc(带 F0 条件)。可在『其它参数(JSON)』里调 `auto_f0_adjust` / `semi_tone_shift` / `f0_condition`。", "input_hint_en": "**Seed-VC singing conversion**: upload source singing and a target-singer reference. The default route is v1_svc with F0 conditioning." }, - { "id": "rvc", "display_name": "RVC (vc, GGUF F16)", "display_name_en": "RVC (voice conversion, GGUF F16)", "family": "rvc", "path": "models/RVC-GGUF", "task": "vc", "mode": "offline", "download_id": "rvc_f16", "min_vram_gb": 4, + { "id": "rvc", "display_name": "RVC (vc, GGUF F16)", "display_name_en": "RVC (voice conversion, GGUF F16)", "family": "rvc", "path": "models/RVC-GGUF/rvc-f16.gguf", "task": "vc", "mode": "offline", "download_id": "rvc_f16", "min_vram_gb": 4, "input_hint": "**RVC**:所选 GGUF 即目标音色;上传源语音即可转换;索引/音高等选项可用 JSON 传。", "input_hint_en": "**RVC**: the selected GGUF is the target voice; upload source speech to convert. Index and pitch options can be passed through the JSON box." }, - { "id": "miocodec", "display_name": "MioCodec (vc; codec dependency)", "family": "miocodec", "path": "models/MioCodec-25Hz-44.1kHz-v2", "task": "vc", "mode": "offline", "download_id": "miocodec_25hz_44k_v2", "min_vram_gb": 3 }, - { "id": "personaplex", "display_name": "PersonaPlex 7B v1 (speech conversation)", "display_name_en": "PersonaPlex 7B v1 (speech conversation)", "family": "personaplex", "path": "models/PersonaPlex-GGUF", "task": "s2s", "mode": "offline", "download_id": "personaplex_7b_v1_q4_k", "min_vram_gb": 8, + { "id": "miocodec", "display_name": "MioCodec (vc; codec dependency)", "family": "miocodec", "path": "models/MioCodec-25Hz-44.1kHz-v2-GGUF/miocodec-25hz-44khz-v2-q8_0.gguf", "task": "vc", "mode": "offline", "download_id": "miocodec_q8_0", "min_vram_gb": 3 }, + { "id": "personaplex", "display_name": "PersonaPlex 7B v1 (speech conversation)", "display_name_en": "PersonaPlex 7B v1 (speech conversation)", "family": "personaplex", "path": "models/PersonaPlex-GGUF/personaplex-7b-v1-q8_0.gguf", "task": "s2s", "mode": "offline", "download_id": "personaplex_7b_v1_q8_0", "min_vram_gb": 8, "request_options": ["voice_id", "system_prompt", "temperature", "text_temperature", "top_k", "text_top_k", "do_sample", "seed"], "input_hint": "**PersonaPlex**:上传用户语音,模型返回语音回复;文本框可填写 assistant system/persona prompt;`voice_id` 选择打包音色,也可上传参考音色覆盖。", "input_hint_en": "**PersonaPlex**: upload user speech and receive a spoken response. The text box provides the assistant system/persona prompt; `voice_id` selects a packaged voice, and an uploaded reference voice overrides it." }, { "id": "audiosr", "display_name": "AudioSR (audio super-resolution)", "family": "audiosr", "path": "models/AudioSR-GGUF/audiosr-basic-f32.gguf", "task": "s2s", "mode": "offline", "download_id": "audiosr_basic_f32", "min_vram_gb": 8, "input_hint_en": "**AudioSR**: upload a source audio file to generate a super-resolved output." }, - { "id": "htdemucs", "display_name": "HTDemucs (sep 音源分离)", "display_name_en": "HTDemucs (source separation)", "family": "htdemucs", "path": "models/htdemucs", "task": "sep", "mode": "offline", "download_id": "htdemucs", "min_vram_gb": 3 }, - { "id": "bs-roformer", "display_name": "BS-RoFormer (sep 人声分离)", "display_name_en": "BS-RoFormer (vocal separation)", "family": "bs_roformer", "path": "models/BS-RoFormer-ep368-GGUF/bs-roformer-ep368-q8_0.gguf", "task": "sep", "mode": "offline", "download_id": "bs_roformer_q8_0", "min_vram_gb": 3 }, - { "id": "mel-band-roformer", "display_name": "Mel-Band RoFormer (sep 人声分离)", "display_name_en": "Mel-Band RoFormer (vocal separation)", "family": "mel_band_roformer", "path": "models/mel-roformer-mlx", "task": "sep", "mode": "offline", "download_id": "mel_band_roformer", "min_vram_gb": 3 }, + { "id": "htdemucs", "display_name": "HTDemucs (sep 音源分离)", "display_name_en": "HTDemucs (source separation)", "family": "htdemucs", "path": "models/HTDemucs-GGUF/htdemucs-q8_0.gguf", "task": "sep", "mode": "offline", "download_id": "htdemucs_q8_0", "min_vram_gb": 3 }, + { "id": "bs-roformer", "display_name": "BS-RoFormer (sep 人声分离)", "display_name_en": "BS-RoFormer (vocal separation)", "family": "bs_roformer", "path": "models/BS-RoFormer-ep368-GGUF/bs-roformer-ep368-q8_0.gguf", "task": "sep", "mode": "offline", "download_id": "bs_roformer_q8_0", "min_vram_gb": 3 }, + { "id": "mel-band-roformer", "display_name": "Mel-Band RoFormer (sep 人声分离)", "display_name_en": "Mel-Band RoFormer (vocal separation)", "family": "mel_band_roformer", "path": "models/Mel-Band-RoFormer-GGUF/mel-band-roformer-q8_0.gguf", "task": "sep", "mode": "offline", "download_id": "mel_band_roformer_q8_0", "min_vram_gb": 3 }, - { "id": "silero-vad", "display_name": "Silero VAD (vad, bundled)", "family": "silero_vad", "path": "assets/framework/models/silero_vad", "task": "vad", "mode": "offline", "min_vram_gb": 1 }, - { "id": "marblenet-vad", "display_name": "MarbleNet VAD (vad, bundled)", "family": "marblenet_vad", "path": "assets/framework/models/marblenet_vad", "task": "vad", "mode": "offline", "min_vram_gb": 1 }, - { "id": "sortformer-diar", "display_name": "Sortformer Diarization 4spk (diar)", "family": "sortformer_diar", "path": "models/diar_sortformer_4spk-v1", "task": "diar", "mode": "offline", "download_id": "sortformer_diar_4spk_v1", "min_vram_gb": 2 }, - { "id": "qwen3-forced-aligner", "display_name": "Qwen3 Forced Aligner (align)", "family": "qwen3_forced_aligner", "path": "models/Qwen3-ForcedAligner-0.6B", "task": "align", "mode": "offline", "download_id": "qwen3_forced_aligner_0_6b", "min_vram_gb": 3 }, - { "id": "muscriptor-small", "display_name": "MuScriptor Small (audio to MIDI)", "family": "muscriptor", "path": "models/MuScriptor-Small-GGUF", "task": "midi", "mode": "offline", "download_id": "muscriptor_small_f32", "min_vram_gb": 4 }, + { "id": "silero-vad", "display_name": "Silero VAD (vad, bundled)", "family": "silero_vad", "path": "assets/framework/models/silero_vad", "task": "vad", "mode": "offline", "min_vram_gb": 1 }, + { "id": "marblenet-vad", "display_name": "MarbleNet VAD (vad, bundled)", "family": "marblenet_vad", "path": "assets/framework/models/marblenet_vad", "task": "vad", "mode": "offline", "min_vram_gb": 1 }, + { "id": "sortformer-diar", "display_name": "Sortformer Diarization 4spk (diar)", "family": "sortformer_diar", "path": "models/Sortformer-Diar-4spk-v1-GGUF/sortformer-diar-4spk-v1-q8_0.gguf", "task": "diar", "mode": "offline", "download_id": "sortformer_diar_4spk_v1_q8_0", "min_vram_gb": 2 }, + { "id": "qwen3-forced-aligner", "display_name": "Qwen3 Forced Aligner (align)", "family": "qwen3_forced_aligner", "path": "models/Qwen3-ForcedAligner-0.6B-GGUF/qwen3-forced-aligner-0.6b-q8_0.gguf", "task": "align", "mode": "offline", "download_id": "qwen3_forced_aligner_0_6b_q8_0", "min_vram_gb": 3 }, + { "id": "muscriptor-small", "display_name": "MuScriptor Small (audio to MIDI)", "family": "muscriptor", "path": "models/MuScriptor-Small-GGUF/muscriptor-small-f32.gguf", "task": "midi", "mode": "offline", "download_id": "muscriptor_small_f32", "min_vram_gb": 4 }, - { "id": "qwen3-tts-1.7b-vdesign", "display_name": "Qwen3-TTS 1.7B VoiceDesign (vdes)", "family": "qwen3_tts", "path": "models/Qwen3-TTS-12Hz-1.7B-VoiceDesign", "task": "vdes", "mode": "offline", "download_id": "qwen3_tts_1_7b_voice_design", "min_vram_gb": 8, + { "id": "qwen3-tts-1.7b-vdesign", "display_name": "Qwen3-TTS 1.7B VoiceDesign (vdes)", "family": "qwen3_tts", "path": "models/Qwen3-TTS-12Hz-1.7B-VoiceDesign-GGUF/qwen3-tts-12hz-1.7b-voicedesign-q8_0.gguf", "task": "vdes", "mode": "offline", "download_id": "qwen3_tts_1_7b_voicedesign_q8_0", "min_vram_gb": 8, "input_hint": "**Qwen3-TTS VoiceDesign**:在『音色描述』里用文字描述想要的声音(如“低沉磁性的中年男声,语速偏慢”),配上要念的文本即可,无需参考音频。", "input_hint_en": "**Qwen3-TTS VoiceDesign**: describe the desired voice, then enter the text to synthesize. No reference recording is required." }, { "id": "fireredtts3-instruct-vdesign", "display_name": "FireRedTTS3 Instruct VoiceDesign", "family": "fireredtts3", "path": "models/FireRedTTS3-Instruct-GGUF/fireredtts3-instruct-q8_0.gguf", "task": "vdes", "mode": "offline", "download_id": "fireredtts3_instruct_q8_0", "min_vram_gb": 8, "input_hint_en": "**FireRedTTS3 VoiceDesign**: describe the target voice in Voice description, then enter the text to synthesize." }, { "id": "firered-audio-vdesign", "display_name": "FireRedAudio VoiceDesign", "family": "firered_audio", "path": "models/FireRedAudio-GGUF/firered-audio-q8_0.gguf", "task": "vdes", "mode": "offline", "download_id": "firered_audio_q8_0", "min_vram_gb": 10, "input_hint_en": "**FireRedAudio VoiceDesign**: describe the target voice in Voice description, then enter the text to synthesize." }, - { "id": "irodori-tts-vdesign", "display_name": "Irodori-TTS v4 Small VoiceDesign (vdes 日语, GGUF Q8)", "display_name_en": "Irodori-TTS v4 Small VoiceDesign (ja vdes, GGUF Q8)", "family": "irodori_tts", "path": "models/Irodori-TTS-v4-Small-GGUF", "task": "vdes", "mode": "offline", "download_id": "irodori_tts_v4_small_q8_0", "min_vram_gb": 4, + { "id": "irodori-tts-vdesign", "display_name": "Irodori-TTS v4 Small VoiceDesign (vdes 日语, GGUF Q8)", "display_name_en": "Irodori-TTS v4 Small VoiceDesign (ja vdes, GGUF Q8)", "family": "irodori_tts", "path": "models/Irodori-TTS-v4-Small-GGUF/irodori-tts-v4-small-q8_0.gguf", "task": "vdes", "mode": "offline", "download_id": "irodori_tts_v4_small_q8_0", "min_vram_gb": 4, "input_hint": "**Irodori-TTS v4 VoiceDesign**(日语):『音色描述』用日语 caption 描述音色(如「落ち着いた大人の男性。深く響く声。」),文本填要念的日语内容,无需参考音频。", "input_hint_en": "**Irodori-TTS v4 VoiceDesign**: provide a Japanese voice caption and Japanese synthesis text. No reference recording is required." }, - { "id": "irodori-tts-v3-vdesign", "display_name": "Irodori-TTS 600M v3 VoiceDesign (vdes 日语)", "display_name_en": "Irodori-TTS 600M v3 VoiceDesign (ja vdes)", "family": "irodori_tts", "path": "models/Irodori-TTS-600M-v3-VoiceDesign-GGUF", "task": "vdes", "mode": "offline", "download_id": "irodori_tts_600m_v3_voicedesign_q8_0", "min_vram_gb": 4, - "input_hint": "**Irodori-TTS v3 VoiceDesign**(日语):『音色描述』用日语 caption 描述音色(如「落ち着いた大人の男性。深く響く声。」),文本填要念的日语内容,无需参考音频。", "input_hint_en": "**Irodori-TTS v3 VoiceDesign**: provide a Japanese voice caption and Japanese synthesis text. No reference recording is required." } + { "id": "irodori-tts-v3-vdesign", "display_name": "Irodori-TTS 600M v3 VoiceDesign (vdes 日语)", "display_name_en": "Irodori-TTS 600M v3 VoiceDesign (ja vdes)", "family": "irodori_tts", "path": "models/Irodori-TTS-600M-v3-VoiceDesign-GGUF/irodori-tts-600m-v3-voicedesign-q8_0.gguf", "task": "vdes", "mode": "offline", "download_id": "irodori_tts_600m_v3_voicedesign_q8_0", "min_vram_gb": 4, + "input_hint": "**Irodori-TTS v3 VoiceDesign**(日语):『音色描述』用日语 caption 描述音色(如「落ち着いた大人の男性。深く響く声。」),文本填要念的日语内容,无需参考音频。", "input_hint_en": "**Irodori-TTS v3 VoiceDesign**: provide a Japanese voice caption and Japanese synthesis text. No reference recording is required." }, + { "id": "moss-voicegen", "display_name": "MOSS-VoiceGenerator (voice design)", "family": "moss_voicegen", "path": "models/MOSS-VoiceGenerator-GGUF/moss_voicegen_bf16_codec_f16_decode.gguf", "task": "vdes", "mode": "offline", "download_id": "moss_voicegen_bf16_codec_f16_decode", "min_vram_gb": 8, + "input_hint_en": "**MOSS-VoiceGenerator**: Apache-2.0 voice design -- the speaker comes from a written description instead of a reference recording. The model reads the description from the `instruct` option, so pass it in the JSON box (for example {\"instruct\": \"A warm male radio voice in his fifties, calm.\"}); it also expects full language names such as English or Chinese rather than en/zh." } ] } diff --git a/webui/native/dist/index.html b/webui/native/dist/index.html index fdcfa4be9..fe87802a4 100644 --- a/webui/native/dist/index.html +++ b/webui/native/dist/index.html @@ -24,27 +24,35 @@ })(); -
diff --git a/webui/native/src/app.css b/webui/native/src/app.css index df994638a..c685a3bd7 100644 --- a/webui/native/src/app.css +++ b/webui/native/src/app.css @@ -156,7 +156,7 @@ button:disabled { opacity: .45; cursor: not-allowed; } button.primary, button.run { color: var(--text-invert); background: linear-gradient(135deg, var(--cyan), #5ac7ff); border: 0; font-weight: 800; } .button-row button { flex: 1; } .studio-package-buttons { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 5px; margin-top: 11px; } -.studio-package-buttons button, .single-model-toggle { position: relative; min-width: 0; min-height: 38px; padding: 6px 4px; font-size: 9px; line-height: 1.15; } +.studio-package-buttons button, .single-model-toggle { position: relative; min-width: 0; min-height: 38px; padding: 6px 4px; overflow: hidden; font-size: 9px; line-height: 1.15; overflow-wrap: anywhere; } .studio-package-buttons button.selected-package:not(:disabled) { border-color: var(--active-border); color: var(--cyan); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--cyan) 10%, transparent); } .studio-package-buttons button:disabled { color: var(--control-disabled); background: var(--panel-2); opacity: .48; } .studio-package-buttons button.resident, .single-model-toggle.resident { padding-right: 12px; border-color: var(--active-border); color: var(--text-strong); background: var(--active-bg); opacity: 1; } @@ -215,6 +215,13 @@ kbd { font: 9px ui-monospace, monospace; padding: 2px 4px; border: 1px solid rgb .audio-list a { color: var(--cyan); text-decoration: none; } audio { width: 100%; height: 38px; } .transcript { margin-top: 14px; } +.timed-rows { margin-top: 14px; } +.timed-rows-scroll { max-height: 250px; overflow: auto; border: 1px solid var(--line); border-radius: 8px; margin-top: 6px; } +.timed-rows table { width: 100%; border-collapse: collapse; font-size: 12px; } +.timed-rows th, .timed-rows td { padding: 6px 9px; text-align: left; border-bottom: 1px solid var(--line); } +.timed-rows th { position: sticky; top: 0; background: var(--card-bg); color: var(--muted); font-weight: 500; } +.timed-rows td:first-child, .timed-rows td:nth-child(2) { white-space: nowrap; font-variant-numeric: tabular-nums; color: var(--text-subtle); } +.timed-rows tr:last-child td { border-bottom: none; } pre { background: var(--code-bg); border: 1px solid var(--line); border-radius: 8px; padding: 10px; overflow: auto; max-height: 250px; color: var(--text-subtle); white-space: pre-wrap; overflow-wrap: anywhere; } .arena-hero { align-items: end; } @@ -268,16 +275,22 @@ pre { background: var(--code-bg); border: 1px solid var(--line); border-radius: .model-actions { display: grid; gap: 6px; width: clamp(280px, 17vw, 324px); min-width: 0; }.model-actions small { text-align: right; } .model-actions button { padding: 7px; font-size: 11px; } .package-buttons { display: grid; grid-template-columns: repeat(3, minmax(76px, 1fr)); gap: 5px; } -.package-buttons.wide-package-set { grid-template-columns: repeat(2, minmax(0, 1fr)); } -.package-buttons.wide-package-set .package-size { line-height: 1.15; } +/* Labels are the build only ('Turbo BF16'), so even a six-package set stays on + the three-column grid instead of widening to two half-width columns. */ +.package-buttons.wide-package-set { grid-template-columns: repeat(3, minmax(0, 1fr)); } .package-buttons:has(> .package-choice:only-child) { grid-template-columns: minmax(0, 1fr); } .package-buttons:has(> .package-choice:nth-child(2):last-child) { grid-template-columns: repeat(2, minmax(0, 1fr)); } -.package-choice { position: relative; height: 48px; min-width: 0; } -.package-install { display: flex; width: 100%; height: 48px; min-width: 0; min-height: 0; padding-top: 4px; padding-bottom: 4px; flex-direction: column; align-items: center; justify-content: center; gap: 2px; white-space: nowrap; } -.package-install > span:first-child { line-height: 1.1; } +.package-choice { position: relative; height: 56px; min-width: 0; } +.package-install { display: flex; width: 100%; height: 56px; min-width: 0; min-height: 0; padding: 4px 6px; flex-direction: column; align-items: center; justify-content: center; gap: 2px; overflow: hidden; text-align: center; } +/* Both lines clamp instead of overflowing: the package label falls back to the + full model name when the short build name would be ambiguous, and the size + line carries a status as well as the byte count. */ +.package-install > span { display: -webkit-box; max-width: 100%; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow-wrap: anywhere; } +.package-install > span:first-child { line-height: 1.15; } .package-install.preferred { border-color: var(--active-border); color: var(--cyan); box-shadow: inset 0 0 0 1px rgba(66,232,213,.1); } -.package-install.downloaded { padding-right: 23px; border-color: var(--button-hover-border); background: color-mix(in srgb, var(--button-bg) 80%, white 20%); color: var(--blue); white-space: normal; } -.package-install.downloaded > span:first-child { white-space: nowrap; } +.package-install.downloaded { padding-right: 23px; border-color: var(--button-hover-border); background: color-mix(in srgb, var(--button-bg) 80%, white 20%); color: var(--blue); } +/* Keep the label clear of the corner badges that overlay the button. */ +.package-choice:has(.package-update) .package-install { padding-left: 26px; } .package-install.downloaded:hover:not(:disabled) { border-color: var(--button-hover-border); background: color-mix(in srgb, var(--button-hover-bg) 76%, white 24%); } .package-install.downloaded.preferred { border-color: var(--active-border); color: var(--cyan); box-shadow: inset 0 0 0 1px rgba(66,232,213,.16); } .model-actions .package-delete { position: absolute; top: 3px; right: 3px; z-index: 1; display: grid; width: 18px; height: 18px; min-height: 0; padding: 2px; place-items: center; border-color: transparent; background: transparent; color: var(--muted); } @@ -285,7 +298,10 @@ pre { background: var(--code-bg); border: 1px solid var(--line); border-radius: .package-delete svg { width: 12px; height: 12px; fill: currentColor; } .model-actions .package-update { position: absolute; top: 3px; left: 3px; z-index: 1; width: auto; min-height: 18px; padding: 2px 4px; border-color: rgba(66,232,213,.3); background: var(--active-bg); color: var(--cyan); font-size: 7px; } .shared-package-note { padding: 7px 9px; border: 1px dashed var(--line); border-radius: 7px; color: var(--muted); font-size: 9px; text-align: center; } -.package-size { color: var(--muted); font-size: 8px; font-weight: 500; letter-spacing: 0; text-transform: none; } +.package-size { color: var(--muted); font-size: 8px; font-weight: 500; line-height: 1.2; letter-spacing: 0; text-transform: none; } +/* The size line carries a status word as well as the byte count, so it gets a + line more than the label above it. */ +.package-install > span.package-size { -webkit-line-clamp: 3; line-clamp: 3; } .install-progress { grid-column: 1/-1; width: 100%; min-width: 0; max-width: 100%; } .install-progress-head { display: flex; justify-content: space-between; gap: 8px; color: var(--muted); font-size: 9px; } .install-progress-head strong { color: var(--blue); text-transform: uppercase; }.install-progress.complete .install-progress-head strong, .install-progress.cleaned .install-progress-head strong { color: var(--cyan); }.install-progress.failed .install-progress-head strong, .install-progress.cancelled .install-progress-head strong { color: var(--danger); } diff --git a/webui/native/src/lib/catalog.ts b/webui/native/src/lib/catalog.ts index baaf51af4..2fbb8a11e 100644 --- a/webui/native/src/lib/catalog.ts +++ b/webui/native/src/lib/catalog.ts @@ -1,6 +1,11 @@ import rawCatalog from '../../../configs/models_catalog.json'; import rawParams from '../../../configs/model_params.json'; -import type { CatalogEntry, InstallPackageChoice, ParamSpec } from './types'; +import type { + CatalogEntry, + InstallPackageChoice, + InstallPackageSlot, + ParamSpec +} from './types'; interface PackageEntry { family: string; @@ -23,6 +28,7 @@ interface PackageSpec { ui?: { builtin_voices?: string[]; default_voice?: string; + recommended_package?: string; }; } @@ -39,14 +45,15 @@ const packages: PackageEntry[] = Object.values(specModules).flatMap((spec) => ); const specsByFamily = new Map(Object.values(specModules).map((spec) => [spec.family, spec])); -const exposeAllGgufPackageFamilies = new Set([ - 'audiosr', - 'controlfoley', - 'firered_audio', - 'fireredtts3', - 'meanvc2', - 'midashenglm_gen' -]); + +// Package ids carry a trailing format/precision tag. Stripping it yields the +// "stem" that identifies the model itself, so a catalog download id written +// before (or after) a requantisation can still be matched to its package. Keep +// longer tags ahead of their prefixes (`q8_0` before `q8`, `q4_0` before `q4`) +// so the alternation strips the whole tag. This list is the single source of +// truth for suffix stripping anywhere in this module. +const packageIdSuffix = + /_(?:q2_k|q3_k|q4_0|q4_k|q4|q5_0|q5_k|q6_k|q8_0|q8|f16|fp16|bf16|f32|safetensors|orig)$/i; const hanCharacters = /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/u; @@ -70,81 +77,103 @@ const cleanPath = (value: string) => value const cleanId = (value: string) => value.toLowerCase().replace(/[^a-z0-9]/g, ''); -function preferredPackage(entries: PackageEntry[]): PackageEntry | undefined { - return entries.find((entry) => entry.default) || +// The id that identifies the model behind a package, independent of how that +// package was quantised. +const packageIdStem = (value: string) => cleanId(value.replace(packageIdSuffix, '')); + +const isGguf = (entry: PackageEntry) => entry.format === 'gguf'; + +const familyPackages = (family: string) => + packages.filter((candidate) => candidate.family === family); + +const recommendedPackageId = (family: string) => + specsByFamily.get(family)?.ui?.recommended_package; + +// Picks one package out of a candidate set. `ui.recommended_package` is the +// spec's own answer to "which build should a new user get", so it outranks the +// structural fallbacks; `default` and q8_0 remain for candidate sets that do +// not contain the recommendation (a second model in the same family). +function preferredPackage(entries: PackageEntry[], recommended?: string): PackageEntry | undefined { + return (recommended ? entries.find((entry) => entry.id === recommended) : undefined) || + entries.find((entry) => entry.default) || entries.find((entry) => entry.precision === 'q8_0') || entries[0]; } -function relatedPackages(entry: CatalogEntry): PackageEntry[] { - const family = packages.filter((candidate) => candidate.family === entry.family); - if (!family.length) return []; - if (entry.family === 'ace_step' || entry.family === 'minimax_music3') return family; - if (!entry.download_id) return family; - const exact = family.find((candidate) => candidate.id === entry.download_id); - if (exact) { - const stem = exact.id.replace(/_(?:q8_0|q8|f16|fp16|bf16|safetensors|orig)$/i, ''); - const matches = family.filter((candidate) => - candidate.id.replace(/_(?:q8_0|q8|f16|fp16|bf16|safetensors|orig)$/i, '') === stem); - return matches.length ? matches : [exact]; - } +// Resolves the single package a catalog entry points at. Precedence, most +// specific first: +// 1. an exact package id — the catalog names the package outright; +// 2. the same id stem — the catalog names the model but a stale (or absent) +// precision tag, e.g. `omnivoice` -> `omnivoice_q8_0`. Stems are compared +// for equality rather than by prefix on purpose: a prefix test makes +// `index_tts2` swallow the separate `index_tts2_5_*` model; +// 3. a GGUF package installing into the catalog `path`. Restricted to GGUF +// because a legacy path such as models/pocket-tts names the upstream +// Safetensors drop, not the GGUF build the entry actually wants; +// 4. the family's `ui.recommended_package`, for entries whose download id is +// a bare family name that never matched a package; +// 5. anything GGUF in the family, so a not-yet-migrated entry still resolves. +// Steps 3-5 exist only for catalog entries that have not been migrated to real +// package ids; steps 1-2 cover every migrated entry. +function resolvedPackage(entry: CatalogEntry): PackageEntry | undefined { + const family = familyPackages(entry.family); + if (!family.length) return undefined; + const recommended = recommendedPackageId(entry.family); + if (entry.download_id) { + const exact = family.find((candidate) => candidate.id === entry.download_id); + if (exact) return exact; - // Resolve a legacy family/variant id before considering its old target - // directory. A directory such as models/pocket-tts may identify the gated - // upstream safetensors package, while the family default is the public GGUF - // package intended by the catalog's generic `pocket_tts` download id. - const legacyId = cleanId(entry.download_id); - const legacyMatches = family.filter((candidate) => { - const currentId = cleanId(candidate.id); - return currentId.startsWith(legacyId) || legacyId.startsWith(currentId); - }); - if (legacyMatches.length) return legacyMatches; - - const target = cleanPath(entry.path); - const targetMatches = family.filter((candidate) => cleanPath(candidate.target_directory) === target); - if (targetMatches.length) { - const stems = new Set(targetMatches.map((candidate) => - candidate.id.replace(/_(?:q8_0|q8|f16|fp16|bf16|safetensors|orig)$/i, ''))); - const matches = family.filter((candidate) => stems.has( - candidate.id.replace(/_(?:q8_0|q8|f16|fp16|bf16|safetensors|orig)$/i, ''))); - return matches.length ? matches : targetMatches; + const stem = packageIdStem(entry.download_id); + const stemMatches = family.filter((candidate) => packageIdStem(candidate.id) === stem); + if (stemMatches.length) { + return preferredPackage(stemMatches.filter(isGguf), recommended) || + preferredPackage(stemMatches, recommended); + } + + const target = cleanPath(entry.path); + const targetMatches = family.filter((candidate) => + isGguf(candidate) && cleanPath(candidate.target_directory) === target); + if (targetMatches.length) return preferredPackage(targetMatches, recommended); } - return family; + const recommendedMatch = recommended + ? family.find((candidate) => candidate.id === recommended) + : undefined; + if (recommendedMatch) return recommendedMatch; + return preferredPackage(family.filter(isGguf), recommended) || + preferredPackage(family, recommended); } -function relatedExposeAllGgufPackages(entry: CatalogEntry): PackageEntry[] { - const family = packages.filter((candidate) => - candidate.family === entry.family && candidate.format === 'gguf'); - if (!family.length) return []; - if (!entry.download_id) return family; - const exact = family.find((candidate) => candidate.id === entry.download_id); - if (!exact) return relatedPackages(entry); - const matches = family.filter((candidate) => candidate.target_directory === exact.target_directory); - return matches.length ? matches : [exact]; +// Packages that are precision variants of one model share a target directory; +// packages that are different models install into different ones. Grouping on +// that field is what the old hand-maintained family allowlist was approximating. +function relatedPackages(entry: CatalogEntry): PackageEntry[] { + const resolved = resolvedPackage(entry); + if (!resolved) return []; + return familyPackages(entry.family).filter((candidate) => + candidate.target_directory === resolved.target_directory); } +// Every quantised precision, kept in step with the tags in `packageIdSuffix` so +// a newly published quantisation sorts with its peers instead of falling +// through to the unknown bucket behind `orig`. +const quantisedPrecisions = [ + 'q2_k', 'q3_k', 'q4', 'q4_0', 'q4_k', 'q5_0', 'q5_k', 'q6_k', 'q8_0', 'q8' +]; + function exposedPackageRank(entry: PackageEntry, selectedId?: string): number { if (selectedId && entry.id === selectedId) return 0; if (entry.default) return 1; - if (['q4_k', 'q4_0', 'q8_0', 'q8'].includes(entry.precision)) return 2; + if (quantisedPrecisions.includes(entry.precision)) return 2; if (['f16', 'fp16', 'bf16'].includes(entry.precision)) return 3; if (entry.precision === 'f32') return 4; if (entry.precision === 'orig') return 5; return 6; } -function packageLabel(entry: PackageEntry): string { - if (entry.family === 'ace_step') { - const precision = entry.precision === 'bf16' - ? 'BF16' - : ['q8_0', 'q8'].includes(entry.precision) - ? 'Q8' - : entry.precision.toUpperCase(); - if (entry.id.includes('_xl_turbo_')) return `GGUF Turbo XL ${precision}`; - if (entry.id.includes('_xl_sft_')) return `GGUF Turbo XL SFT ${precision}`; - if (entry.id.includes('_turbo_')) return `GGUF Turbo ${precision}`; - return `GGUF ${precision}`; - } +// Derived label, used only when a package has no display_name. It describes the +// build, not the model, so it cannot tell two models in one target directory +// apart — see packageLabel. +function derivedPackageLabel(entry: PackageEntry): string { if (entry.format === 'safetensors') return 'Safetensors'; if (entry.id.includes('int8_dit')) return 'GGUF Q4 ConvRot'; if (entry.precision === 'q4_k' || entry.precision === 'q4_0') return 'GGUF Q4'; @@ -154,6 +183,14 @@ function packageLabel(entry: PackageEntry): string { return `GGUF ${entry.precision.toUpperCase()}`; } +// display_name is unique per package and already distinguishes both the model +// and its precision ("ACE-Step 1.5 XL Turbo BF16 GGUF"), which the derived +// label cannot: a target directory may hold several distinct models, and those +// would otherwise all render as "GGUF Q8". +function packageLabel(entry: PackageEntry): string { + return englishUiText(entry.display_name) || derivedPackageLabel(entry); +} + function packageModelPath(entry: PackageEntry): string { let modelFile: string | undefined; if (entry.format === 'gguf' && entry.family === 'minimax_h3') { @@ -199,60 +236,97 @@ function packageSessionOptions(entry: PackageEntry): Record | un return undefined; } +// The build alone, without the model name or the "GGUF" every exposed package +// shares. Install buttons are around 90px wide, so the full display_name +// overflowed them; the precision is what actually distinguishes the buttons. +function shortPackageLabel(entry: PackageEntry): string { + if (entry.format !== 'gguf') return derivedPackageLabel(entry); + const precision = entry.precision === 'orig' ? 'Original' : entry.precision.toUpperCase(); + return entry.id.includes('int8_dit') ? `${precision} ConvRot` : precision; +} + +const installChoice = (candidate: PackageEntry, shortLabel: string): InstallPackageChoice => ({ + id: candidate.id, + label: packageLabel(candidate), + short_label: shortLabel, + path: packageModelPath(candidate), + format: candidate.format, + precision: candidate.precision, + session_options: packageSessionOptions(candidate) +}); + function installChoices(entry: CatalogEntry): InstallPackageChoice[] { - const exposesAllGguf = exposeAllGgufPackageFamilies.has(entry.family); - const related = exposesAllGguf ? relatedExposeAllGgufPackages(entry) : relatedPackages(entry); - if (entry.family === 'ace_step' || entry.family === 'minimax_music3' || - exposesAllGguf) { - return related - .filter((candidate) => candidate.format === 'gguf') - .sort((left, right) => exposesAllGguf - ? exposedPackageRank(left, entry.download_id) - exposedPackageRank(right, entry.download_id) - : Number(right.default === true) - Number(left.default === true)) - .map((candidate) => ({ - id: candidate.id, - label: packageLabel(candidate), - path: packageModelPath(candidate), - format: candidate.format, - precision: candidate.precision, - session_options: packageSessionOptions(candidate) - })); - } - const q8 = preferredPackage(related.filter((candidate) => - candidate.format === 'gguf' && ['q8_0', 'q8'].includes(candidate.precision))); - const fp16 = preferredPackage(related.filter((candidate) => - candidate.format === 'gguf' && ['f16', 'fp16'].includes(candidate.precision))) || - preferredPackage(related.filter((candidate) => - candidate.format === 'gguf' && candidate.precision === 'bf16')); - const otherGguf = !q8 && !fp16 - ? related.filter((candidate) => candidate.format === 'gguf') - : []; // The native model manager intentionally exposes complete GGUF packages // only. Safetensors packages frequently depend on source-tree sidecars and - // are not yet reliable as one-click UI installs. - return [q8, fp16, ...otherGguf] - .filter((candidate): candidate is PackageEntry => candidate !== undefined) - .map((candidate) => ({ - id: candidate.id, - label: packageLabel(candidate), - path: packageModelPath(candidate), - format: candidate.format, - precision: candidate.precision, - session_options: packageSessionOptions(candidate) - })); + // are not yet reliable as one-click UI installs. Every other build of the + // resolved model is offered: they share a target directory, so switching + // between them is a requantisation rather than a different model. + const exposed = relatedPackages(entry).filter(isGguf); + if (!exposed.length) return []; + // An entry that names a package explicitly gets that package, even when the + // family recommends another build of it: the catalog id is the per-entry + // decision (a deliberate low-VRAM pick, say) and the recommendation is only + // the family-wide default for entries that express no preference. + const explicit = exposed.find((candidate) => candidate.id === entry.download_id); + const selected = explicit || preferredPackage(exposed, recommendedPackageId(entry.family)); + const ordered = exposed + .slice() + .sort((left, right) => + exposedPackageRank(left, selected?.id) - exposedPackageRank(right, selected?.id)); + // Two distinct models can share a target directory, and the short label + // describes the build only, so it cannot tell those apart. Keep the full + // display_name on every button of such a set rather than render one + // ambiguous button. + const labels = distinctLabels(ordered.map(shortPackageLabel)) || + distinctLabels(trimmedPackageLabels(ordered.map(packageLabel))) || + ordered.map(packageLabel); + return ordered.map((candidate, index) => installChoice(candidate, labels[index])); +} + +function distinctLabels(labels: string[]): string[] | undefined { + const seen = new Set(labels.map((value) => value.toLowerCase())); + return seen.size === labels.length && labels.every(Boolean) ? labels : undefined; +} + +// Second try when precisions alone collide, which happens when one target +// directory holds several models ("ACE-Step 1.5 Turbo BF16 GGUF" beside +// "ACE-Step 1.5 XL SFT BF16 GGUF"). Drop the leading words every package of the +// set shares and the trailing format word, leaving what actually differs. +function trimmedPackageLabels(labels: string[]): string[] { + if (labels.length < 2) return labels; + const words = labels.map((label) => label.split(' ').filter(Boolean)); + let shared = 0; + while (words.every((entry) => shared < entry.length - 1 && entry[shared] === words[0][shared])) { + shared++; + } + return words.map((entry) => { + const rest = entry.slice(shared); + if (rest.length > 1 && rest[rest.length - 1].toLowerCase() === 'gguf') rest.pop(); + return rest.join(' '); + }); +} + +// Why a managed entry ended up with no installable package. Returned as display +// text so the entry can stay listed instead of vanishing from the whole UI. +function unavailableReason(entry: CatalogEntry): string { + const family = familyPackages(entry.family); + if (!family.length) { + return `No model_specs package definition for family "${entry.family}".`; + } + return 'Only Safetensors packages are published for this model. The native ' + + 'model manager installs GGUF packages only.'; } -export const catalog = (rawCatalog.models as CatalogEntry[]).flatMap((entry) => { +export const catalog = (rawCatalog.models as CatalogEntry[]).map((entry) => { const choices = installChoices(entry); // A managed catalog entry with no remaining GGUF choice is Safetensors-only - // (or otherwise not installable by the native manager). Do not expose it as - // an apparently available Studio model after Safetensors UI support is - // disabled. Entries without a download id are bundled or locally managed - // and must remain visible. - if (entry.download_id && choices.length === 0) return []; + // (or has no spec yet). Keep it listed and flag it: dropping it here made a + // packaging gap look like the model had never existed. Entries without a + // download id are bundled or locally managed and are never flagged. + const unavailable = Boolean(entry.download_id) && choices.length === 0; const installPackage = choices[0]; const spec = specsByFamily.get(entry.family); - return [{ + return { ...entry, display_name: englishUiText(entry.display_name_en, entry.display_name) || entry.id, input_hint: englishUiText(entry.input_hint_en, entry.input_hint), @@ -264,10 +338,24 @@ export const catalog = (rawCatalog.models as CatalogEntry[]).flatMap((entry) => ?.filter((option) => option.required === true) .map((option) => option.name), builtin_voices: spec?.ui?.builtin_voices, - default_voice: spec?.ui?.default_voice - }]; + default_voice: spec?.ui?.default_voice, + unavailable: unavailable || undefined, + unavailable_reason: unavailable ? unavailableReason(entry) : undefined + }; }); +// The install buttons an entry renders. Every exposed package gets its own +// slot: catalog.ts already decided which packages belong together, so the UI +// must not re-derive that from family names or collapse the list to fixed +// q8/fp16 slots. +export function installPackageSlots(entry: CatalogEntry): InstallPackageSlot[] { + return (entry.install_packages || []).map((choice) => ({ + key: choice.id, + label: choice.short_label, + choice + })); +} + export const parameterCatalog = Object.fromEntries( Object.entries(rawParams as unknown as Record) .filter((entry): entry is [string, ParamSpec[]] => Array.isArray(entry[1])) diff --git a/webui/native/src/lib/i18n.ts b/webui/native/src/lib/i18n.ts index 5a37eef69..f2da739ff 100644 --- a/webui/native/src/lib/i18n.ts +++ b/webui/native/src/lib/i18n.ts @@ -122,6 +122,11 @@ const english: Record = { 'studio.subtitle.music': 'Create music and sound from a prompt, lyrics, or reference audio when supported.', 'studio.subtitle.vc': 'Transform a recording into another voice while preserving the spoken or sung performance.', 'studio.subtitle.sep': 'Split a recording into vocals, instruments, or other available audio stems.', + // The workflow tabs are keyed 'conversion' and 'separation' while the task + // labels above use 'vc' and 'sep'. Both spellings resolve so the hero subtitle + // renders text rather than the lookup key itself. + 'studio.subtitle.conversion': 'Transform a recording into another voice while preserving the spoken or sung performance.', + 'studio.subtitle.separation': 'Split a recording into vocals, instruments, or other available audio stems.', 'studio.subtitle.analysis': 'Analyze audio for speech activity, speakers, timing, and alignment.', 'studio.subtitle.design': 'Create or refine a voice from a written description and supported reference controls.', 'studio.model': 'Model', @@ -227,6 +232,15 @@ const english: Record = { 'result.tracks': 'tracks', 'result.saveWav': 'Save WAV', 'result.empty': 'Generated audio and structured results appear here.', + 'result.rows.segments': 'Segments', + 'result.rows.words': 'Word timings', + 'result.rows.speaker_turns': 'Speaker turns', + 'result.saveSrt': 'Save SRT', + 'result.saveVtt': 'Save VTT', + 'result.start': 'Start', + 'result.end': 'End', + 'result.speaker': 'Speaker', + 'result.content': 'Content', 'models.eyebrow': 'MODEL LIBRARY', 'models.title': 'Local packages', 'models.subtitle': 'Download and manage model packages without leaving the native interface.', diff --git a/webui/native/src/lib/types.ts b/webui/native/src/lib/types.ts index 061ef0977..799965ae4 100644 --- a/webui/native/src/lib/types.ts +++ b/webui/native/src/lib/types.ts @@ -3,12 +3,26 @@ export type StringMap = Record; export interface InstallPackageChoice { id: string; label: string; + // Button text. `label` names the model as well as the build ("IndexTTS2.5 + // Original-Dtype GGUF"), which is wider than an install button; the model + // name is already printed beside the buttons, so the short form keeps the + // build only. It falls back to `label` when that would not be unique. + short_label: string; path: string; format: string; precision: string; session_options?: StringMap; } +// One rendered install button. The catalog decides which packages an entry +// exposes, so the UI renders every slot it is given rather than re-deriving the +// set from family names. +export interface InstallPackageSlot { + key: string; + label: string; + choice: InstallPackageChoice; +} + export interface CatalogEntry { id: string; display_name: string; @@ -29,6 +43,11 @@ export interface CatalogEntry { required_request_options?: string[]; builtin_voices?: string[]; default_voice?: string; + // Set when a managed entry resolves to no installable package. The entry is + // still listed so the gap is visible instead of the model disappearing from + // the UI; `unavailable_reason` explains why it cannot be installed. + unavailable?: boolean; + unavailable_reason?: string; } export interface ParamSpec { diff --git a/webui/native/src/routes/+page.svelte b/webui/native/src/routes/+page.svelte index 777984830..ec0a47ca5 100644 --- a/webui/native/src/routes/+page.svelte +++ b/webui/native/src/routes/+page.svelte @@ -29,7 +29,7 @@ type ModelPackageSize, type DirectoryBrowserResponse } from '$lib/api'; - import { catalog, parameterCatalog, taskLabels } from '$lib/catalog'; + import { catalog, installPackageSlots, parameterCatalog, taskLabels } from '$lib/catalog'; import { createTranslator, resolveUiLanguage, uiLanguages } from '$lib/i18n'; import MediaPreview from '$lib/MediaPreview.svelte'; import { defaultChunkBudget, splitTtsChunks } from '$lib/text'; @@ -72,8 +72,12 @@ let instructions = ''; let lyrics = ''; let duration = 30; - let seed = 1234; - let maxTokens = 1024; + // -1 means "let the engine pick", matching the field label. A fixed default + // here would silently pin every model whose own default is a random seed. + let seed = -1; + // Blank means "use the model's own limit". A shared constant here overrode + // per-model ceilings ranging from 300 to 4096, halving some and doubling others. + let maxTokens: number | '' = ''; let sourceFile: File | null = null; let videoFile: File | null = null; let voiceFile: File | null = null; @@ -91,6 +95,11 @@ let outputArtifacts: Array<{ id: string; url: string; extension: string }> = []; let outputText = ''; let outputJson = ''; + // Timed detail rows returned by ASR, diarization, VAD and forced alignment. + // Spans arrive as sample offsets alongside the rate they were counted in. + type TimedRow = { start: number; end: number; label: string; text: string; confidence: number }; + let outputRows: TimedRow[] = []; + let outputRowKind = ''; let logs: string[] = []; let aborter: AbortController | null = null; let longText = true; @@ -142,14 +151,6 @@ demo_3_woman: 'demo_3_woman', demo_4_woman: 'demo_4_woman' }; - const exposeAllStudioPackageFamilies = new Set([ - 'audiosr', - 'controlfoley', - 'firered_audio', - 'fireredtts3', - 'meanvc2', - 'midashenglm_gen' - ]); function chooseUiLanguage(code: string) { uiLanguage = resolveUiLanguage([code]); @@ -402,18 +403,30 @@ $: usesVibeVoiceSpeakerFiles = selected?.family === 'vibevoice'; $: isQwenBase = selected?.task === 'tts' && selected?.family === 'qwen3_tts' && !selected?.id.includes('custom'); - $: allowsQuickStartVoice = ['tts', 'clon'].includes(selected?.task); + // Voice design also picks a named voice, and any model shipping built-in + // voices should offer them whatever its task. + $: allowsQuickStartVoice = ['tts', 'clon', 'vdes'].includes(selected?.task) || + (selected?.builtin_voices || []).length > 0; $: referenceVoiceRequired = !(allowsQuickStartVoice && quickStartVoice) && ( (['clon', 'vc', 'svc'].includes(selected?.task) && selected?.family !== 'rvc') || isQwenBase); $: lyricsRequired = requiresRequestOption(selected, 'lyrics'); $: referenceTextRequired = requiresRequestOption(selected, 'reference_text') || (Boolean(voiceFile) && isQwenBase); - $: quickStartVoices = server && !server.ui_management - ? configuredVoices + // Voices come from three places: the spec's built-in list, whatever the server + // reports for this model, and the bundled demo clips. The demo clips only make + // sense for models that take an arbitrary reference. + $: demoQuickStartVoices = server && !server.ui_management + ? [] : Object.entries(demoVoiceSources) .filter(([, source]) => bundledVoices.includes(source)) .map(([voice]) => voice); - $: quickStartVoicePreview = quickStartVoice && server?.ui_management !== false + $: quickStartVoices = Array.from(new Set([ + ...(selected?.builtin_voices || []), + ...configuredVoices, + ...demoQuickStartVoices + ])); + $: quickStartVoicePreview = quickStartVoice && server?.ui_management !== false && + demoQuickStartVoices.includes(quickStartVoice) ? voicePreviewUrl(demoVoiceSources[quickStartVoice] || quickStartVoice) : ''; $: showsText = ['tts', 'clon', 'gen', 's2s', 'align', 'vdes'].includes(selected?.task); @@ -518,32 +531,7 @@ return packageIsResident(entry, choice, models) || sizes[choice.id]?.installed === true; } - function studioPackageSlots(entry: CatalogEntry) { - const choices = entry.install_packages || []; - if (entry.family === 'ace_step' || entry.family === 'minimax_music3' || - exposeAllStudioPackageFamilies.has(entry.family)) { - return choices.map((choice) => ({ key: choice.id, label: choice.label, choice })); - } - const q8 = choices.find((choice) => choice.format === 'gguf' && - ['q8', 'q8_0'].includes(choice.precision)); - const fp16 = choices.find((choice) => choice.format === 'gguf' && - ['f16', 'fp16', 'bf16'].includes(choice.precision)); - if (!q8 && !fp16) { - return choices.map((choice) => ({ key: choice.id, label: choice.label, choice })); - } - return [ - { - key: 'q8', - label: q8?.label || 'GGUF Q8', - choice: q8 - }, - { - key: 'fp16', - label: fp16?.label || 'GGUF FP16', - choice: fp16 - }, - ]; - } + const studioPackageSlots = installPackageSlots; function resolveRequestSeed(value: number) { if (!Number.isInteger(value) || value < -1 || value > 0xffffffff) { @@ -705,6 +693,62 @@ return ['tts', 'clon', 'gen', 's2s', 'vdes'].includes(entry.task); } + // Seed has a dedicated input rather than a parameter widget, so the tasks + // that never declared it could not be made reproducible at all. Trust the + // spec where one exists; fall back to the historical task list otherwise. + function timedRowsFromResult(result: Record): { rows: TimedRow[]; kind: string } { + const rate = Number(result.sample_rate) || 0; + const toSeconds = (samples: unknown) => (rate > 0 ? Number(samples) / rate : Number.NaN); + const read = (value: unknown, label: (entry: Record) => string) => + (Array.isArray(value) ? value : []).map((entry: Record) => ({ + start: toSeconds(entry.start_sample), + end: toSeconds(entry.end_sample), + label: label(entry), + text: typeof entry.text === 'string' ? entry.text : '', + confidence: Number(entry.confidence) || 0 + })); + const turns = read(result.speaker_turns, (entry) => String(entry.speaker_id ?? '')); + if (turns.length) return { rows: turns, kind: 'speaker_turns' }; + const words = read(result.words, (entry) => String(entry.word ?? '')); + if (words.length) return { rows: words, kind: 'words' }; + return { rows: read(result.segments, () => ''), kind: 'segments' }; + } + + function formatTimecode(seconds: number, millisecondSeparator: string) { + if (!Number.isFinite(seconds) || seconds < 0) seconds = 0; + const whole = Math.floor(seconds); + const milliseconds = Math.round((seconds - whole) * 1000); + const pad = (value: number, width = 2) => String(value).padStart(width, '0'); + return `${pad(Math.floor(whole / 3600))}:${pad(Math.floor(whole / 60) % 60)}:${pad(whole % 60)}` + + `${millisecondSeparator}${pad(milliseconds, 3)}`; + } + + function subtitleText(rows: TimedRow[], format: 'srt' | 'vtt') { + const separator = format === 'srt' ? ',' : '.'; + const cues = rows.map((row, index) => { + const caption = [row.label, row.text].filter(Boolean).join(': ') || `#${index + 1}`; + const timing = + `${formatTimecode(row.start, separator)} --> ${formatTimecode(row.end, separator)}`; + return format === 'srt' ? `${index + 1}\n${timing}\n${caption}\n` : `${timing}\n${caption}\n`; + }); + return (format === 'vtt' ? 'WEBVTT\n\n' : '') + cues.join('\n'); + } + + function downloadSubtitles(format: 'srt' | 'vtt') { + if (!outputRows.length || !selected) return; + const blob = new Blob([subtitleText(outputRows, format)], { type: 'text/plain;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const anchor = document.createElement('a'); + anchor.href = url; + anchor.download = `${selected.id}-transcript.${format}`; + anchor.click(); + URL.revokeObjectURL(url); + } + + function supportsSeed(entry: CatalogEntry) { + return entry.request_options?.includes('seed') === true; + } + function supportsRequestOption(entry: CatalogEntry, option: string) { // Specs that publish request metadata are authoritative. Older specs // without that metadata keep the legacy UI behavior until migrated. @@ -735,10 +779,13 @@ job: ModelInstallJob | undefined, translate = tr ) { - if (job?.state === 'running') return `${choice.label}…`; - if (job?.state === 'queued') return `${choice.label} ${translate('models.queued')}`; - if (job?.state === 'cancelling') return `${choice.label} ${translate('models.stopping')}`; - return choice.label; + // short_label, not label: the model name is already printed beside the + // buttons and would not fit inside one. The full name stays on the title + // and aria-label. + if (job?.state === 'running') return `${choice.short_label}…`; + if (job?.state === 'queued') return `${choice.short_label} ${translate('models.queued')}`; + if (job?.state === 'cancelling') return `${choice.short_label} ${translate('models.stopping')}`; + return choice.short_label; } function packageSizeLabel( @@ -751,8 +798,10 @@ ? formatBytes(size.size_bytes) : ''; if (size?.installed) { - const version = packageVersionLabel(size, translate); - return `${selected ? translate('models.selected') : translate('models.downloaded')}${version ? ` · ${version}` : ''}${bytes ? ` · ${bytes}` : ''}`; + // The version state lives on the button title now: an update has its own + // corner badge, the other states are not actionable, and the three-part + // line did not fit the button. + return `${selected ? translate('models.selected') : translate('models.downloaded')}${bytes ? ` · ${bytes}` : ''}`; } if (bytes) return bytes; if (size?.state === 'pending') return translate('models.checkingSize'); @@ -922,7 +971,7 @@ applyingModelsFolder = true; warningStatus = ''; errorStatus = ''; - status = useDefault ? 'Restoring the default models folder…' : 'Changing models folder…'; + status = useDefault ? 'Restoring the default models folder…' : 'Changing models folder…'; try { const root = await setModelsRoot(useDefault ? '' : modelsFolderInput.trim()); acceptModelsRoot(root); @@ -1297,6 +1346,8 @@ outputArtifacts = []; outputText = ''; outputJson = ''; + outputRows = []; + outputRowKind = ''; } async function ensureLoaded() { @@ -1405,10 +1456,13 @@ } async function refreshConfiguredVoices() { - if (!selectedId || server?.ui_management !== false) { + if (!selectedId) { configuredVoices = []; return; } + // Ask for this model's own voices regardless of management mode. The server + // returns its configured presets plus any embeddings shipped beside the + // weights, and skipping the call in managed mode made both unreachable. try { configuredVoices = await availableVoices(selectedId); if (quickStartVoice && !configuredVoices.includes(quickStartVoice)) quickStartVoice = ''; @@ -1569,6 +1623,22 @@ if (lyricsRequired && !lyrics.trim()) { throw new StatusWarning(`${selected.display_name_en || selected.display_name} requires lyrics.`); } + if (selected.task === 'vdes' && !instructions.trim()) { + throw new StatusWarning( + `${selected.display_name_en || selected.display_name} requires a voice description.`); + } + if (selected.task === 'align') { + // Forced aligners need both halves; the engine throws for either, and a + // form message is a better place to learn that than a raw engine error. + if (!text.trim()) { + throw new StatusWarning( + `${selected.display_name_en || selected.display_name} requires the transcript to align.`); + } + if (!language.trim()) { + throw new StatusWarning( + `${selected.display_name_en || selected.display_name} requires a transcript language.`); + } + } await ensureLoaded(); const options = requestOptions(); if (usesVibeVoiceSpeakerFiles) { @@ -1601,7 +1671,7 @@ seed: chunkSeed(resolvedSeed, index), options }; - if (supportsMaxTokens(selected)) body.max_tokens = maxTokens; + if (supportsMaxTokens(selected) && maxTokens !== '') body.max_tokens = maxTokens; if (voiceRef) body.voice_ref = voiceRef; else if (quickStartVoice) body.voice = demoVoiceSources[quickStartVoice] || quickStartVoice; else if (selected.default_voice) body.voice = selected.default_voice; @@ -1637,6 +1707,7 @@ options }, aborter.signal); outputText = String(result.text || ''); + ({ rows: outputRows, kind: outputRowKind } = timedRowsFromResult(result)); outputJson = JSON.stringify(result, null, 2); } else { if (needsSource && !audio) throw new StatusWarning('Choose a source audio file.'); @@ -1650,10 +1721,15 @@ else request.duration_seconds = duration; } request.seed = resolvedSeed; - if (supportsMaxTokens(selected)) request.max_tokens = maxTokens; + if (supportsMaxTokens(selected) && maxTokens !== '') request.max_tokens = maxTokens; } else if (selected.task === 's2s') { request.seed = resolvedSeed; - if (supportsMaxTokens(selected)) request.max_tokens = maxTokens; + if (supportsMaxTokens(selected) && maxTokens !== '') request.max_tokens = maxTokens; + } else if (supportsSeed(selected)) { + // Voice conversion and the analysis tasks declare a seed but were never + // sent one, so their engines fell back to a fresh random value on every + // run and identical inputs could not be reproduced. + request.seed = resolvedSeed; } if (audio) request.audio = audio; if (voiceRef) request.voice_ref = voiceRef; @@ -1681,6 +1757,7 @@ })); } outputText = typeof result.text === 'string' ? result.text : ''; + ({ rows: outputRows, kind: outputRowKind } = timedRowsFromResult(result)); outputJson = JSON.stringify(result, (key, value) => (key === 'audio' || key === 'payload') && typeof value === 'string' ? `` : value, 2); @@ -2134,8 +2211,9 @@ disabled={loadingModel || !available} title={resident ? `Unload ${choice?.label}` : available ? `Load ${choice?.label}` : `${choice?.label || slot.label} is not downloaded`} + aria-label={choice?.label || slot.label} on:click={() => choice && toggleStudioPackage(choice)}> - {choice?.label || slot.label} + {choice?.short_label || slot.label} {/each} @@ -2181,7 +2259,7 @@ {/if} - {#if selected.task === 'gen'} + {#if selected.task === 'gen' && supportsRequestOption(selected, 'lyrics') && !isFireRedAudioEdit} @@ -2213,7 +2291,7 @@ {/if} - {#if ['tts', 'clon', 'gen', 's2s', 'vdes'].includes(selected.task)} + {#if ['tts', 'clon', 'gen', 's2s', 'vdes'].includes(selected.task) || supportsSeed(selected)}
@@ -2225,7 +2303,7 @@
{/if} - {#if selected.task === 'gen'} + {#if selected.task === 'gen' && !isFireRedAudioEdit}

{tr('result.empty')}

{/if} {#if outputText}{/if} + {#if outputRows.length} +
+
+ {tr(`result.rows.${outputRowKind}`)} + + +
+
+ + + + + + + + + + {#each outputRows as row} + + + + + + {/each} + +
{tr('result.start')}{tr('result.end')}{outputRowKind === 'speaker_turns' ? tr('result.speaker') : tr('result.content')}
{formatTimecode(row.start, '.')}{formatTimecode(row.end, '.')}{[row.label, row.text].filter(Boolean).join(': ')}
+
+
+ {/if} {#if outputJson}
{outputJson}
{/if} @@ -2570,7 +2677,10 @@ aria-pressed={packageIsSelected(entry, choice)} disabled={groupInstallBusy(group, installJobs) || (packageSizeState === 'running' && Object.keys(packageSizes).length === 0)} - title={`${choice.format.toUpperCase()} ${choice.precision}: ${resolveCatalogPath(choice.path)}`} + title={`${choice.label} — ${choice.format.toUpperCase()} ${choice.precision}${ + packageVersionLabel(packageSizes[choice.id]) ? ` · ${packageVersionLabel(packageSizes[choice.id])}` : '' + }: ${resolveCatalogPath(choice.path)}`} + aria-label={choice.label} on:click={() => useOrInstallPackage(entry, choice)}> {installButtonLabel(choice, installJobs[choice.id], tr)} {#if packageSizeLabel(packageSizes[choice.id], packageSizeState, diff --git a/webui/native/src/routes/Arena.svelte b/webui/native/src/routes/Arena.svelte index 4a7a930d4..c29e409f0 100644 --- a/webui/native/src/routes/Arena.svelte +++ b/webui/native/src/routes/Arena.svelte @@ -20,7 +20,7 @@ export let loadedModels: LoadedModel[] = []; export let server: ServerHealth | null = null; export let modelsFolder = ''; - export let maxTokens = 1024; + export let maxTokens: number | '' = ''; export let entrySelectable: (entry: CatalogEntry) => boolean = () => true; export let studioPackageSlots: (entry: CatalogEntry) => Array<{ key: string; label: string; choice?: InstallPackageChoice }> = () => []; @@ -483,7 +483,7 @@ seed: resolveRequestSeed(arenaSeed), options }; - if (supportsMaxTokens(entry)) body.max_tokens = maxTokens; + if (supportsMaxTokens(entry) && maxTokens !== '') body.max_tokens = maxTokens; if (voiceRef) body.voice_ref = voiceRef; else if (builtinVoice) body.voice = builtinVoice; else if (entry.default_voice) body.voice = entry.default_voice; From cc5c5102f353ed44f6b9170a270cef1abcae1804 Mon Sep 17 00:00:00 2001 From: Warren B Date: Mon, 31 Aug 2026 01:07:10 +0100 Subject: [PATCH 3/5] webui: give every model its real parameter surface The parameter file covered 42 of the catalogued families and got several of them wrong. It now covers 63 groups and 414 controls, each default pinned to the C++ that reads it. Controls that did nothing - index_tts2 emitted "lang"; the engine reads only "language". This is the multilingual selector on the model whose headline feature is multilingual cloning, and it had never worked. - miotts emitted "best_of_n"; the engine reads "miotts.best_of_n". - firered-audio-tts exposed top_k, top_p and temperature, which that family reads only on its understanding path, never on generation. Defaults that overrode the model Every default in this file is sent on every request, so a value here is not a fallback, it is an override. - vibevoice num_inference_steps was 10 while the shipped GGUF carries ddpm_num_inference_steps 20, so the UI silently halved diffusion quality. The default is now omitted rather than corrected: the value is read per package, and any literal would override it again. - firered-audio-asr max_new_tokens was 512 against an engine default of 300, and sending it at all defeated the model's own bump to 1024 when enable_thinking is set. Omitted for the same reason. - vevo2 temperature and top_k are read from the checkpoint's own generation config; the literals here overrode them. - personaplex text_temperature and text_top_k are documented to follow temperature and top_k when unset, which pinning them prevented. - fireredtts3 voice design carried the instruct-path guidance_scale of 2.0 instead of its own tuned 1.2. - ace_step shift was 3.0, a value the engine applies only on the extract route. - Deleted the seed entries: the server overwrites options["seed"] from the top-level field, so they never had any effect. Ranges that permitted hard errors Nine minimums sat below the engine's guard, so a legal-looking slider position produced a 500: dramabox durations, confucius4_tts temperature and top_p, qwen3_tts temperature, midashenglm_gen min_stop_step, controlfoley duration_sec and guidance_scale, vevo2 and personaplex temperature. echo_tts num_inference_steps had a maximum equal to its own default, so the control could only reduce quality. Values that do not exist ace_step offered a "remix" route. The engine defines seven routes and throws on anything else; "remix" appears nowhere in the tree. Removed, along with the five parameters that existed only to serve it, none of which is read. Missing controls 22 families had no group at all, so their entire option surface was reachable only by hand-writing JSON -- including silero_vad's threshold and min-speech/min-silence knobs, which are the point of that model, kroko_asr's beam search and hotword biasing, muscriptor's output format, and the whole control surface of dots_tts, outetts, glm_tts and fish_audio. Added, along with quality-critical options missing from existing groups: omnivoice's reference_max_seconds, irodori_tts's seven guidance knobs, index_tts2's sampling block, chatterbox's min_p (its actual truncation filter; top_p defaults to a documented no-op), qwen3_tts's sub-talker block, minimax_music3's ensemble takes, and seed_vc's pitch conditioning without which the SVC entry cannot do what SVC is for. Also renames seed_vc's three *_cfg_rate controls to the *_guidance_scale names its spec declares; they worked only through a deprecated alias table that throws if both spellings arrive. The file's own comment claimed only user-modified values are sent, which has not been true in this UI. It now documents the real behaviour, including that omitting a default is the way to say "unset", because the option parser skips empty values. Validation: python3 -m json.tool webui/configs/model_params.json python3 tools/check_loader_catalog_sync.py # ok, in sync cd webui/native && npm run build Backend tested: Metal (Apple M4 Max). Verified against a live server that a request carrying empty-string parameter sentinels renders normally. Known limitations: defaults were read from the engine source rather than observed per model, since most of these families have no package installed here. Controls were deliberately left out where a value is a sentinel rather than a setting (silero_vad neg_threshold and max_speech_duration_s), where the option is session-scoped and would be rejected as a request option (soprano_tts text_chunk_size), where the legal values are discovered from the package (nemotron_asr lookahead_tokens), and for file-path and raw-tensor inputs. The five families that read no request options keep no group. --- webui/configs/model_params.json | 426 +++++++++++++++++++++++++------- webui/native/dist/index.html | 18 +- 2 files changed, 341 insertions(+), 103 deletions(-) diff --git a/webui/configs/model_params.json b/webui/configs/model_params.json index 68890fdfa..33b69d601 100644 --- a/webui/configs/model_params.json +++ b/webui/configs/model_params.json @@ -1,48 +1,59 @@ { "echo_tts": [ - {"name": "num_inference_steps", "type": "slider", "label": "num_inference_steps", "label_en": "Sampling steps", "default": 40, "minimum": 8, "maximum": 40, "step": 1, "precision": 0, "info": "Euler sampler steps."}, + {"name": "num_inference_steps", "type": "slider", "label": "num_inference_steps", "label_en": "Sampling steps", "default": 40, "minimum": 8, "maximum": 120, "step": 1, "precision": 0, "info": "Euler sampler steps.", "info_en": "Euler sampler steps. No upper bound in the model; higher is slower and steadier."}, {"name": "text_guidance_scale", "type": "slider", "label": "text_guidance_scale", "label_en": "Text guidance", "default": 3.0, "minimum": 0.0, "maximum": 10.0, "step": 0.1}, {"name": "speaker_guidance_scale", "type": "slider", "label": "speaker_guidance_scale", "label_en": "Speaker guidance", "default": 8.0, "minimum": 0.0, "maximum": 15.0, "step": 0.1}, {"name": "truncation_factor", "type": "slider", "label": "truncation_factor", "label_en": "Noise truncation", "default": 0.8, "minimum": 0.0, "maximum": 1.0, "step": 0.05}, {"name": "guidance_interval", "type": "slider", "label": "guidance_interval", "label_en": "Guidance interval", "default": 1, "minimum": 1, "maximum": 3, "step": 1, "precision": 0, "info": "Refresh the unconditional CFG lanes every Nth guided step. Higher is faster and works best with more steps; 1 is highest fidelity."}, - {"name": "reference_duration_sec", "type": "slider", "label": "reference_duration_sec", "label_en": "Reference trim (s)", "default": 15.0, "minimum": 1.0, "maximum": 60.0, "step": 1.0, "info": "Trim the speaker reference before encoding. Around 10 s usually clones best."}, - {"name": "seed", "type": "number", "label": "seed", "label_en": "Seed", "default": 0, "minimum": 0, "step": 1, "precision": 0} + {"name": "reference_duration_sec", "type": "slider", "label": "reference_duration_sec", "label_en": "Reference trim (s)", "default": 15.0, "minimum": 1.0, "maximum": 60.0, "step": 1.0, "info": "Trim the speaker reference before encoding. Around 10 s usually clones best."} ], - "_comment": "WebUI TTS 高级参数控件配置:按模型 family 动态生成控件(gr.render)。每项字段:name=选项键(随请求 options 透传给模型);type=slider|number|bool|text|choice;label/info=显示文案;default=默认值(应等于模型默认,已按 src/models//*.cpp 校对);minimum/maximum/step=数值范围;precision=0 表示整数;choices=下拉候选。规则:只有被用户改动过的控件值才会随请求发送;seed/max_tokens 已有专用输入框,勿在此重复;参考文本用『参考文本』框(reference_text);文件路径/parity 类参数(如 *_noise_file)未纳入,可用『其它参数(JSON)』兜底框传。", + + "_comment": "WebUI 模型参数控件配置:按 catalog 条目 id 或 family 生成控件。每项字段:name=选项键(随请求 options 透传给模型);type=slider|number|bool|text|choice;label/label_en、info/info_en、placeholder/placeholder_en=显示文案;default=默认值;minimum/maximum/step=数值范围;precision=0 表示整数;choices=下拉候选。 BEHAVIOUR: the native UI seeds every control from its `default` and sends the whole set with every request (webui/native/src/routes/+page.svelte resetParams/requestOptions), so a `default` here is an UNCONDITIONAL OVERRIDE of the model's own default, never a fallback. Every default must therefore be read from the C++ (src/models//, src/community_models//) or from the packaged config the C++ derives it from. To let the model or its checkpoint decide, OMIT `default`: the control then sends \"\", and runtime::find_option_match (src/framework/runtime/options.cpp:151) ignores empty values, so the option counts as unset. Use type=number/text for those so the widget renders blank. `minimum` must not sit below the C++ guard - a slider that can reach an illegal value produces a 500. KEYS: ~27 families call runtime::validate_spec_backed_request_options (include/engine/framework/runtime/spec_backed_model.h:59) and hard-reject any key absent from model_specs/.json options.request - the KEY is checked even when the value is empty. Spec min/max are documentation only and are never enforced. L10N: catalog.ts discards any label/info/placeholder containing Han characters, so every entry needs the _en variant or it renders as raw snake_case. 勿在此重复:seed/max_tokens 已有专用输入框(服务端用顶层 seed 覆盖 options[\"seed\"],app/server/runtime.cpp:1876);参考文本用『参考文本』框(reference_text);语种用顶层 language 框。文件路径/说话人向量/parity 类参数(如 *_noise_file、speaker_embedding、multi_reference_cond)未纳入,可用『其它参数(JSON)』兜底框传。", "qwen3_tts": [ - {"name": "temperature", "type": "slider", "label": "temperature", "default": 0.9, "minimum": 0.0, "maximum": 2.0, "step": 0.05}, + {"name": "temperature", "type": "slider", "label": "temperature", "default": 0.9, "minimum": 0.05, "maximum": 2.0, "step": 0.05, "info_en": "Must be above 0 while do_sample is on."}, {"name": "top_k", "type": "number", "label": "top_k", "default": 50, "minimum": 0, "step": 1, "precision": 0}, {"name": "top_p", "type": "slider", "label": "top_p", "default": 1.0, "minimum": 0.0, "maximum": 1.0, "step": 0.01}, {"name": "repetition_penalty", "type": "slider", "label": "repetition_penalty", "default": 1.05, "minimum": 1.0, "maximum": 2.0, "step": 0.01}, {"name": "do_sample", "type": "bool", "label": "do_sample", "default": true}, - {"name": "instruct", "type": "text", "label": "instruct(仅 VoiceDesign/CustomVoice)", "default": "", "placeholder": "风格/音色指令,Base 版忽略"}, - {"name": "speaker", "type": "text", "label": "speaker(仅 CustomVoice)", "default": "", "placeholder": "内置音色名,其它版忽略"} + {"name": "subtalker_do_sample", "type": "bool", "label": "subtalker_do_sample", "label_en": "Sub-talker: sample", "default": true}, + {"name": "subtalker_temperature", "type": "slider", "label": "subtalker_temperature", "label_en": "Sub-talker: temperature", "default": 0.9, "minimum": 0.05, "maximum": 2.0, "step": 0.05, "info_en": "Must be above 0 while subtalker_do_sample is on."}, + {"name": "subtalker_top_k", "type": "number", "label": "subtalker_top_k", "label_en": "Sub-talker: top-k", "default": 50, "minimum": 0, "step": 1, "precision": 0}, + {"name": "subtalker_top_p", "type": "slider", "label": "subtalker_top_p", "label_en": "Sub-talker: top-p", "default": 1.0, "minimum": 0.0, "maximum": 1.0, "step": 0.01}, + {"name": "x_vector_only_mode", "type": "bool", "label": "x_vector_only_mode(仅 VoiceClone)", "label_en": "x_vector_only_mode (VoiceClone only)", "default": false, "info_en": "Condition on the speaker embedding alone instead of in-context reference audio. Ignored by other variants."}, + {"name": "instruct", "type": "text", "label": "instruct(仅 VoiceDesign/CustomVoice)", "label_en": "instruct (VoiceDesign / CustomVoice only)", "default": "", "placeholder": "风格/音色指令,Base 版忽略", "placeholder_en": "Style or timbre instruction; ignored by Base models"}, + {"name": "speaker", "type": "text", "label": "speaker(仅 CustomVoice)", "label_en": "speaker (CustomVoice only)", "default": "", "placeholder": "内置音色名,其它版忽略", "placeholder_en": "Built-in voice name; ignored by other variants"} ], "vibevoice": [ - {"name": "num_inference_steps", "type": "number", "label": "num_inference_steps", "default": 10, "minimum": 1, "step": 1, "precision": 0, "info": "扩散步数(官方默认 10),越大越慢越稳"}, - {"name": "guidance_scale", "type": "slider", "label": "guidance_scale", "default": 1.3, "minimum": 0.0, "maximum": 5.0, "step": 0.1, "info": "CFG 引导强度"}, - {"name": "max_length_times", "type": "number", "label": "max_length_times", "default": 2.0, "minimum": 0.1, "step": 0.1, "info": "最大输出长度倍数"}, + {"name": "num_inference_steps", "type": "number", "label": "num_inference_steps", "minimum": 1, "step": 1, "precision": 0, "info": "留空=用模型自带的 ddpm_num_inference_steps(随包,7B 为 20)", "info_en": "Blank uses the packaged model's own ddpm_num_inference_steps (20 in the shipped 7B). Setting a value overrides it for every request."}, + {"name": "guidance_scale", "type": "slider", "label": "guidance_scale", "default": 1.3, "minimum": 0.0, "maximum": 5.0, "step": 0.1, "info": "CFG 引导强度", "info_en": "Classifier-free guidance strength."}, + {"name": "max_length_times", "type": "number", "label": "max_length_times", "default": 2.0, "minimum": 0.1, "step": 0.1, "info": "最大输出长度倍数", "info_en": "Cap on output length as a multiple of the estimated token count."}, {"name": "temperature", "type": "slider", "label": "temperature", "default": 1.0, "minimum": 0.05, "maximum": 2.0, "step": 0.05}, {"name": "top_p", "type": "slider", "label": "top_p", "default": 1.0, "minimum": 0.05, "maximum": 1.0, "step": 0.01}, + {"name": "top_k", "type": "number", "label": "top_k", "label_en": "Top-k", "default": 50, "minimum": 0, "step": 1, "precision": 0, "info_en": "Must be non-negative; 0 disables top-k."}, {"name": "do_sample", "type": "bool", "label": "do_sample", "default": false}, - {"name": "voice_samples", "type": "text", "label": "voice_samples(多说话人,逗号分隔 wav,≤4)", "default": "", "placeholder": "D:/a.wav,D:/b.wav — 用此项时勿再上传参考音色"} + {"name": "voice_samples", "type": "text", "label": "voice_samples(多说话人,逗号分隔 wav,≤4)", "label_en": "voice_samples (multi-speaker: comma-separated WAVs, max 4)", "default": "", "placeholder": "D:/a.wav,D:/b.wav — 用此项时勿再上传参考音色", "placeholder_en": "/a.wav,/b.wav - do not also upload a reference voice when using this"} ], "voxcpm2": [ - {"name": "num_inference_steps", "type": "number", "label": "num_inference_steps", "default": 10, "minimum": 1, "step": 1, "precision": 0, "info": "CFM/DiT 步数"}, + {"name": "num_inference_steps", "type": "number", "label": "num_inference_steps", "default": 10, "minimum": 1, "step": 1, "precision": 0, "info": "CFM/DiT 步数", "info_en": "CFM/DiT sampling steps."}, {"name": "guidance_scale", "type": "slider", "label": "guidance_scale", "default": 2.0, "minimum": 0.0, "maximum": 5.0, "step": 0.1}, {"name": "text_chunk_mode", "type": "choice", "label": "text_chunk_mode", "default": "tag_aware", "choices": ["default", "tag_aware", "japanese", "endline"]}, {"name": "min_tokens", "type": "number", "label": "min_tokens", "default": 2, "minimum": 0, "step": 1, "precision": 0}, - {"name": "retry_badcase", "type": "bool", "label": "retry_badcase(自动重试异常输出)", "default": true} + {"name": "retry_badcase", "type": "bool", "label": "retry_badcase(自动重试异常输出)", "label_en": "retry_badcase (auto-retry bad generations)", "default": true}, + {"name": "retry_badcase_max_times", "type": "number", "label": "retry_badcase_max_times(最大重试次数)", "label_en": "Retry attempts", "default": 3, "minimum": 1, "step": 1, "precision": 0, "info_en": "Must be positive."}, + {"name": "retry_badcase_ratio_threshold", "type": "number", "label": "retry_badcase_ratio_threshold(时长/字数比阈值)", "label_en": "Retry length-ratio threshold", "default": 6.0, "minimum": 0.1, "step": 0.5, "info_en": "Audio-seconds-per-character ratio above which a take is treated as a bad case. Must be positive."} ], "voxcpm1": [ - {"name": "num_inference_steps", "type": "number", "label": "num_inference_steps", "default": 10, "minimum": 1, "step": 1, "precision": 0, "info": "CFM/DiT 步数"}, + {"name": "num_inference_steps", "type": "number", "label": "num_inference_steps", "default": 10, "minimum": 1, "step": 1, "precision": 0, "info": "CFM/DiT 步数", "info_en": "CFM/DiT sampling steps."}, {"name": "guidance_scale", "type": "slider", "label": "guidance_scale", "default": 2.0, "minimum": 0.0, "maximum": 5.0, "step": 0.1}, + {"name": "text_chunk_mode", "type": "choice", "label": "text_chunk_mode", "label_en": "Text chunk mode", "default": "tag_aware", "choices": ["default", "tag_aware", "japanese", "endline"]}, {"name": "min_tokens", "type": "number", "label": "min_tokens", "default": 2, "minimum": 0, "step": 1, "precision": 0}, - {"name": "retry_badcase", "type": "bool", "label": "retry_badcase(自动重试异常输出)", "default": true} + {"name": "retry_badcase", "type": "bool", "label": "retry_badcase(自动重试异常输出)", "label_en": "retry_badcase (auto-retry bad generations)", "default": true}, + {"name": "retry_badcase_max_times", "type": "number", "label": "retry_badcase_max_times(最大重试次数)", "label_en": "Retry attempts", "default": 3, "minimum": 1, "step": 1, "precision": 0, "info_en": "Must be positive."}, + {"name": "retry_badcase_ratio_threshold", "type": "number", "label": "retry_badcase_ratio_threshold(时长/字数比阈值)", "label_en": "Retry length-ratio threshold", "default": 6.0, "minimum": 0.1, "step": 0.5, "info_en": "Audio-seconds-per-character ratio above which a take is treated as a bad case. Must be positive."} ], "miotts": [ @@ -50,14 +61,17 @@ {"name": "top_k", "type": "number", "label": "top_k", "default": 50, "minimum": 0, "step": 1, "precision": 0}, {"name": "top_p", "type": "slider", "label": "top_p", "default": 1.0, "minimum": 0.0, "maximum": 1.0, "step": 0.01}, {"name": "repetition_penalty", "type": "slider", "label": "repetition_penalty", "default": 1.0, "minimum": 1.0, "maximum": 1.5, "step": 0.01}, - {"name": "best_of_n", "type": "number", "label": "best_of_n(候选数,>1 自动开启)", "default": 1, "minimum": 1, "maximum": 8, "step": 1, "precision": 0} + {"name": "miotts.best_of_n", "type": "number", "label": "miotts.best_of_n(候选数,>1 自动开启)", "label_en": "best_of_n (candidate count; >1 enables best-of-N selection)", "default": 1, "minimum": 1, "maximum": 8, "step": 1, "precision": 0} ], "chatterbox": [ - {"name": "exaggeration", "type": "slider", "label": "exaggeration", "default": 0.5, "minimum": 0.0, "maximum": 2.0, "step": 0.05, "info": "改动后需重新『加载模型』才生效"}, - {"name": "guidance_scale", "type": "slider", "label": "guidance_scale", "default": 0.5, "minimum": 0.0, "maximum": 1.0, "step": 0.05, "info": "改动后需重新『加载模型』才生效"}, + {"name": "exaggeration", "type": "slider", "label": "exaggeration", "default": 0.5, "minimum": 0.0, "maximum": 2.0, "step": 0.05, "info_en": "Applied at model load; reload the model after changing it."}, + {"name": "guidance_scale", "type": "slider", "label": "guidance_scale", "default": 0.5, "minimum": 0.0, "maximum": 1.0, "step": 0.05, "info_en": "Applied at model load; reload the model after changing it."}, {"name": "temperature", "type": "slider", "label": "temperature", "default": 0.8, "minimum": 0.0, "maximum": 2.0, "step": 0.05}, - {"name": "repetition_penalty", "type": "slider", "label": "repetition_penalty", "default": 1.2, "minimum": 1.0, "maximum": 2.0, "step": 0.01} + {"name": "repetition_penalty", "type": "slider", "label": "repetition_penalty", "default": 1.2, "minimum": 1.0, "maximum": 2.0, "step": 0.01}, + {"name": "min_p", "type": "slider", "label": "min_p", "label_en": "Min-p", "default": 0.05, "minimum": 0.0, "maximum": 1.0, "step": 0.01, "info_en": "Chatterbox's real truncation filter: tokens below max_prob * min_p are masked. top_p defaults to 1.0 and is a deliberate no-op."}, + {"name": "top_p", "type": "slider", "label": "top_p", "label_en": "Top-p", "default": 1.0, "minimum": 0.0, "maximum": 1.0, "step": 0.01, "info_en": "Inert at 1.0 by design; lower it only if you also raise min_p out of the way."}, + {"name": "s3gen_cfg_rate", "type": "slider", "label": "s3gen_cfg_rate", "label_en": "S3Gen guidance", "default": 0.7, "minimum": 0.0, "maximum": 2.0, "step": 0.05} ], "chatterbox-vc": [ @@ -69,23 +83,38 @@ {"name": "num_inference_steps", "type": "number", "label": "num_inference_steps", "default": 32, "minimum": 1, "step": 1, "precision": 0}, {"name": "guidance_scale", "type": "slider", "label": "guidance_scale", "default": 2.0, "minimum": 0.0, "maximum": 5.0, "step": 0.1}, {"name": "speed", "type": "slider", "label": "speed", "default": 1.0, "minimum": 0.5, "maximum": 2.0, "step": 0.05}, - {"name": "instruct", "type": "text", "label": "instruct(风格/音色指令)", "default": "", "placeholder": "如:以轻快的语气朗读"} + {"name": "instruct", "type": "text", "label": "instruct(风格/音色指令)", "label_en": "instruct (style / voice instruction)", "default": "", "placeholder": "如:以轻快的语气朗读", "placeholder_en": "e.g. read this in a bright, upbeat tone"}, + {"name": "reference_max_seconds", "type": "number", "label": "reference_max_seconds(参考音频最长秒数)", "label_en": "Reference cap (s)", "default": 15.0, "minimum": 0.0, "step": 1.0, "info": "参考超过约 15 s 后音色克隆明显劣化;0=不限制", "info_en": "Cloning quality collapses past roughly 15 s of reference speech; the clip is cut back to the last pause inside this window. 0 disables the limit."}, + {"name": "reference_pad_ms", "type": "number", "label": "reference_pad_ms", "label_en": "Reference silence pad (ms)", "default": 150, "minimum": 0, "step": 10, "precision": 0, "info_en": "Digital silence written onto both ends of a trimmed reference. Must be non-negative."}, + {"name": "t_shift", "type": "slider", "label": "t_shift", "label_en": "Timestep shift", "default": 0.1, "minimum": 0.0, "maximum": 1.0, "step": 0.01}, + {"name": "class_temperature", "type": "slider", "label": "class_temperature", "label_en": "Class temperature", "default": 0.0, "minimum": 0.0, "maximum": 2.0, "step": 0.05, "info_en": "0 picks the argmax token; above 0 samples the codebook class."}, + {"name": "position_temperature", "type": "slider", "label": "position_temperature", "label_en": "Position temperature", "default": 5.0, "minimum": 0.0, "maximum": 20.0, "step": 0.5, "info_en": "0 disables Gumbel sampling of the unmasking order."}, + {"name": "layer_penalty_factor", "type": "slider", "label": "layer_penalty_factor", "label_en": "Layer penalty", "default": 5.0, "minimum": 0.0, "maximum": 20.0, "step": 0.5, "info_en": "Score penalty per codebook layer when choosing which position to unmask next."}, + {"name": "denoise", "type": "bool", "label": "denoise", "label_en": "Denoise reference", "default": true}, + {"name": "preprocess_prompt", "type": "bool", "label": "preprocess_prompt", "label_en": "Preprocess reference", "default": true}, + {"name": "postprocess_output", "type": "bool", "label": "postprocess_output", "label_en": "Postprocess output", "default": true}, + {"name": "audio_chunk_threshold", "type": "number", "label": "audio_chunk_threshold", "label_en": "Long-form chunk threshold (s)", "default": 30.0, "minimum": 0.0, "step": 1.0}, + {"name": "audio_chunk_duration", "type": "number", "label": "audio_chunk_duration", "label_en": "Long-form chunk length (s)", "default": 15.0, "minimum": 0.0, "step": 1.0}, + {"name": "text_chunk_mode", "type": "choice", "label": "text_chunk_mode", "label_en": "Text chunk mode", "default": "tag_aware", "choices": ["default", "tag_aware", "japanese", "endline"]} ], "sense_asr": [ {"name": "enable_itn", "type": "bool", "label": "enable_itn(逆文本规范化)", "label_en": "enable_itn", "default": true}, {"name": "keep_tags", "type": "bool", "label": "keep_tags(保留语言/情绪/事件标签)", "label_en": "keep_tags", "default": false}, - {"name": "audio_chunk_mode", "type": "choice", "label": "audio_chunk_mode", "default": "auto", "choices": ["auto", "fixed", "none"]}, - {"name": "audio_chunk_duration_sec", "type": "number", "label": "audio_chunk_duration_sec", "default": 30, "minimum": 0.001, "step": 1} + {"name": "audio_chunk_mode", "type": "choice", "label": "audio_chunk_mode", "label_en": "Audio chunk mode", "default": "auto", "choices": ["auto", "fixed", "vad", "none"], "info_en": "vad segments on detected speech; auto and vad share the same code path."}, + {"name": "audio_chunk_duration_sec", "type": "number", "label": "audio_chunk_duration_sec", "label_en": "Audio chunk length (s)", "default": 30, "minimum": 0.001, "step": 1} ], "pocket_tts": [ - {"name": "frames_after_eos", "type": "number", "label": "frames_after_eos(-1=自动)", "default": -1, "minimum": -1, "step": 1, "precision": 0} + {"name": "frames_after_eos", "type": "number", "label": "frames_after_eos(-1=自动)", "label_en": "frames_after_eos (-1 = automatic)", "default": -1, "minimum": -1, "step": 1, "precision": 0} ], "neutts": [ {"name": "voice_id", "type": "choice", "label": "voice_id(内置音色)", "label_en": "voice_id (built-in voice)", "default": "emily", "choices": ["dave", "emily", "greta", "jo", "juliette", "mateo", "paul", "sophie", "steven"]}, - {"name": "emotion", "type": "choice", "label": "emotion(情绪)", "label_en": "emotion", "default": "neutral", "choices": ["angry", "disgusted", "sad", "happy", "fearful", "neutral", "surprised"]} + {"name": "emotion", "type": "choice", "label": "emotion(情绪)", "label_en": "emotion", "default": "neutral", "choices": ["angry", "disgusted", "sad", "happy", "fearful", "neutral", "surprised"]}, + {"name": "temperature", "type": "slider", "label": "temperature", "label_en": "Temperature", "default": 1.0, "minimum": 0.05, "maximum": 2.0, "step": 0.05, "info_en": "Must be positive."}, + {"name": "top_k", "type": "number", "label": "top_k", "label_en": "Top-k", "default": 50, "minimum": 1, "step": 1, "precision": 0, "info_en": "Must be positive."}, + {"name": "min_tokens", "type": "number", "label": "min_tokens", "label_en": "Min speech tokens", "default": 50, "minimum": 0, "step": 1, "precision": 0, "info_en": "Minimum tokens generated before an end-of-speech token may stop decoding."} ], "magpie_tts": [ @@ -111,16 +140,16 @@ {"name": "guidance_scale", "type": "slider", "label": "guidance_scale", "default": 2.5, "minimum": 0.0, "maximum": 8.0, "step": 0.1}, {"name": "spatio_temporal_guidance_scale", "type": "slider", "label": "spatio_temporal_guidance_scale", "default": 1.5, "minimum": 0.0, "maximum": 5.0, "step": 0.1}, {"name": "duration_scale", "type": "slider", "label": "duration_scale(自动估时倍率)", "label_en": "duration_scale", "default": 1.1, "minimum": 0.5, "maximum": 2.0, "step": 0.05}, - {"name": "reference_duration_sec", "type": "number", "label": "reference_duration_sec(参考音频裁剪/重复秒数)", "label_en": "reference_duration_sec", "default": 10.0, "minimum": 0.0, "step": 0.5}, - {"name": "guidance_rescale", "type": "text", "label": "guidance_rescale", "default": "auto", "placeholder": "auto 或数值"}, - {"name": "audio_chunk_threshold_sec", "type": "number", "label": "audio_chunk_threshold_sec(长文本阈值)", "label_en": "audio_chunk_threshold_sec", "default": 45.0, "minimum": 0.0, "step": 1.0}, - {"name": "audio_chunk_duration_sec", "type": "number", "label": "audio_chunk_duration_sec(长文本分段目标时长)", "label_en": "audio_chunk_duration_sec", "default": 37.0, "minimum": 0.0, "step": 1.0}, + {"name": "reference_duration_sec", "type": "number", "label": "reference_duration_sec(参考音频裁剪/重复秒数)", "label_en": "reference_duration_sec", "default": 10.0, "minimum": 0.5, "step": 0.5, "info_en": "Must be positive."}, + {"name": "guidance_rescale", "type": "text", "label": "guidance_rescale", "default": "auto", "placeholder": "auto 或数值", "placeholder_en": "auto, or a number"}, + {"name": "audio_chunk_threshold_sec", "type": "number", "label": "audio_chunk_threshold_sec(长文本阈值)", "label_en": "audio_chunk_threshold_sec", "default": 45.0, "minimum": 1.0, "step": 1.0, "info_en": "Must be positive."}, + {"name": "audio_chunk_duration_sec", "type": "number", "label": "audio_chunk_duration_sec(长文本分段目标时长)", "label_en": "audio_chunk_duration_sec", "default": 37.0, "minimum": 1.0, "step": 1.0, "info_en": "Must be positive."}, {"name": "cross_fade_duration_sec", "type": "number", "label": "cross_fade_duration_sec(分段交叉淡化)", "label_en": "cross_fade_duration_sec", "default": 0.05, "minimum": 0.0, "step": 0.01} ], "confucius4_tts": [ - {"name": "temperature", "type": "slider", "label": "temperature", "default": 0.8, "minimum": 0.0, "maximum": 2.0, "step": 0.05}, - {"name": "top_p", "type": "slider", "label": "top_p", "default": 0.8, "minimum": 0.0, "maximum": 1.0, "step": 0.01}, + {"name": "temperature", "type": "slider", "label": "temperature", "default": 0.8, "minimum": 0.05, "maximum": 2.0, "step": 0.05, "info_en": "Must be positive."}, + {"name": "top_p", "type": "slider", "label": "top_p", "default": 0.8, "minimum": 0.01, "maximum": 1.0, "step": 0.01, "info_en": "Must be within (0, 1]."}, {"name": "top_k", "type": "number", "label": "top_k", "default": 30, "minimum": 1, "step": 1, "precision": 0}, {"name": "num_beams", "type": "number", "label": "num_beams", "default": 3, "minimum": 1, "step": 1, "precision": 0}, {"name": "repetition_penalty", "type": "slider", "label": "repetition_penalty", "default": 10.0, "minimum": 0.0, "maximum": 20.0, "step": 0.1}, @@ -134,27 +163,28 @@ ], "ace_step": [ - {"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}, - {"name": "audio_cover_strength", "type": "slider", "label": "【cover】audio_cover_strength", "default": 1.0, "minimum": 0.0, "maximum": 1.0, "step": 0.05, "info": "1=贴近原曲,0=自由发挥;建议 0.5"}, - {"name": "cover_noise_strength", "type": "slider", "label": "【cover】cover_noise_strength", "default": 0.0, "minimum": 0.0, "maximum": 1.0, "step": 0.05, "info": "保旋律强度;推荐 0.1~0.25"}, - {"name": "source_caption", "type": "text", "label": "【remix】source_caption", "default": "", "placeholder": "源歌曲描述;『🔍 分析』自动填"}, - {"name": "source_lyrics", "type": "text", "lines": 4, "label": "【remix】source_lyrics", "default": "", "placeholder": "源歌曲原歌词;『🔍 分析』自动填"}, - {"name": "flow_edit_n_min", "type": "slider", "label": "【remix】flow_edit_n_min", "default": 0.0, "minimum": 0.0, "maximum": 1.0, "step": 0.05, "info": "调大更保源曲、换词更弱"}, - {"name": "flow_edit_n_max", "type": "slider", "label": "【remix】flow_edit_n_max", "default": 1.0, "minimum": 0.0, "maximum": 1.0, "step": 0.05, "info": "唱不出新歌词时降到 0.7~0.9"}, - {"name": "flow_edit_n_avg", "type": "number", "label": "【remix】flow_edit_n_avg", "default": 2, "minimum": 1, "maximum": 4, "step": 1, "precision": 0, "info": "每步多次采样取平均(remix 默认 2);1=最快"}, - {"name": "bpm", "type": "number", "label": "【曲谱】BPM", "default": 0, "minimum": 0, "step": 1, "precision": 0, "info": "0=不指定"}, - {"name": "keyscale", "type": "text", "label": "【曲谱】keyscale", "default": "", "placeholder": "如 F major"}, - {"name": "timesignature", "type": "text", "label": "【曲谱】timesignature", "default": "", "placeholder": "如 4"} + {"name": "route", "type": "choice", "label": "route(操作类型)", "label_en": "route (task)", "default": "text2music", "choices": ["text2music", "complete", "lego", "extract", "cover", "cover-nofsq", "repaint"], "info": "cover/cover-nofsq=换词翻唱;非 text2music 需上传源音频;详见 webui/README.md", "info_en": "cover and cover-nofsq re-sing new lyrics. Every route except text2music needs a source audio upload; see webui/README.md."}, + {"name": "negative_prompt", "type": "text", "label": "negative_prompt", "label_en": "Negative prompt", "default": "", "placeholder_en": "Blank uses the model's \"NO USER INPUT\" placeholder"}, + {"name": "num_inference_steps", "type": "number", "label": "num_inference_steps", "default": 8, "minimum": 1, "maximum": 20, "step": 1, "precision": 0, "info": "扩散步数;turbo 权重会被静默限制为 8", "info_en": "Diffusion steps. Turbo checkpoints are silently clamped to 8; only base checkpoints use more."}, + {"name": "shift", "type": "slider", "label": "shift(时间步弯曲)", "label_en": "shift (timestep curve)", "default": 1.0, "minimum": 1.0, "maximum": 5.0, "step": 0.5, "info": "官方默认 1.0;extract 路由由模型自动改用 3.0", "info_en": "Model default is 1.0. The extract route sets 3.0 itself, so leaving this at 1.0 no longer overrides it."}, + {"name": "guidance_scale", "type": "slider", "label": "guidance_scale", "label_en": "Guidance scale", "default": 1.0, "minimum": 0.0, "maximum": 5.0, "step": 0.1}, + {"name": "audio_cover_strength", "type": "slider", "label": "【cover】audio_cover_strength", "label_en": "audio_cover_strength (cover routes)", "default": 1.0, "minimum": 0.0, "maximum": 1.0, "step": 0.05, "info": "1=贴近原曲,0=自由发挥;建议 0.5", "info_en": "1 hugs the source track, 0 improvises. Around 0.5 works well."}, + {"name": "cover_noise_strength", "type": "slider", "label": "【cover】cover_noise_strength", "label_en": "cover_noise_strength (cover routes)", "default": 0.0, "minimum": 0.0, "maximum": 1.0, "step": 0.05, "info": "保旋律强度;推荐 0.1~0.25", "info_en": "Melody retention. 0.1 to 0.25 is the usual range."}, + {"name": "bpm", "type": "number", "label": "【曲谱】BPM", "label_en": "BPM", "default": 0, "minimum": 0, "step": 1, "precision": 0, "info": "0=不指定", "info_en": "0 leaves the tempo unspecified."}, + {"name": "keyscale", "type": "text", "label": "【曲谱】keyscale", "label_en": "Key / scale", "default": "", "placeholder": "如 F major", "placeholder_en": "e.g. F major"}, + {"name": "timesignature", "type": "text", "label": "【曲谱】timesignature", "label_en": "Time signature", "default": "", "placeholder": "如 4", "placeholder_en": "e.g. 4"} ], "minimax_music3": [ {"name": "num_inference_steps", "type": "number", "label": "Flow steps per window", "default": 30, "minimum": 1, "maximum": 200, "step": 1, "precision": 0, "info": "Flow-matching Euler steps per 200-frame denoising window."}, {"name": "guidance_scale", "type": "slider", "label": "Flow guidance scale", "default": 1.7, "minimum": 0.0, "maximum": 10.0, "step": 0.1}, {"name": "ar_guidance_scale", "type": "slider", "label": "AR guidance scale", "default": 1.5, "minimum": 0.0, "maximum": 10.0, "step": 0.1, "info": "Classifier-free guidance of the semantic and residual code sampling."}, - {"name": "top_k", "type": "number", "label": "top_k", "default": 50, "minimum": 1, "maximum": 1024, "step": 1, "precision": 0} + {"name": "top_k", "type": "number", "label": "top_k", "default": 50, "minimum": 1, "maximum": 1024, "step": 1, "precision": 0}, + {"name": "ensemble_takes", "type": "number", "label": "ensemble_takes", "label_en": "Ensemble takes", "default": 1, "minimum": 1, "maximum": 16, "step": 1, "precision": 0, "info_en": "Decode N independent takes in one batched pass (seeds seed, seed+1, ...). Extra takes are returned as take_01...take_NN."}, + {"name": "ensemble_prefix_frames", "type": "number", "label": "ensemble_prefix_frames", "label_en": "Ensemble intro lock (frames)", "default": 0, "minimum": 0, "step": 10, "precision": 0, "info_en": "Decode the first N frames once and share them across takes before they diverge. 0 disables the fork."}, + {"name": "flow_uncond_interval", "type": "number", "label": "flow_uncond_interval", "label_en": "Flow CFG reuse interval", "default": 1, "minimum": 1, "step": 1, "precision": 0, "info_en": "Above 1 reuses the cached guidance delta on intermediate steps: faster, slightly off the exact reference trajectory. 1 is exact."}, + {"name": "flow_uncond_warmup", "type": "number", "label": "flow_uncond_warmup", "label_en": "Flow CFG warmup steps", "default": 2, "minimum": 0, "step": 1, "precision": 0, "info_en": "Leading steps that always evaluate the unconditional branch when reuse is on."}, + {"name": "flow_chunk_hop_frames", "type": "number", "label": "flow_chunk_hop_frames", "label_en": "Flow chunk hop (frames)", "default": 0, "minimum": 0, "step": 10, "precision": 0, "info_en": "0 uses the model config's hop of 100. A larger hop means fewer chunks and less double-denoising."} ], "minimax_h3": [ @@ -163,29 +193,35 @@ {"name": "guidance_scale", "type": "slider", "label": "Guidance scale", "default": 1.0, "minimum": 0.0, "maximum": 5.0, "step": 0.1}, {"name": "sampler", "type": "choice", "label": "Sampler", "default": "euler", "choices": ["euler", "res_multistep", "dpmpp_2m", "unipc"]}, {"name": "dit_acceleration", "type": "choice", "label": "DiT acceleration", "default": "none", "choices": ["none", "spectrum", "first_block_cache"], "info": "None uses the quality-first full-DiT path. Acceleration modes are experimental and may distort some outputs."}, - {"name": "return_video", "type": "bool", "label": "Decode video", "default": false, "info": "Disabled by default to reduce memory use and return audio only."} + {"name": "return_video", "type": "bool", "label": "Decode video", "default": false, "info": "Disabled by default to reduce memory use and return audio only."}, + {"name": "negative_prompt", "type": "text", "label": "negative_prompt", "label_en": "Negative prompt", "default": "", "placeholder_en": "Blank uses the model default"} ], "stable_audio": [ - {"name": "num_inference_steps", "type": "number", "label": "num_inference_steps", "default": 8, "minimum": 1, "step": 1, "precision": 0, "info": "RF 扩散步数"}, + {"name": "num_inference_steps", "type": "number", "label": "num_inference_steps", "default": 8, "minimum": 1, "step": 1, "precision": 0, "info": "RF 扩散步数", "info_en": "Rectified-flow sampling steps."}, {"name": "guidance_scale", "type": "slider", "label": "guidance_scale", "default": 1.0, "minimum": 0.0, "maximum": 5.0, "step": 0.1}, - {"name": "audio_input_kind", "type": "choice", "label": "audio_input_kind(仅上传源音频时生效)", "default": "init_audio", "choices": ["init_audio", "inpaint_audio"]}, - {"name": "init_noise_level", "type": "slider", "label": "init_noise_level(init_audio 强度)", "default": 1.0, "minimum": 0.0, "maximum": 1.0, "step": 0.05} + {"name": "audio_input_kind", "type": "choice", "label": "audio_input_kind(仅上传源音频时生效)", "label_en": "audio_input_kind (only used when a source audio file is uploaded)", "default": "init_audio", "choices": ["init_audio", "inpaint_audio"]}, + {"name": "init_noise_level", "type": "slider", "label": "init_noise_level(init_audio 强度)", "label_en": "init_noise_level (init_audio strength)", "default": 1.0, "minimum": 0.0, "maximum": 1.0, "step": 0.05}, + {"name": "negative_prompt", "type": "text", "label": "negative_prompt", "label_en": "Negative prompt", "default": "", "placeholder_en": "Blank applies no negative conditioning"}, + {"name": "sampler", "type": "choice", "label": "sampler", "label_en": "Sampler", "default": "pingpong", "choices": ["pingpong", "euler"], "info_en": "Only pingpong and euler are accepted; the dpmpp entries in the CLI help text are rejected by the parser."} ], "seed_vc": [ - {"name": "route", "type": "choice", "label": "route(转换路径)", "default": "", "choices": ["", "v2_vc", "v1_whisper_bigvgan_vc", "v1_xlsr_hift_vc", "v1_svc"], "info": "留空=按任务默认"}, - {"name": "num_inference_steps", "type": "number", "label": "num_inference_steps", "default": 30, "minimum": 1, "step": 1, "precision": 0, "info": "CFM 扩散步数"}, - {"name": "length_adjust", "type": "slider", "label": "length_adjust(时长伸缩)", "default": 1.0, "minimum": 0.5, "maximum": 2.0, "step": 0.05}, - {"name": "intelligibility_cfg_rate", "type": "slider", "label": "intelligibility_cfg_rate(仅 v2_vc)", "default": 0.7, "minimum": 0.0, "maximum": 1.0, "step": 0.05}, - {"name": "similarity_cfg_rate", "type": "slider", "label": "similarity_cfg_rate(仅 v2_vc)", "default": 0.7, "minimum": 0.0, "maximum": 1.0, "step": 0.05}, - {"name": "inference_cfg_rate", "type": "slider", "label": "inference_cfg_rate(仅 v1 路径)", "default": 0.7, "minimum": 0.0, "maximum": 1.0, "step": 0.05} + {"name": "route", "type": "choice", "label": "route(转换路径)", "label_en": "route (conversion path)", "default": "", "choices": ["", "v2_vc", "v1_whisper_bigvgan_vc", "v1_xlsr_hift_vc", "v1_svc"], "info": "留空=按任务默认", "info_en": "Blank = pick the default path for the selected task."}, + {"name": "num_inference_steps", "type": "number", "label": "num_inference_steps", "default": 30, "minimum": 1, "step": 1, "precision": 0, "info": "CFM 扩散步数", "info_en": "CFM diffusion steps."}, + {"name": "length_adjust", "type": "slider", "label": "length_adjust(时长伸缩)", "label_en": "length_adjust (duration stretch)", "default": 1.0, "minimum": 0.5, "maximum": 2.0, "step": 0.05}, + {"name": "intelligibility_guidance_scale", "type": "slider", "label": "intelligibility_guidance_scale(仅 v2_vc)", "label_en": "Intelligibility guidance (v2_vc only)", "default": 0.7, "minimum": 0.0, "maximum": 1.0, "step": 0.05}, + {"name": "similarity_guidance_scale", "type": "slider", "label": "similarity_guidance_scale(仅 v2_vc)", "label_en": "Similarity guidance (v2_vc only)", "default": 0.7, "minimum": 0.0, "maximum": 1.0, "step": 0.05}, + {"name": "inference_guidance_scale", "type": "slider", "label": "inference_guidance_scale(仅 v1 路径)", "label_en": "Inference guidance (v1 paths only)", "default": 0.7, "minimum": 0.0, "maximum": 1.0, "step": 0.05}, + {"name": "f0_condition", "type": "bool", "label": "f0_condition(仅 v1 路径)", "label_en": "f0_condition (v1 routes only)", "default": false, "info_en": "Enable F0 conditioning. Required for singing voice conversion on the v1_svc route."}, + {"name": "auto_f0_adjust", "type": "bool", "label": "auto_f0_adjust(仅 v1 路径)", "label_en": "auto_f0_adjust (v1 routes only)", "default": false, "info_en": "Shift the source pitch toward the target speaker's pitch level."}, + {"name": "semitone_shift", "type": "number", "label": "semitone_shift(仅 v1 路径)", "label_en": "semitone_shift (v1 routes only)", "default": 0, "minimum": -24, "maximum": 24, "step": 1, "precision": 0} ], "rvc": [ {"name": "voice_id", "type": "choice", "label": "voice_id(打包音色)", "label_en": "voice_id", "default": "default", "choices": ["default", "manthos", "chocola", "fraise"]}, - {"name": "voice_model_path", "type": "text", "label": "voice_model_path(自定义 RVC .pth/.pt)", "label_en": "voice_model_path", "default": "", "placeholder": "留空=使用打包音色"}, - {"name": "retrieval_index_path", "type": "text", "label": "retrieval_index_path(FAISS index)", "label_en": "retrieval_index_path", "default": "", "placeholder": "可选 .index 路径"}, + {"name": "voice_model_path", "type": "text", "label": "voice_model_path(自定义 RVC .pth/.pt)", "label_en": "voice_model_path", "default": "", "placeholder": "留空=使用打包音色", "placeholder_en": "Blank = use the packaged voice"}, + {"name": "retrieval_index_path", "type": "text", "label": "retrieval_index_path(FAISS index)", "label_en": "retrieval_index_path", "default": "", "placeholder": "可选 .index 路径", "placeholder_en": "Optional .index path"}, {"name": "retrieval_blend", "type": "slider", "label": "retrieval_blend", "default": 0.0, "minimum": 0.0, "maximum": 1.0, "step": 0.05}, {"name": "semitone_shift", "type": "number", "label": "semitone_shift(半音变调)", "label_en": "semitone_shift", "default": 0, "step": 1, "precision": 0}, {"name": "pitch_filter_radius", "type": "number", "label": "pitch_filter_radius", "default": 3, "minimum": 0, "step": 1, "precision": 0}, @@ -199,53 +235,70 @@ {"name": "split_threshold_sec", "type": "number", "label": "split_threshold_sec", "default": 32, "minimum": 1, "step": 1, "precision": 0} ], - "meanvc2": [ - {"name": "seed", "type": "number", "label": "seed", "default": 42, "minimum": 0, "step": 1, "precision": 0} - ], - "personaplex": [ {"name": "voice_id", "type": "choice", "label": "voice_id(打包音色)", "label_en": "voice_id (packaged voice)", "default": "NATF2", "choices": ["NATF0", "NATF1", "NATF2", "NATF3", "NATM0", "NATM1", "NATM2", "NATM3", "VARF0", "VARF1", "VARF2", "VARF3", "VARF4", "VARM0", "VARM1", "VARM2", "VARM3", "VARM4"]}, {"name": "system_prompt", "type": "text", "label": "system_prompt", "default": "", "placeholder": "Leave blank to use the text box as the system prompt."}, - {"name": "temperature", "type": "slider", "label": "temperature", "default": 0.8, "minimum": 0.0, "maximum": 2.0, "step": 0.05}, - {"name": "text_temperature", "type": "slider", "label": "text_temperature", "default": 0.8, "minimum": 0.0, "maximum": 2.0, "step": 0.05}, + {"name": "temperature", "type": "slider", "label": "temperature", "default": 0.8, "minimum": 0.05, "maximum": 2.0, "step": 0.05, "info_en": "Must be above 0 while do_sample is on."}, + {"name": "text_temperature", "type": "number", "label": "text_temperature", "label_en": "text_temperature (blank follows temperature)", "minimum": 0.05, "maximum": 2.0, "step": 0.05, "info_en": "Blank makes the text head follow temperature. Must be above 0 while do_sample is on."}, {"name": "top_k", "type": "number", "label": "top_k", "default": 250, "minimum": 0, "step": 1, "precision": 0}, - {"name": "text_top_k", "type": "number", "label": "text_top_k", "default": 250, "minimum": 0, "step": 1, "precision": 0}, + {"name": "text_top_k", "type": "number", "label": "text_top_k", "label_en": "text_top_k (blank follows top_k)", "minimum": 0, "step": 1, "precision": 0, "info_en": "Blank makes the text head follow top_k."}, {"name": "do_sample", "type": "bool", "label": "do_sample", "default": true} ], "vevo2": [ - {"name": "route", "type": "choice", "label": "route(任务路线)", "default": "", "choices": ["", "style_preserved_vc", "style_converted_vc", "style_preserved_svc", "style_converted_svc", "singing_style_conversion", "editing"], "info": "留空=按任务默认;详见 webui/README.md"}, - {"name": "num_inference_steps", "type": "number", "label": "num_inference_steps", "default": 32, "minimum": 1, "step": 1, "precision": 0, "info": "流匹配步数"}, - {"name": "use_pitch_shift", "type": "choice", "label": "use_pitch_shift(自动音高对齐)", "default": "", "choices": ["", "true", "false"], "info": "留空=按路线默认"}, - {"name": "temperature", "type": "slider", "label": "temperature(AR 路线用)", "default": 0.7, "minimum": 0.0, "maximum": 2.0, "step": 0.05, "info": "默认取自模型 generation_config.json"}, - {"name": "top_k", "type": "number", "label": "top_k(AR 路线用)", "default": 20, "minimum": 0, "step": 1, "precision": 0, "info": "默认取自模型 generation_config.json"}, - {"name": "top_p", "type": "slider", "label": "top_p(AR 路线用)", "default": 0.8, "minimum": 0.0, "maximum": 1.0, "step": 0.01} + {"name": "route", "type": "choice", "label": "route(任务路线)", "label_en": "route (task path)", "default": "", "choices": ["", "style_preserved_vc", "style_converted_vc", "style_preserved_svc", "style_converted_svc", "singing_style_conversion", "editing"], "info": "留空=按任务默认;详见 webui/README.md", "info_en": "Blank = pick the default path for the selected task; see webui/README.md."}, + {"name": "num_inference_steps", "type": "number", "label": "num_inference_steps", "default": 32, "minimum": 1, "step": 1, "precision": 0, "info": "流匹配步数", "info_en": "Flow-matching steps."}, + {"name": "use_pitch_shift", "type": "choice", "label": "use_pitch_shift(自动音高对齐)", "label_en": "use_pitch_shift (automatic pitch alignment)", "default": "", "choices": ["", "true", "false"], "info": "留空=按路线默认", "info_en": "Blank = use the path's own default."}, + {"name": "temperature", "type": "number", "label": "temperature(AR 路线用)", "label_en": "temperature (AR paths)", "minimum": 0.05, "maximum": 2.0, "step": 0.05, "info": "留空=取自模型 generation_config.json(回退 1.0)", "info_en": "Blank uses the checkpoint's generation_config.json (C++ fallback 1.0). Must be above 0."}, + {"name": "top_k", "type": "number", "label": "top_k(AR 路线用)", "label_en": "top_k (AR paths)", "minimum": 0, "step": 1, "precision": 0, "info": "留空=取自模型 generation_config.json(回退 25)", "info_en": "Blank uses the checkpoint's generation_config.json (C++ fallback 25)."}, + {"name": "top_p", "type": "number", "label": "top_p(AR 路线用)", "label_en": "top_p (AR paths)", "minimum": 0.0, "maximum": 1.0, "step": 0.01, "info": "留空=取自模型 generation_config.json(回退 0.8)", "info_en": "Blank uses the checkpoint's generation_config.json (C++ fallback 0.8)."} ], "heartmula": [ - {"name": "tags", "type": "text", "label": "tags(必填,逗号分隔)", "default": "", "placeholder": "pop,bright,drums,female vocals", "info": "风格/情绪/乐器/人声标签,模型必需"}, + {"name": "tags", "type": "text", "label": "tags(必填,逗号分隔)", "label_en": "tags (required, comma-separated)", "default": "", "info": "风格/情绪/乐器/人声标签,模型必需", "info_en": "Required. Style, mood, instrument and vocal tags; HeartMuLa throws on an empty value.", "placeholder": "pop,bright,drums,female vocals"}, {"name": "temperature", "type": "slider", "label": "temperature", "default": 1.0, "minimum": 0.0, "maximum": 2.0, "step": 0.05}, {"name": "top_k", "type": "number", "label": "top_k", "default": 50, "minimum": 0, "step": 1, "precision": 0}, - {"name": "guidance_scale", "type": "slider", "label": "guidance_scale(MuLa CFG)", "default": 1.5, "minimum": 0.0, "maximum": 5.0, "step": 0.1}, - {"name": "num_inference_steps", "type": "number", "label": "num_inference_steps(codec 步数)", "default": 10, "minimum": 1, "step": 1, "precision": 0}, - {"name": "infinite_mode", "type": "bool", "label": "infinite_mode(长输出分段生成)", "default": false}, - {"name": "codec_guidance_scale", "type": "slider", "label": "codec_guidance_scale", "default": 1.25, "minimum": 0.0, "maximum": 5.0, "step": 0.05} + {"name": "guidance_scale", "type": "slider", "label": "guidance_scale(MuLa CFG)", "label_en": "guidance_scale (MuLa CFG)", "default": 1.5, "minimum": 0.0, "maximum": 5.0, "step": 0.1}, + {"name": "num_inference_steps", "type": "number", "label": "num_inference_steps(codec 步数)", "label_en": "num_inference_steps (codec steps)", "default": 10, "minimum": 1, "step": 1, "precision": 0}, + {"name": "infinite_mode", "type": "bool", "label": "infinite_mode(长输出分段生成)", "label_en": "infinite_mode (chunked long-form generation)", "default": false}, + {"name": "codec_guidance_scale", "type": "slider", "label": "codec_guidance_scale", "default": 1.25, "minimum": 0.0, "maximum": 5.0, "step": 0.05}, + {"name": "text_chunk_size", "type": "number", "label": "text_chunk_size", "label_en": "Text chunk size (infinite mode)", "default": 4096, "minimum": 1, "step": 64, "precision": 0}, + {"name": "infinite_chunk_audio_duration_ms", "type": "number", "label": "infinite_chunk_audio_duration_ms", "label_en": "Infinite-mode chunk audio (ms)", "default": 240000, "minimum": 1, "step": 10000, "precision": 0, "info_en": "Maximum audio generated per infinite-mode chunk. Must be positive."} ], "index_tts2": [ - {"name": "lang", "type": "choice", "label": "lang(语种提示, 仅 IndexTTS2.5 模型)", "label_en": "lang (language hint, IndexTTS2.5 models only)", "default": "auto", "choices": ["auto", "zh", "en", "ja", "es", "ar"], "info": "仅对 IndexTTS2.5(多语种)模型生效:auto 含汉字按中文,否则按英文;日/西/阿建议显式选择", "info_en": "Only applies to IndexTTS2.5 (multilingual) models: auto picks zh when the text contains Han characters, otherwise en; set ja/es/ar explicitly"}, - {"name": "emotion_text", "type": "text", "label": "emotion_text(情绪参考文本)", "label_en": "emotion_text (emotion reference text)", "default": "", "placeholder": "例:你吓死我了!你是鬼吗?", "placeholder_en": "e.g. You scared me to death!", "info": "填写后自动开启情感条件(use_emotion_text)", "info_en": "Setting this enables emotion conditioning."}, + {"name": "language", "type": "choice", "label": "language(语种提示, 仅 IndexTTS2.5 模型)", "label_en": "language (language hint, IndexTTS2.5 models only)", "default": "auto", "choices": ["auto", "zh", "en", "ja", "es", "ar"], "info": "仅对 IndexTTS2.5(多语种)模型生效:auto 含汉字按中文,否则按英文;日/西/阿建议显式选择", "info_en": "Only applies to IndexTTS2.5 (multilingual) models: auto picks zh when the text contains Han characters, otherwise en; set ja/es/ar explicitly"}, + {"name": "emotion_text", "type": "text", "label": "emotion_text(情绪参考文本)", "label_en": "emotion_text (emotion reference text)", "default": "", "info": "填写后自动开启情感条件(use_emotion_text)", "info_en": "Setting this enables emotion conditioning.", "placeholder": "例:你吓死我了!你是鬼吗?", "placeholder_en": "e.g. You scared me to death!"}, {"name": "emotion_alpha", "type": "slider", "label": "emotion_alpha(情感强度)", "label_en": "emotion_alpha", "default": 1.0, "minimum": 0.0, "maximum": 1.0, "step": 0.05}, {"name": "use_emotion_text", "type": "bool", "label": "use_emotion_text(从朗读文本推断情感)", "label_en": "use_emotion_text (infer from text)", "default": false}, {"name": "use_random_emotion", "type": "bool", "label": "use_random_emotion(随机情感)", "label_en": "use_random_emotion", "default": false}, {"name": "interval_silence_ms", "type": "number", "label": "interval_silence_ms(分段间静音)", "label_en": "interval_silence_ms", "default": 200, "minimum": 0, "step": 50, "precision": 0}, - {"name": "duration_factor", "type": "slider", "label": "duration_factor(语速/时长倍率,>1 更慢,<1 更快)", "label_en": "duration_factor (duration multiplier; >1 slower, <1 faster)", "default": 1.0, "minimum": 0.5, "maximum": 2.0, "step": 0.05, "info": "对齐官方 IndexTTS2.5 的 duration_factor:缩放输出时长,不改变音色/内容", "info_en": "Matches official IndexTTS2.5 duration_factor: scales output duration without changing timbre or content"} + {"name": "duration_factor", "type": "slider", "label": "duration_factor(语速/时长倍率,>1 更慢,<1 更快)", "label_en": "duration_factor (duration multiplier; >1 slower, <1 faster)", "default": 1.0, "minimum": 0.5, "maximum": 2.0, "step": 0.05, "info": "对齐官方 IndexTTS2.5 的 duration_factor:缩放输出时长,不改变音色/内容", "info_en": "Matches official IndexTTS2.5 duration_factor: scales output duration without changing timbre or content"}, + {"name": "do_sample", "type": "bool", "label": "do_sample", "label_en": "Sample", "default": true}, + {"name": "temperature", "type": "slider", "label": "temperature", "label_en": "Temperature", "default": 0.8, "minimum": 0.05, "maximum": 2.0, "step": 0.05, "info_en": "Must be positive."}, + {"name": "top_p", "type": "slider", "label": "top_p", "label_en": "Top-p", "default": 0.8, "minimum": 0.01, "maximum": 1.0, "step": 0.01, "info_en": "Must be within (0, 1]."}, + {"name": "top_k", "type": "number", "label": "top_k", "label_en": "Top-k", "default": 30, "minimum": 1, "step": 1, "precision": 0, "info_en": "Must be positive."}, + {"name": "repetition_penalty", "type": "slider", "label": "repetition_penalty", "label_en": "Repetition penalty", "default": 10.0, "minimum": 0.0, "maximum": 20.0, "step": 0.1}, + {"name": "num_beams", "type": "number", "label": "num_beams", "label_en": "Beam count", "default": 3, "minimum": 1, "step": 1, "precision": 0, "info_en": "Must be positive."}, + {"name": "length_penalty", "type": "slider", "label": "length_penalty", "label_en": "Length penalty", "default": 0.0, "minimum": -2.0, "maximum": 2.0, "step": 0.1} ], "irodori_tts": [ {"name": "num_inference_steps", "type": "number", "label": "num_inference_steps(RF 扩散步数)", "label_en": "num_inference_steps", "default": 40, "minimum": 1, "step": 1, "precision": 0}, {"name": "duration_sec", "type": "number", "label": "duration_sec(0=模型自动预测时长)", "label_en": "duration_sec (0 = auto)", "default": 0, "minimum": 0, "step": 0.5}, - {"name": "duration_scale", "type": "slider", "label": "duration_scale(语速倒数,越大越慢)", "label_en": "duration_scale", "default": 1.0, "minimum": 0.5, "maximum": 2.0, "step": 0.05} + {"name": "duration_scale", "type": "slider", "label": "duration_scale(语速倒数,越大越慢)", "label_en": "duration_scale", "default": 1.0, "minimum": 0.5, "maximum": 2.0, "step": 0.05}, + {"name": "text_guidance_scale", "type": "slider", "label": "text_guidance_scale", "label_en": "Text guidance", "default": 3.0, "minimum": 0.0, "maximum": 10.0, "step": 0.1}, + {"name": "speaker_guidance_scale", "type": "slider", "label": "speaker_guidance_scale", "label_en": "Speaker guidance", "default": 5.0, "minimum": 0.0, "maximum": 15.0, "step": 0.1}, + {"name": "caption_guidance_scale", "type": "slider", "label": "caption_guidance_scale", "label_en": "Caption guidance", "default": 3.0, "minimum": 0.0, "maximum": 10.0, "step": 0.1}, + {"name": "guidance_mode", "type": "choice", "label": "guidance_mode", "label_en": "Guidance mode", "default": "independent", "choices": ["independent", "joint", "alternating"]}, + {"name": "guidance_min_t", "type": "slider", "label": "guidance_min_t", "label_en": "Guidance start timestep", "default": 0.5, "minimum": 0.0, "maximum": 1.0, "step": 0.05, "info_en": "Must not exceed the guidance end timestep."}, + {"name": "guidance_max_t", "type": "slider", "label": "guidance_max_t", "label_en": "Guidance end timestep", "default": 1.0, "minimum": 0.0, "maximum": 1.0, "step": 0.05}, + {"name": "min_duration_sec", "type": "number", "label": "min_duration_sec", "label_en": "Min duration (s)", "default": 0.5, "minimum": 0.05, "step": 0.5, "info_en": "Must be positive and no greater than the maximum."}, + {"name": "max_duration_sec", "type": "number", "label": "max_duration_sec", "label_en": "Max duration (s)", "default": 30.0, "minimum": 0.05, "step": 0.5}, + {"name": "no_ref", "type": "bool", "label": "no_ref", "label_en": "No-reference generation", "default": true, "info_en": "Forced off automatically whenever a speaker reference is supplied."}, + {"name": "trim_tail", "type": "bool", "label": "trim_tail", "label_en": "Trim trailing silence", "default": true}, + {"name": "text_chunk_mode", "type": "choice", "label": "text_chunk_mode", "label_en": "Text chunk mode", "default": "endline", "choices": ["japanese", "endline"]}, + {"name": "instruction", "type": "text", "label": "instruction", "label_en": "instruction (caption-conditioned checkpoints)", "default": "", "placeholder_en": "Voice or style description; ignored by checkpoints without caption conditioning"} ], "moss_tts_local": [ @@ -273,9 +326,9 @@ ], "controlfoley": [ - {"name": "duration_sec", "type": "number", "label": "duration_sec", "default": 8.0, "minimum": 0.1, "step": 0.5}, + {"name": "duration_sec", "type": "number", "label": "duration_sec", "default": 8.0, "minimum": 0.64, "maximum": 60.0, "step": 0.1, "info_en": "Shorter than 0.64 s produces an empty sync sequence and fails."}, {"name": "num_inference_steps", "type": "number", "label": "num_inference_steps", "default": 25, "minimum": 1, "step": 1, "precision": 0}, - {"name": "guidance_scale", "type": "slider", "label": "guidance_scale", "default": 4.5, "minimum": 0.0, "maximum": 10.0, "step": 0.1}, + {"name": "guidance_scale", "type": "slider", "label": "guidance_scale", "default": 4.5, "minimum": 0.1, "maximum": 10.0, "step": 0.1, "info_en": "Must be positive."}, {"name": "negative_prompt", "type": "text", "label": "negative_prompt", "default": "", "placeholder": "optional negative prompt"}, {"name": "mask_away_clip", "type": "bool", "label": "mask_away_clip", "default": false} ], @@ -284,7 +337,7 @@ {"name": "duration_sec", "type": "number", "label": "duration_sec", "default": 20.0, "minimum": 0.1, "step": 0.5}, {"name": "guidance_scale", "type": "slider", "label": "guidance_scale", "default": 2.0, "minimum": 0.0, "maximum": 10.0, "step": 0.1}, {"name": "stop_threshold", "type": "slider", "label": "stop_threshold", "default": 0.5, "minimum": 0.0, "maximum": 1.0, "step": 0.05}, - {"name": "min_stop_step", "type": "number", "label": "min_stop_step", "default": 5, "minimum": 0, "step": 1, "precision": 0} + {"name": "min_stop_step", "type": "number", "label": "min_stop_step", "default": 5, "minimum": 1, "step": 1, "precision": 0, "info_en": "Must be positive."} ], "fireredtts3-base": [ @@ -308,7 +361,7 @@ {"name": "template_name", "type": "choice", "label": "template_name", "default": "voice_design", "choices": ["voice_design"]}, {"name": "language", "type": "choice", "label": "language", "default": "Chinese", "choices": ["Chinese", "English", "Cantonese", "Japanese", "Korean", "Spanish", "French", "Russian", "Arabic", "Turkish", "Indonesian", "Portuguese", "Italian", "Dutch", "Vietnamese", "German", "Ukrainian", "Thai", "Polish", "Romanian", "Greek", "Czech", "Finnish", "Hindi", "ZH_Anhui", "ZH_Fujian", "ZH_Gansu", "ZH_Guizhou", "ZH_Hebei", "ZH_Henan", "ZH_Hubei", "ZH_Hunan", "ZH_Jiangxi", "ZH_Liaoning", "ZH_Minnan", "ZH_Ningxia", "ZH_Shaanxi", "ZH_Shandong", "ZH_Shanghai", "ZH_Shanxi", "ZH_Tianjin", "ZH_Wenzhou", "ZH_Wu", "ZH_Yunnan"]}, {"name": "num_inference_steps", "type": "number", "label": "num_inference_steps", "default": 10, "minimum": 1, "step": 1, "precision": 0}, - {"name": "guidance_scale", "type": "slider", "label": "guidance_scale", "default": 2.0, "minimum": 0.0, "maximum": 10.0, "step": 0.1}, + {"name": "guidance_scale", "type": "slider", "label": "guidance_scale", "default": 1.2, "minimum": 0.0, "maximum": 10.0, "step": 0.1, "info_en": "FireRedTTS3 voice design is tuned for 1.2; 2.0 is the instruct_tts value."}, {"name": "stop_threshold", "type": "slider", "label": "stop_threshold", "default": 0.5, "minimum": 0.0, "maximum": 1.0, "step": 0.05} ], @@ -318,9 +371,7 @@ {"name": "num_inference_steps", "type": "number", "label": "num_inference_steps", "default": 10, "minimum": 1, "step": 1, "precision": 0}, {"name": "guidance_scale", "type": "slider", "label": "guidance_scale", "default": 2.0, "minimum": 0.0, "maximum": 10.0, "step": 0.1}, {"name": "max_new_audio_steps", "type": "number", "label": "max_new_audio_steps", "default": 750, "minimum": 1, "step": 1, "precision": 0}, - {"name": "top_k", "type": "number", "label": "top_k", "default": 20, "minimum": 0, "step": 1, "precision": 0}, - {"name": "top_p", "type": "slider", "label": "top_p", "default": 0.8, "minimum": 0.0, "maximum": 1.0, "step": 0.01}, - {"name": "temperature", "type": "slider", "label": "temperature", "default": 0.7, "minimum": 0.0, "maximum": 2.0, "step": 0.05} + {"name": "min_new_audio_steps", "type": "number", "label": "min_new_audio_steps", "label_en": "Min audio steps", "default": 6, "minimum": 0, "step": 1, "precision": 0, "info_en": "Floor on generated audio steps; suppresses truncated outputs. Must be non-negative."} ], "firered-audio-vdesign": [ @@ -328,16 +379,18 @@ {"name": "language", "type": "choice", "label": "language", "default": "zh", "choices": ["zh", "en"]}, {"name": "num_inference_steps", "type": "number", "label": "num_inference_steps", "default": 10, "minimum": 1, "step": 1, "precision": 0}, {"name": "guidance_scale", "type": "slider", "label": "guidance_scale", "default": 2.0, "minimum": 0.0, "maximum": 10.0, "step": 0.1}, - {"name": "max_new_audio_steps", "type": "number", "label": "max_new_audio_steps", "default": 750, "minimum": 1, "step": 1, "precision": 0} + {"name": "max_new_audio_steps", "type": "number", "label": "max_new_audio_steps", "default": 750, "minimum": 1, "step": 1, "precision": 0}, + {"name": "min_new_audio_steps", "type": "number", "label": "min_new_audio_steps", "label_en": "Min audio steps", "default": 6, "minimum": 0, "step": 1, "precision": 0, "info_en": "Floor on generated audio steps; suppresses truncated outputs. Must be non-negative."} ], "firered-audio-semantic-edit": [ {"name": "template_name", "type": "choice", "label": "template_name", "default": "semantic_edit", "choices": ["semantic_edit"]}, {"name": "language", "type": "choice", "label": "language", "default": "zh", "choices": ["zh", "en"]}, - {"name": "instruction", "type": "text", "label": "instruction", "default": "", "placeholder": "delete '比普通的茶叶要'"}, + {"name": "instruction", "type": "text", "label": "instruction", "default": "", "placeholder": "delete '比普通的茶叶要'", "placeholder_en": "delete 'the phrase to remove'"}, {"name": "num_inference_steps", "type": "number", "label": "num_inference_steps", "default": 10, "minimum": 1, "step": 1, "precision": 0}, {"name": "guidance_scale", "type": "slider", "label": "guidance_scale", "default": 2.0, "minimum": 0.0, "maximum": 10.0, "step": 0.1}, {"name": "max_new_audio_steps", "type": "number", "label": "max_new_audio_steps", "default": 750, "minimum": 1, "step": 1, "precision": 0}, + {"name": "min_new_audio_steps", "type": "number", "label": "min_new_audio_steps", "label_en": "Min audio steps", "default": 6, "minimum": 0, "step": 1, "precision": 0, "info_en": "Floor on generated audio steps; suppresses truncated outputs. Must be non-negative."}, {"name": "max_new_text_tokens", "type": "number", "label": "max_new_text_tokens", "default": 512, "minimum": 1, "step": 1, "precision": 0} ], @@ -347,22 +400,207 @@ {"name": "instruction", "type": "text", "label": "instruction", "default": "shift the pitch by 3 steps", "placeholder": "shift the pitch by 3 steps"}, {"name": "num_inference_steps", "type": "number", "label": "num_inference_steps", "default": 10, "minimum": 1, "step": 1, "precision": 0}, {"name": "guidance_scale", "type": "slider", "label": "guidance_scale", "default": 2.0, "minimum": 0.0, "maximum": 10.0, "step": 0.1}, - {"name": "max_new_audio_steps", "type": "number", "label": "max_new_audio_steps", "default": 750, "minimum": 1, "step": 1, "precision": 0} + {"name": "max_new_audio_steps", "type": "number", "label": "max_new_audio_steps", "default": 750, "minimum": 1, "step": 1, "precision": 0}, + {"name": "min_new_audio_steps", "type": "number", "label": "min_new_audio_steps", "label_en": "Min audio steps", "default": 6, "minimum": 0, "step": 1, "precision": 0, "info_en": "Floor on generated audio steps; suppresses truncated outputs. Must be non-negative."} ], "firered-audio-asr": [ {"name": "template_name", "type": "choice", "label": "template_name", "default": "asr", "choices": ["asr", "understand"]}, {"name": "language", "type": "choice", "label": "language", "default": "zh", "choices": ["zh", "en"]}, - {"name": "enable_thinking", "type": "bool", "label": "enable_thinking", "default": false}, - {"name": "max_new_tokens", "type": "number", "label": "max_new_tokens", "default": 512, "minimum": 1, "step": 1, "precision": 0}, - {"name": "top_k", "type": "number", "label": "top_k", "default": 20, "minimum": 0, "step": 1, "precision": 0}, - {"name": "top_p", "type": "slider", "label": "top_p", "default": 0.8, "minimum": 0.0, "maximum": 1.0, "step": 0.01}, - {"name": "temperature", "type": "slider", "label": "temperature", "default": 0.7, "minimum": 0.0, "maximum": 2.0, "step": 0.05} + {"name": "enable_thinking", "type": "bool", "label": "enable_thinking", "default": false, "info_en": "Rejected by the asr template; use template_name=understand."}, + {"name": "max_new_tokens", "type": "number", "label": "max_new_tokens", "label_en": "max_new_tokens", "minimum": 1, "step": 1, "precision": 0, "info_en": "Blank uses 300, or 1024 when enable_thinking is on. Setting a value here defeats the automatic thinking-mode bump."}, + {"name": "top_k", "type": "number", "label": "top_k", "default": 20, "minimum": 0, "step": 1, "precision": 0, "info_en": "Sampling options apply to template_name=understand only; asr decodes greedily."}, + {"name": "top_p", "type": "slider", "label": "top_p", "default": 0.8, "minimum": 0.0, "maximum": 1.0, "step": 0.01, "info_en": "Sampling options apply to template_name=understand only; asr decodes greedily."}, + {"name": "temperature", "type": "slider", "label": "temperature", "default": 0.7, "minimum": 0.0, "maximum": 2.0, "step": 0.05, "info_en": "Sampling options apply to template_name=understand only; asr decodes greedily."} ], "supertonic": [ {"name": "voice", "type": "choice", "label": "voice(预置音色:M 男声 / F 女声)", "label_en": "voice (M = male, F = female presets)", "default": "M1", "choices": ["M1", "M2", "M3", "M4", "M5", "F1", "F2", "F3", "F4", "F5"]}, {"name": "speaking_rate", "type": "slider", "label": "speaking_rate(语速倍率)", "label_en": "speaking_rate", "default": 1.05, "minimum": 0.5, "maximum": 2.0, "step": 0.05}, {"name": "num_inference_steps", "type": "number", "label": "num_inference_steps(流匹配步数)", "label_en": "num_inference_steps", "default": 8, "minimum": 1, "step": 1, "precision": 0} + ], + + "dots_tts": [ + {"name": "template_name", "type": "choice", "label": "template_name", "label_en": "Template", "default": "tts", "choices": ["tts", "instruction_tts", "text_to_audio", "tts_interleave", "edit"], "info_en": "Generation template. instruction_tts and edit need an instruction; edit also needs source audio plus source_text and target_text."}, + {"name": "num_inference_steps", "type": "number", "label": "num_inference_steps", "label_en": "Sampling steps", "default": 10, "minimum": 1, "step": 1, "precision": 0}, + {"name": "guidance_scale", "type": "slider", "label": "guidance_scale", "label_en": "Guidance scale", "default": 1.2, "minimum": 0.0, "maximum": 5.0, "step": 0.1}, + {"name": "speaker_scale", "type": "slider", "label": "speaker_scale", "label_en": "Speaker scale", "default": 1.5, "minimum": 0.0, "maximum": 5.0, "step": 0.1, "info_en": "Classifier-free guidance weight on the speaker reference."}, + {"name": "sampler_mode", "type": "choice", "label": "sampler_mode", "label_en": "ODE sampler", "default": "euler", "choices": ["euler", "midpoint", "rk4"]}, + {"name": "vocoder_merge_steps", "type": "number", "label": "vocoder_merge_steps", "label_en": "Vocoder merge steps", "default": 4, "minimum": 1, "step": 1, "precision": 0}, + {"name": "use_xvector", "type": "choice", "label": "use_xvector", "label_en": "Speaker x-vector", "default": "auto", "choices": ["auto", "on", "off"]}, + {"name": "reference_duration_sec", "type": "number", "label": "reference_duration_sec", "label_en": "Reference trim (s)", "minimum": 0.0, "step": 0.5, "info_en": "Blank uses the whole reference clip."}, + {"name": "text_chunk_size", "type": "number", "label": "text_chunk_size", "label_en": "Text chunk size", "default": 320, "minimum": 1, "step": 1, "precision": 0}, + {"name": "text_chunk_mode", "type": "choice", "label": "text_chunk_mode", "label_en": "Text chunk mode", "default": "tag_aware", "choices": ["default", "tag_aware", "japanese", "endline"]}, + {"name": "instruction", "type": "text", "label": "instruction", "label_en": "instruction (instruction_tts / edit)", "default": "", "placeholder_en": "Style or edit instruction"}, + {"name": "source_text", "type": "text", "label": "source_text", "label_en": "source_text (edit template)", "default": "", "placeholder_en": "Transcript of the source audio"}, + {"name": "target_text", "type": "text", "label": "target_text", "label_en": "target_text (edit template)", "default": "", "placeholder_en": "Transcript the edit should produce"} + ], + + "outetts": [ + {"name": "temperature", "type": "number", "label": "temperature", "label_en": "Temperature", "minimum": 0.0, "step": 0.05, "info_en": "Blank uses the packaged checkpoint's value (C++ fallback 0.4)."}, + {"name": "top_k", "type": "number", "label": "top_k", "label_en": "Top-k", "minimum": 0, "step": 1, "precision": 0, "info_en": "Blank uses the packaged checkpoint's value (C++ fallback 40)."}, + {"name": "top_p", "type": "number", "label": "top_p", "label_en": "Top-p", "minimum": 0.01, "maximum": 1.0, "step": 0.01, "info_en": "Blank uses the packaged checkpoint's value (C++ fallback 0.9). Must be above 0."}, + {"name": "min_p", "type": "number", "label": "min_p", "label_en": "Min-p", "minimum": 0.0, "maximum": 1.0, "step": 0.01, "info_en": "Blank uses the packaged checkpoint's value (C++ fallback 0.05)."}, + {"name": "repetition_penalty", "type": "number", "label": "repetition_penalty", "label_en": "Repetition penalty", "minimum": 0.01, "step": 0.01, "info_en": "Blank uses the packaged checkpoint's value (C++ fallback 1.1). Must be above 0."}, + {"name": "repetition_window", "type": "number", "label": "repetition_window", "label_en": "Repetition window", "default": 64, "minimum": 0, "step": 1, "precision": 0}, + {"name": "reference_language", "type": "text", "label": "reference_language", "label_en": "Reference language", "default": "", "placeholder_en": "Blank follows the request language, else en"}, + {"name": "text_chunk_size", "type": "number", "label": "text_chunk_size", "label_en": "Text chunk size", "default": 256, "minimum": 1, "step": 1, "precision": 0}, + {"name": "text_chunk_mode", "type": "choice", "label": "text_chunk_mode", "label_en": "Text chunk mode", "default": "default", "choices": ["default", "tag_aware", "japanese", "endline"]} + ], + + "glm_tts": [ + {"name": "temperature", "type": "slider", "label": "temperature", "label_en": "Temperature", "default": 1.0, "minimum": 0.05, "maximum": 2.0, "step": 0.05}, + {"name": "top_k", "type": "number", "label": "top_k", "label_en": "Top-k", "default": 25, "minimum": 0, "step": 1, "precision": 0}, + {"name": "top_p", "type": "slider", "label": "top_p", "label_en": "Top-p", "default": 0.8, "minimum": 0.0, "maximum": 1.0, "step": 0.01}, + {"name": "num_inference_steps", "type": "number", "label": "num_inference_steps", "label_en": "Flow steps", "minimum": 1, "step": 1, "precision": 0, "info_en": "Blank uses the packaged checkpoint's value (C++ fallback 10)."}, + {"name": "flow_guidance_scale", "type": "number", "label": "flow_guidance_scale", "label_en": "Flow guidance scale", "minimum": 0.0, "step": 0.05, "info_en": "Blank uses the packaged checkpoint's value (C++ fallback 0.7)."} + ], + + "fish_audio": [ + {"name": "temperature", "type": "slider", "label": "temperature", "label_en": "Temperature", "default": 0.8, "minimum": 0.05, "maximum": 1.95, "step": 0.05, "info_en": "Must stay strictly between 0 and 2."}, + {"name": "top_p", "type": "slider", "label": "top_p", "label_en": "Top-p", "default": 0.8, "minimum": 0.01, "maximum": 1.0, "step": 0.01}, + {"name": "top_k", "type": "number", "label": "top_k", "label_en": "Top-k", "default": 30, "minimum": 1, "step": 1, "precision": 0}, + {"name": "text_chunk_size", "type": "number", "label": "text_chunk_size", "label_en": "Text chunk size", "default": 200, "minimum": 1, "step": 1, "precision": 0}, + {"name": "text_chunk_mode", "type": "choice", "label": "text_chunk_mode", "label_en": "Text chunk mode", "default": "default", "choices": ["default", "tag_aware", "japanese", "endline"]} + ], + + "higgs_audio_tts": [ + {"name": "temperature", "type": "slider", "label": "temperature", "label_en": "Temperature", "default": 0.8, "minimum": 0.05, "maximum": 2.0, "step": 0.05}, + {"name": "top_p", "type": "slider", "label": "top_p", "label_en": "Top-p", "default": 0.8, "minimum": 0.01, "maximum": 1.0, "step": 0.01}, + {"name": "top_k", "type": "number", "label": "top_k", "label_en": "Top-k", "default": 30, "minimum": 0, "step": 1, "precision": 0}, + {"name": "text_chunk_size", "type": "number", "label": "text_chunk_size", "label_en": "Text chunk size", "default": 1024, "minimum": 1, "step": 1, "precision": 0}, + {"name": "text_chunk_mode", "type": "choice", "label": "text_chunk_mode", "label_en": "Text chunk mode", "default": "default", "choices": ["default", "tag_aware", "japanese", "endline"]} + ], + + "vietneu_tts": [ + {"name": "do_sample", "type": "bool", "label": "do_sample", "label_en": "Sample", "default": true}, + {"name": "temperature", "type": "slider", "label": "temperature", "label_en": "Temperature", "default": 0.9, "minimum": 0.0, "maximum": 2.0, "step": 0.05}, + {"name": "top_k", "type": "number", "label": "top_k", "label_en": "Top-k", "default": 50, "minimum": 0, "step": 1, "precision": 0}, + {"name": "top_p", "type": "slider", "label": "top_p", "label_en": "Top-p", "default": 1.0, "minimum": 0.0, "maximum": 1.0, "step": 0.01}, + {"name": "repetition_penalty", "type": "slider", "label": "repetition_penalty", "label_en": "Repetition penalty", "default": 1.05, "minimum": 1.0, "maximum": 2.0, "step": 0.01}, + {"name": "subtalker_do_sample", "type": "bool", "label": "subtalker_do_sample", "label_en": "Sub-talker: sample", "default": true}, + {"name": "subtalker_temperature", "type": "slider", "label": "subtalker_temperature", "label_en": "Sub-talker: temperature", "default": 0.9, "minimum": 0.0, "maximum": 2.0, "step": 0.05}, + {"name": "subtalker_top_k", "type": "number", "label": "subtalker_top_k", "label_en": "Sub-talker: top-k", "default": 50, "minimum": 0, "step": 1, "precision": 0}, + {"name": "subtalker_top_p", "type": "slider", "label": "subtalker_top_p", "label_en": "Sub-talker: top-p", "default": 1.0, "minimum": 0.0, "maximum": 1.0, "step": 0.01}, + {"name": "x_vector_only_mode", "type": "bool", "label": "x_vector_only_mode", "label_en": "Speaker-embedding-only mode", "default": false, "info_en": "Condition on the speaker embedding alone instead of in-context reference audio."}, + {"name": "text_chunk_size", "type": "number", "label": "text_chunk_size", "label_en": "Text chunk size", "default": 200, "minimum": 1, "step": 1, "precision": 0}, + {"name": "text_chunk_mode", "type": "choice", "label": "text_chunk_mode", "label_en": "Text chunk mode", "default": "default", "choices": ["default", "tag_aware", "japanese", "endline"]} + ], + + "soprano_tts": [ + {"name": "temperature", "type": "slider", "label": "temperature", "label_en": "Temperature", "default": 0.3, "minimum": 0.05, "maximum": 2.0, "step": 0.05, "info_en": "Must be above 0."}, + {"name": "top_p", "type": "slider", "label": "top_p", "label_en": "Top-p", "default": 0.95, "minimum": 0.0, "maximum": 1.0, "step": 0.01}, + {"name": "repetition_penalty", "type": "slider", "label": "repetition_penalty", "label_en": "Repetition penalty", "default": 1.2, "minimum": 1.0, "maximum": 2.0, "step": 0.01}, + {"name": "eos_bias", "type": "slider", "label": "eos_bias", "label_en": "End-of-speech bias", "default": 0.0, "minimum": -5.0, "maximum": 5.0, "step": 0.1, "info_en": "Positive values stop generation sooner."} + ], + + "muscriptor": [ + {"name": "output_format", "type": "choice", "label": "output_format", "label_en": "Output format", "default": "midi", "choices": ["midi", "json"]}, + {"name": "instruments", "type": "text", "label": "instruments", "label_en": "Instruments", "default": "", "placeholder_en": "Comma-separated instrument names; blank transcribes all"}, + {"name": "do_sample", "type": "bool", "label": "do_sample", "label_en": "Sample", "default": false}, + {"name": "temperature", "type": "slider", "label": "temperature", "label_en": "Temperature", "default": 1.0, "minimum": 0.0, "maximum": 2.0, "step": 0.05}, + {"name": "guidance_scale", "type": "slider", "label": "guidance_scale", "label_en": "Guidance scale", "default": 1.0, "minimum": 0.0, "maximum": 5.0, "step": 0.1, "info_en": "Exactly 1.0 disables classifier-free guidance; other values double the conditioning batch."}, + {"name": "num_beams", "type": "number", "label": "num_beams", "label_en": "Beam count", "default": 1, "minimum": 1, "step": 1, "precision": 0}, + {"name": "batch_size", "type": "number", "label": "batch_size", "label_en": "Batch size", "default": 1, "minimum": 1, "step": 1, "precision": 0, "info_en": "Values above 1 require prelude_forcing to be off."}, + {"name": "prelude_forcing", "type": "bool", "label": "prelude_forcing", "label_en": "Prelude forcing", "default": true} + ], + + "hviske_asr": [ + {"name": "punctuation", "type": "bool", "label": "punctuation", "label_en": "Punctuation", "default": true}, + {"name": "num_beams", "type": "number", "label": "num_beams", "label_en": "Beam count", "default": 1, "minimum": 1, "step": 1, "precision": 0}, + {"name": "length_penalty", "type": "slider", "label": "length_penalty", "label_en": "Length penalty", "default": 1.0, "minimum": 0.05, "maximum": 3.0, "step": 0.05, "info_en": "Must be above 0."}, + {"name": "do_sample", "type": "bool", "label": "do_sample", "label_en": "Sample", "default": false}, + {"name": "temperature", "type": "slider", "label": "temperature", "label_en": "Temperature", "default": 1.0, "minimum": 0.05, "maximum": 2.0, "step": 0.05, "info_en": "Must be above 0."}, + {"name": "top_k", "type": "number", "label": "top_k", "label_en": "Top-k", "default": 50, "minimum": 0, "step": 1, "precision": 0, "info_en": "0 disables top-k filtering."}, + {"name": "top_p", "type": "slider", "label": "top_p", "label_en": "Top-p", "default": 1.0, "minimum": 0.01, "maximum": 1.0, "step": 0.01}, + {"name": "audio_chunk_mode", "type": "choice", "label": "audio_chunk_mode", "label_en": "Audio chunk mode", "default": "auto", "choices": ["auto", "fixed", "quiet_energy", "none"]}, + {"name": "audio_chunk_duration_sec", "type": "number", "label": "audio_chunk_duration_sec", "label_en": "Audio chunk length (s)", "minimum": 0.001, "step": 1.0, "info_en": "Blank uses the packaged model's max_audio_clip_s."} + ], + + "kroko_asr": [ + {"name": "decoding_method", "type": "choice", "label": "decoding_method", "label_en": "Decoding method", "default": "greedy_search", "choices": ["greedy_search", "modified_beam_search"]}, + {"name": "num_beams", "type": "number", "label": "num_beams", "label_en": "Beam count", "default": 4, "minimum": 1, "maximum": 64, "step": 1, "precision": 0, "info_en": "Only used by modified_beam_search."}, + {"name": "blank_penalty", "type": "slider", "label": "blank_penalty", "label_en": "Blank penalty", "default": 0.0, "minimum": 0.0, "maximum": 5.0, "step": 0.1}, + {"name": "hotwords", "type": "text", "label": "hotwords", "label_en": "Hotwords", "default": "", "placeholder_en": "Phrases separated by / or newline; needs modified_beam_search"}, + {"name": "hotwords_score", "type": "slider", "label": "hotwords_score", "label_en": "Hotword boost", "default": 1.5, "minimum": 0.0, "maximum": 10.0, "step": 0.1}, + {"name": "enable_endpoint", "type": "bool", "label": "enable_endpoint", "label_en": "Endpoint detection", "default": false}, + {"name": "rule1_min_trailing_silence_sec", "type": "number", "label": "rule1_min_trailing_silence_sec", "label_en": "Endpoint rule 1: trailing silence (s)", "default": 2.4, "minimum": 0.0, "step": 0.1}, + {"name": "rule2_min_trailing_silence_sec", "type": "number", "label": "rule2_min_trailing_silence_sec", "label_en": "Endpoint rule 2: trailing silence (s)", "default": 1.2, "minimum": 0.0, "step": 0.1}, + {"name": "rule3_min_utterance_length_sec", "type": "number", "label": "rule3_min_utterance_length_sec", "label_en": "Endpoint rule 3: utterance length (s)", "default": 20.0, "minimum": 0.0, "step": 1.0} + ], + + "qwen3_asr": [ + {"name": "return_timestamps", "type": "bool", "label": "return_timestamps", "label_en": "Return timestamps", "default": false, "info_en": "Also shortens the automatic chunk length from 30 s to 15 s."}, + {"name": "clamp_timestamps_to_audio", "type": "bool", "label": "clamp_timestamps_to_audio", "label_en": "Clamp timestamps to audio length", "default": false}, + {"name": "audio_chunk_mode", "type": "choice", "label": "audio_chunk_mode", "label_en": "Audio chunk mode", "default": "auto", "choices": ["auto", "fixed", "vad", "none"]}, + {"name": "audio_chunk_duration_sec", "type": "number", "label": "audio_chunk_duration_sec", "label_en": "Audio chunk length (s)", "minimum": 0.001, "step": 1.0, "info_en": "Blank uses 30 s, or 15 s when timestamps or VAD chunking are on."} + ], + + "nemotron_asr": [ + {"name": "keep_language_tags", "type": "bool", "label": "keep_language_tags", "label_en": "Keep language tags", "default": false} + ], + + "vibevoice_asr": [ + {"name": "temperature", "type": "slider", "label": "temperature", "label_en": "Temperature", "default": 0.0, "minimum": 0.0, "maximum": 2.0, "step": 0.05, "info_en": "0 decodes deterministically."}, + {"name": "top_p", "type": "slider", "label": "top_p", "label_en": "Top-p", "default": 1.0, "minimum": 0.01, "maximum": 1.0, "step": 0.01}, + {"name": "top_k", "type": "number", "label": "top_k", "label_en": "Top-k", "default": 50, "minimum": 0, "step": 1, "precision": 0}, + {"name": "repetition_penalty", "type": "slider", "label": "repetition_penalty", "label_en": "Repetition penalty", "default": 1.0, "minimum": 0.05, "maximum": 2.0, "step": 0.01, "info_en": "Must be above 0."}, + {"name": "num_beams", "type": "number", "label": "num_beams", "label_en": "Beam count", "default": 1, "minimum": 1, "step": 1, "precision": 0}, + {"name": "audio_chunk_mode", "type": "choice", "label": "audio_chunk_mode", "label_en": "Audio chunk mode", "default": "auto", "choices": ["auto", "fixed", "vad", "none"]}, + {"name": "audio_chunk_seconds", "type": "number", "label": "audio_chunk_seconds", "label_en": "Audio chunk length (s)", "default": 1200.0, "minimum": 0.001, "step": 10.0} + ], + + "voxtral_realtime": [ + {"name": "do_sample", "type": "bool", "label": "do_sample", "label_en": "Sample", "default": false}, + {"name": "temperature", "type": "slider", "label": "temperature", "label_en": "Temperature", "default": 1.0, "minimum": 0.05, "maximum": 2.0, "step": 0.05, "info_en": "Must be above 0."}, + {"name": "top_p", "type": "slider", "label": "top_p", "label_en": "Top-p", "default": 1.0, "minimum": 0.01, "maximum": 1.0, "step": 0.01}, + {"name": "top_k", "type": "number", "label": "top_k", "label_en": "Top-k", "default": 50, "minimum": 0, "step": 1, "precision": 0}, + {"name": "max_new_tokens", "type": "number", "label": "max_new_tokens", "label_en": "Max new tokens", "minimum": 1, "step": 1, "precision": 0, "info_en": "Blank lets the model use the remaining audio-token budget. Voxtral-Realtime reads max_new_tokens, not max_tokens."} + ], + + "higgs_audio_stt": [ + {"name": "enable_thinking", "type": "bool", "label": "enable_thinking", "label_en": "Thinking mode", "default": true}, + {"name": "audio_chunk_mode", "type": "choice", "label": "audio_chunk_mode", "label_en": "Audio chunk mode", "default": "auto", "choices": ["auto", "fixed", "none"]}, + {"name": "audio_chunk_duration_sec", "type": "number", "label": "audio_chunk_duration_sec", "label_en": "Audio chunk length (s)", "default": 4.0, "minimum": 0.001, "step": 1.0} + ], + + "fun_asr_nano": [ + {"name": "enable_itn", "type": "bool", "label": "enable_itn", "label_en": "Inverse text normalization", "default": true}, + {"name": "audio_chunk_mode", "type": "choice", "label": "audio_chunk_mode", "label_en": "Audio chunk mode", "default": "auto", "choices": ["auto", "fixed", "none"]}, + {"name": "audio_chunk_seconds", "type": "number", "label": "audio_chunk_seconds", "label_en": "Audio chunk length (s)", "default": 30.0, "minimum": 0.001, "step": 1.0, "info_en": "Fun-ASR-Nano accepts only audio_chunk_seconds; the canonical audio_chunk_duration_sec is rejected."} + ], + + "parakeet_tdt": [ + {"name": "keep_language_tags", "type": "bool", "label": "keep_language_tags", "label_en": "Keep language tags", "default": false}, + {"name": "audio_chunk_mode", "type": "choice", "label": "audio_chunk_mode", "label_en": "Audio chunk mode", "default": "auto", "choices": ["auto", "fixed", "vad", "none"]}, + {"name": "audio_chunk_duration_sec", "type": "number", "label": "audio_chunk_duration_sec", "label_en": "Audio chunk length (s)", "minimum": 0.001, "step": 0.5, "info_en": "Blank uses the session's centre-window length (2.0 s by default)."} + ], + + "granite5asr": [ + {"name": "audio_chunk_mode", "type": "choice", "label": "audio_chunk_mode", "label_en": "Audio chunk mode", "default": "auto", "choices": ["auto", "fixed", "vad", "none"]}, + {"name": "audio_chunk_duration_sec", "type": "number", "label": "audio_chunk_duration_sec", "label_en": "Audio chunk length (s)", "default": 30.0, "minimum": 0.001, "step": 1.0} + ], + + "silero_vad": [ + {"name": "threshold", "type": "slider", "label": "threshold", "label_en": "Speech probability threshold", "default": 0.5, "minimum": 0.0, "maximum": 1.0, "step": 0.01, "info_en": "These options apply to offline runs. Streaming VAD uses the session configuration and ignores per-request values."}, + {"name": "min_speech_duration_ms", "type": "number", "label": "min_speech_duration_ms", "label_en": "Min speech duration (ms)", "default": 250, "minimum": 0, "step": 10, "precision": 0}, + {"name": "min_silence_duration_ms", "type": "number", "label": "min_silence_duration_ms", "label_en": "Min silence duration (ms)", "default": 100, "minimum": 0, "step": 10, "precision": 0}, + {"name": "speech_pad_ms", "type": "number", "label": "speech_pad_ms", "label_en": "Speech padding (ms)", "default": 30, "minimum": 0, "step": 10, "precision": 0}, + {"name": "min_silence_at_max_speech_ms", "type": "number", "label": "min_silence_at_max_speech_ms", "label_en": "Min silence at max speech (ms)", "default": 98, "minimum": 0, "step": 10, "precision": 0}, + {"name": "use_max_poss_sil_at_max_speech", "type": "bool", "label": "use_max_poss_sil_at_max_speech", "label_en": "Split at the longest silence when max speech is reached", "default": true} + ], + + "marblenet_vad": [ + {"name": "threshold", "type": "slider", "label": "threshold", "label_en": "Speech probability threshold", "default": 0.5, "minimum": 0.0, "maximum": 1.0, "step": 0.01} + ], + + "sortformer_diar": [ + {"name": "speaker_threshold", "type": "slider", "label": "speaker_threshold", "label_en": "Speaker probability threshold", "default": 0.5, "minimum": 0.0, "maximum": 1.0, "step": 0.01}, + {"name": "speaker_min_frames", "type": "number", "label": "speaker_min_frames", "label_en": "Min speaker frames", "default": 0, "minimum": 0, "step": 1, "precision": 0, "info_en": "0 disables the minimum-length filter."}, + {"name": "speaker_pad_frames", "type": "number", "label": "speaker_pad_frames", "label_en": "Speaker pad frames", "default": 0, "minimum": 0, "step": 1, "precision": 0} + ], + + "qwen3_forced_aligner": [ + {"name": "clamp_timestamps_to_audio", "type": "bool", "label": "clamp_timestamps_to_audio", "label_en": "Clamp timestamps to audio length", "default": false} ] } diff --git a/webui/native/dist/index.html b/webui/native/dist/index.html index fe87802a4..b3f3c3769 100644 --- a/webui/native/dist/index.html +++ b/webui/native/dist/index.html @@ -31,17 +31,17 @@
From 5dd76512b6c791b984b1d1ee3dc9577a4d8fe429 Mon Sep 17 00:00:00 2001 From: Warren B Date: Mon, 31 Aug 2026 01:07:33 +0100 Subject: [PATCH 4/5] webui: complete the shipped translations Seven keys were missing from all four translation files, and two more were missing from every language including English. - file.clear and file.preview sit on every file input in the app, so their absence left the localised UI visibly half-English. - task.midi labelled MuScriptor in English for every non-English user. - request.autoDuration, request.rewriteCaption, request.rewritingCaption and voice.configured were likewise untranslated. - studio.subtitle.conversion and studio.subtitle.separation did not exist at all, so the hero rendered the literal lookup key on those two tabs. The English entries are added alongside; these files carry the translations. Also fills in param.* coverage, which stood at roughly ten keys per language out of nearly three hundred. Chinese gains 104 keys back-filled from the original Chinese label, info and placeholder text already in model_params.json, which the build discards because catalog.ts strips Han characters -- so that wording was present in the repo but unreachable even for Chinese users. Italian, Polish and Russian gain 32 each, limited to entries with an English source; entries whose only source text is Chinese are left for a pass after those gain English variants. Note for anyone adding param.* keys: they are namespaced by spec family, not by the key used in model_params.json. Nine groups there are keyed by catalog entry id, and the lookup resolves the family first, so a key written with the group name never fires. Removes arena.subtitle and models.reinstall from the four files; neither is referenced by any component. Validation: python3 -m json.tool on each of the four language files cd webui/native && npm run build Key sets of the four files verified identical to each other outside param.*, and every key present in the English source. Known limitations: 33 entries across the three European languages remain the English string. Each was checked and left deliberately -- loanwords, acronyms and identical cognates such as Arena, Model, Backend, RTF and WER. The Russian runtime.backend is the one arguable case, where a Cyrillic transliteration would match the file's style; it is flagged rather than changed. --- webui/native/dist/index.html | 18 ++--- webui/native/lang/lang_it.json | 45 ++++++++++++- webui/native/lang/lang_pl.json | 45 ++++++++++++- webui/native/lang/lang_ru.json | 45 ++++++++++++- webui/native/lang/lang_zh.json | 117 ++++++++++++++++++++++++++++++++- 5 files changed, 249 insertions(+), 21 deletions(-) diff --git a/webui/native/dist/index.html b/webui/native/dist/index.html index b3f3c3769..39979e809 100644 --- a/webui/native/dist/index.html +++ b/webui/native/dist/index.html @@ -31,17 +31,17 @@
diff --git a/webui/native/lang/lang_it.json b/webui/native/lang/lang_it.json index d64880980..d8f744a32 100644 --- a/webui/native/lang/lang_it.json +++ b/webui/native/lang/lang_it.json @@ -134,7 +134,6 @@ "models.hfAccess": "Accesso HF richiesto", "models.model": "modello", "models.queued": "in coda", - "models.reinstall": "Reinstalla", "models.selected": "Selezionato", "models.sharedPackage": "Usa il pacchetto condiviso {name} mostrato sopra.", "models.sizeUnavailable": "dimensione non disponibile", @@ -150,7 +149,6 @@ "arena.title.tts": "Confronto TTS", "arena.title.vc": "Confronto conversione vocale", "arena.title.asr": "Confronto ASR", - "arena.subtitle": "Esegue lo stesso input sui modelli installati e sui pacchetti scelti, uno dopo l'altro.", "arena.subtitle.tts": "Esegue lo stesso testo sui modelli TTS e sui pacchetti scelti.", "arena.subtitle.vc": "Esegue lo stesso audio sorgente sui modelli di conversione vocale scelti.", "arena.subtitle.asr": "Esegue lo stesso audio sorgente sui modelli ASR scelti e confronta le trascrizioni.", @@ -256,6 +254,47 @@ "voice.recommendedClone": "consigliata per la clonazione", "voice.recording": "Registrazione voce di riferimento", "voice.requiredClone": "obbligatoria per questa clonazione vocale", - "voice.transcriptPlaceholder": "Digita le parole esatte dell'audio di riferimento oppure carica il file .txt corrispondente." + "voice.transcriptPlaceholder": "Digita le parole esatte dell'audio di riferimento oppure carica il file .txt corrispondente.", + "file.clear": "Cancella file", + "file.preview": "Anteprima", + "param.chatterbox.s3gen_cfg_rate.label": "s3gen_cfg_rate (guida della voce)", + "param.controlfoley.negative_prompt.placeholder": "prompt negativo opzionale", + "param.dramabox.duration_sec.label": "duration_sec (0 = automatico)", + "param.echo_tts.guidance_interval.info": "Aggiorna le corsie CFG non condizionate ogni N passaggi guidati. Valori più alti sono più veloci e funzionano meglio con molti passaggi; 1 offre la fedeltà massima.", + "param.echo_tts.guidance_interval.label": "Intervallo di guida", + "param.echo_tts.num_inference_steps.info": "Passaggi del campionatore Euler.", + "param.echo_tts.num_inference_steps.label": "Passaggi di campionamento", + "param.echo_tts.reference_duration_sec.info": "Taglia il riferimento del parlante prima della codifica. Circa 10 s di solito danno la clonazione migliore.", + "param.echo_tts.reference_duration_sec.label": "Taglio del riferimento (s)", + "param.echo_tts.seed.label": "Seed", + "param.echo_tts.speaker_guidance_scale.label": "Guida del parlante", + "param.echo_tts.text_guidance_scale.label": "Guida del testo", + "param.echo_tts.truncation_factor.label": "Troncamento del rumore", + "param.index_tts2.duration_factor.info": "Corrisponde al duration_factor ufficiale di IndexTTS2.5: scala la durata dell'output senza modificare timbro o contenuto", + "param.index_tts2.duration_factor.label": "duration_factor (moltiplicatore di durata; >1 più lento, <1 più veloce)", + "param.index_tts2.emotion_text.info": "Impostandolo si attiva il condizionamento emotivo.", + "param.index_tts2.emotion_text.label": "emotion_text (testo di riferimento dell'emozione)", + "param.index_tts2.lang.info": "Vale solo per i modelli IndexTTS2.5 (multilingua): auto sceglie zh quando il testo contiene caratteri Han, altrimenti en; imposta ja/es/ar esplicitamente", + "param.index_tts2.lang.label": "lang (suggerimento di lingua, solo modelli IndexTTS2.5)", + "param.index_tts2.use_emotion_text.label": "use_emotion_text (deduci dal testo)", + "param.irodori_tts.duration_sec.label": "duration_sec (0 = automatico)", + "param.magpie_tts.voice_id.label": "voice_id (voce inclusa nel pacchetto)", + "param.minimax_music3.ar_guidance_scale.info": "Guida senza classificatore del campionamento dei codici semantici e residui.", + "param.minimax_music3.ar_guidance_scale.label": "Scala di guida AR", + "param.minimax_music3.guidance_scale.label": "Scala di guida del flow", + "param.minimax_music3.num_inference_steps.info": "Passaggi Euler di flow matching per ogni finestra di denoising da 200 fotogrammi.", + "param.minimax_music3.num_inference_steps.label": "Passaggi di flow per finestra", + "param.neutts.voice_id.label": "voice_id (voce integrata)", + "param.personaplex.system_prompt.placeholder": "Lascia vuoto per usare la casella di testo come system prompt.", + "param.personaplex.voice_id.label": "voice_id (voce inclusa nel pacchetto)", + "param.rvc.output_sample_rate.label": "output_sample_rate (0 = predefinito della voce)", + "param.supertonic.voice.label": "voice (preset M = maschile, F = femminile)", + "request.autoDuration": "-1 = automatico", + "request.rewriteCaption": "Riscrivi descrizione", + "request.rewritingCaption": "Riscrittura descrizione...", + "studio.subtitle.conversion": "Trasforma una registrazione in un'altra voce conservando l'interpretazione parlata o cantata.", + "studio.subtitle.separation": "Separa una registrazione in voce, strumenti o altre tracce audio disponibili.", + "task.midi": "Da audio a MIDI", + "voice.configured": "Voci configurate" } } diff --git a/webui/native/lang/lang_pl.json b/webui/native/lang/lang_pl.json index e4aff0311..e2433136b 100644 --- a/webui/native/lang/lang_pl.json +++ b/webui/native/lang/lang_pl.json @@ -134,7 +134,6 @@ "models.hfAccess": "Wymagany dostęp do HF", "models.model": "model", "models.queued": "w kolejce", - "models.reinstall": "Zainstaluj ponownie", "models.selected": "Wybrano", "models.sharedPackage": "Używa wspólnego pakietu {name} pokazanego powyżej.", "models.sizeUnavailable": "rozmiar niedostępny", @@ -150,7 +149,6 @@ "arena.title.tts": "Porównanie TTS", "arena.title.vc": "Porównanie konwersji głosu", "arena.title.asr": "Porównanie ASR", - "arena.subtitle": "Uruchamia ten sam input na wybranych zainstalowanych modelach i wariantach pakietów, jeden po drugim.", "arena.subtitle.tts": "Uruchamia ten sam tekst na wybranych modelach TTS i wariantach pakietów.", "arena.subtitle.vc": "Uruchamia to samo audio źródłowe na wybranych modelach konwersji głosu.", "arena.subtitle.asr": "Uruchamia to samo audio źródłowe na wybranych modelach ASR i porównuje transkrypcje.", @@ -256,6 +254,47 @@ "voice.recommendedClone": "zalecana do klonowania", "voice.recording": "Nagrywanie głosu referencyjnego", "voice.requiredClone": "wymagana dla tego klonowania głosu", - "voice.transcriptPlaceholder": "Wpisz dokładne słowa z audio referencyjnego lub wczytaj pasujący plik .txt powyżej." + "voice.transcriptPlaceholder": "Wpisz dokładne słowa z audio referencyjnego lub wczytaj pasujący plik .txt powyżej.", + "file.clear": "Wyczyść plik", + "file.preview": "Podgląd", + "param.chatterbox.s3gen_cfg_rate.label": "s3gen_cfg_rate (naprowadzanie głosem)", + "param.controlfoley.negative_prompt.placeholder": "opcjonalne polecenie negatywne", + "param.dramabox.duration_sec.label": "duration_sec (0 = automatycznie)", + "param.echo_tts.guidance_interval.info": "Odświeża bezwarunkowe ścieżki CFG co N-ty krok naprowadzany. Większe wartości są szybsze i działają najlepiej przy większej liczbie kroków; 1 daje najwyższą wierność.", + "param.echo_tts.guidance_interval.label": "Interwał naprowadzania", + "param.echo_tts.num_inference_steps.info": "Kroki próbnika Eulera.", + "param.echo_tts.num_inference_steps.label": "Kroki próbkowania", + "param.echo_tts.reference_duration_sec.info": "Przytnij referencję mówcy przed kodowaniem. Około 10 s zwykle daje najlepsze klonowanie.", + "param.echo_tts.reference_duration_sec.label": "Przycięcie referencji (s)", + "param.echo_tts.seed.label": "Ziarno", + "param.echo_tts.speaker_guidance_scale.label": "Naprowadzanie mówcą", + "param.echo_tts.text_guidance_scale.label": "Naprowadzanie tekstem", + "param.echo_tts.truncation_factor.label": "Obcinanie szumu", + "param.index_tts2.duration_factor.info": "Odpowiada oficjalnemu duration_factor z IndexTTS2.5: skaluje czas trwania wyjścia bez zmiany barwy ani treści", + "param.index_tts2.duration_factor.label": "duration_factor (mnożnik czasu trwania; >1 wolniej, <1 szybciej)", + "param.index_tts2.emotion_text.info": "Ustawienie tego pola włącza warunkowanie emocjami.", + "param.index_tts2.emotion_text.label": "emotion_text (tekst referencyjny emocji)", + "param.index_tts2.lang.info": "Działa tylko w modelach IndexTTS2.5 (wielojęzycznych): auto wybiera zh, gdy tekst zawiera znaki Han, w przeciwnym razie en; ja/es/ar ustaw wprost", + "param.index_tts2.lang.label": "lang (podpowiedź języka, tylko modele IndexTTS2.5)", + "param.index_tts2.use_emotion_text.label": "use_emotion_text (wywnioskuj z tekstu)", + "param.irodori_tts.duration_sec.label": "duration_sec (0 = automatycznie)", + "param.magpie_tts.voice_id.label": "voice_id (głos z pakietu)", + "param.minimax_music3.ar_guidance_scale.info": "Naprowadzanie bez klasyfikatora przy próbkowaniu kodów semantycznych i rezydualnych.", + "param.minimax_music3.ar_guidance_scale.label": "Skala naprowadzania AR", + "param.minimax_music3.guidance_scale.label": "Skala naprowadzania flow", + "param.minimax_music3.num_inference_steps.info": "Kroki Eulera flow matching na każde 200-klatkowe okno odszumiania.", + "param.minimax_music3.num_inference_steps.label": "Kroki flow na okno", + "param.neutts.voice_id.label": "voice_id (głos wbudowany)", + "param.personaplex.system_prompt.placeholder": "Zostaw puste, aby użyć pola tekstowego jako system prompt.", + "param.personaplex.voice_id.label": "voice_id (głos z pakietu)", + "param.rvc.output_sample_rate.label": "output_sample_rate (0 = domyślna dla głosu)", + "param.supertonic.voice.label": "voice (M = głos męski, F = żeński)", + "request.autoDuration": "-1 = automatycznie", + "request.rewriteCaption": "Przepisz opis", + "request.rewritingCaption": "Przepisywanie opisu...", + "studio.subtitle.conversion": "Przekształcaj nagranie w inny głos, zachowując sposób mówienia lub śpiewania.", + "studio.subtitle.separation": "Rozdzielaj nagranie na wokal, instrumenty lub inne dostępne ścieżki.", + "task.midi": "Audio na MIDI", + "voice.configured": "Skonfigurowane głosy" } } diff --git a/webui/native/lang/lang_ru.json b/webui/native/lang/lang_ru.json index 7070c35c6..42032f5c3 100644 --- a/webui/native/lang/lang_ru.json +++ b/webui/native/lang/lang_ru.json @@ -134,7 +134,6 @@ "models.hfAccess": "Требуется доступ HF", "models.model": "модель", "models.queued": "в очереди", - "models.reinstall": "Переустановить", "models.selected": "Выбрано", "models.sharedPackage": "Использует общий пакет {name}, показанный выше.", "models.sizeUnavailable": "размер недоступен", @@ -150,7 +149,6 @@ "arena.title.tts": "Сравнение TTS", "arena.title.vc": "Сравнение преобразования голоса", "arena.title.asr": "Сравнение ASR", - "arena.subtitle": "Запускает один и тот же ввод через выбранные установленные модели и варианты пакетов по очереди.", "arena.subtitle.tts": "Запускает один и тот же текст через выбранные модели TTS и варианты пакетов.", "arena.subtitle.vc": "Запускает одно и то же исходное аудио через выбранные модели преобразования голоса.", "arena.subtitle.asr": "Запускает одно и то же исходное аудио через выбранные модели ASR и сравнивает расшифровки.", @@ -256,6 +254,47 @@ "voice.recommendedClone": "рекомендуется для клонирования", "voice.recording": "Запись эталонного голоса", "voice.requiredClone": "требуется для этого клонирования", - "voice.transcriptPlaceholder": "Введите точные слова из эталонного аудио или загрузите соответствующий файл .txt выше." + "voice.transcriptPlaceholder": "Введите точные слова из эталонного аудио или загрузите соответствующий файл .txt выше.", + "file.clear": "Очистить файл", + "file.preview": "Предпросмотр", + "param.chatterbox.s3gen_cfg_rate.label": "s3gen_cfg_rate (управление голосом)", + "param.controlfoley.negative_prompt.placeholder": "необязательный негативный запрос", + "param.dramabox.duration_sec.label": "duration_sec (0 = автоматически)", + "param.echo_tts.guidance_interval.info": "Обновляет безусловные линии CFG на каждом N-м управляемом шаге. Больше — быстрее и лучше работает при большем числе шагов; 1 даёт максимальную точность.", + "param.echo_tts.guidance_interval.label": "Интервал управления", + "param.echo_tts.num_inference_steps.info": "Шаги сэмплера Эйлера.", + "param.echo_tts.num_inference_steps.label": "Шаги сэмплирования", + "param.echo_tts.reference_duration_sec.info": "Обрезает эталон голоса перед кодированием. Около 10 с обычно дают лучшее клонирование.", + "param.echo_tts.reference_duration_sec.label": "Обрезка эталона (с)", + "param.echo_tts.seed.label": "Seed", + "param.echo_tts.speaker_guidance_scale.label": "Управление по голосу", + "param.echo_tts.text_guidance_scale.label": "Управление по тексту", + "param.echo_tts.truncation_factor.label": "Усечение шума", + "param.index_tts2.duration_factor.info": "Соответствует официальному duration_factor из IndexTTS2.5: масштабирует длительность вывода, не меняя тембр и содержание", + "param.index_tts2.duration_factor.label": "duration_factor (множитель длительности; >1 медленнее, <1 быстрее)", + "param.index_tts2.emotion_text.info": "Заполнение этого поля включает управление эмоцией.", + "param.index_tts2.emotion_text.label": "emotion_text (эталонный текст эмоции)", + "param.index_tts2.lang.info": "Действует только для моделей IndexTTS2.5 (многоязычных): auto выбирает zh, если в тексте есть иероглифы, иначе en; ja/es/ar задавайте явно", + "param.index_tts2.lang.label": "lang (подсказка языка, только модели IndexTTS2.5)", + "param.index_tts2.use_emotion_text.label": "use_emotion_text (определять по тексту)", + "param.irodori_tts.duration_sec.label": "duration_sec (0 = автоматически)", + "param.magpie_tts.voice_id.label": "voice_id (голос из пакета)", + "param.minimax_music3.ar_guidance_scale.info": "Управление без классификатора при сэмплировании семантических и остаточных кодов.", + "param.minimax_music3.ar_guidance_scale.label": "Масштаб управления AR", + "param.minimax_music3.guidance_scale.label": "Масштаб управления flow", + "param.minimax_music3.num_inference_steps.info": "Шаги Эйлера flow matching на каждое окно шумоподавления в 200 кадров.", + "param.minimax_music3.num_inference_steps.label": "Шаги flow на окно", + "param.neutts.voice_id.label": "voice_id (встроенный голос)", + "param.personaplex.system_prompt.placeholder": "Оставьте пустым, чтобы использовать текстовое поле как system prompt.", + "param.personaplex.voice_id.label": "voice_id (голос из пакета)", + "param.rvc.output_sample_rate.label": "output_sample_rate (0 = по умолчанию для голоса)", + "param.supertonic.voice.label": "voice (M = мужской, F = женский пресет)", + "request.autoDuration": "-1 = автоматически", + "request.rewriteCaption": "Переписать описание", + "request.rewritingCaption": "Переписывание описания...", + "studio.subtitle.conversion": "Преобразуйте запись в другой голос, сохраняя манеру речи или пения.", + "studio.subtitle.separation": "Разделяйте запись на вокал, инструменты и другие доступные аудиодорожки.", + "task.midi": "Аудио в MIDI", + "voice.configured": "Настроенные голоса" } } diff --git a/webui/native/lang/lang_zh.json b/webui/native/lang/lang_zh.json index 1c9ca0207..56e4cae1b 100644 --- a/webui/native/lang/lang_zh.json +++ b/webui/native/lang/lang_zh.json @@ -134,7 +134,6 @@ "models.hfAccess": "需要 HF 访问权限", "models.model": "模型", "models.queued": "排队中", - "models.reinstall": "重新安装", "models.selected": "已选择", "models.sharedPackage": "使用上方显示的共享 {name} 软件包。", "models.sizeUnavailable": "大小不可用", @@ -150,7 +149,6 @@ "arena.title.tts": "TTS 对比", "arena.title.vc": "语音转换对比", "arena.title.asr": "ASR 对比", - "arena.subtitle": "用同一输入依次运行已选择的已安装模型和精度包,方便比较输出。", "arena.subtitle.tts": "用同一文本依次运行已选择的 TTS 模型和精度包。", "arena.subtitle.vc": "用同一源音频依次运行已选择的语音转换模型。", "arena.subtitle.asr": "用同一源音频依次运行已选择的 ASR 模型并比较转录结果。", @@ -256,6 +254,119 @@ "voice.recommendedClone": "建议用于克隆", "voice.recording": "正在录制参考音色", "voice.requiredClone": "此声音克隆必需", - "voice.transcriptPlaceholder": "输入参考音频中的准确文字,或加载上方匹配的 .txt 文件。" + "voice.transcriptPlaceholder": "输入参考音频中的准确文字,或加载上方匹配的 .txt 文件。", + "file.clear": "清除文件", + "file.preview": "预览", + "param.ace_step.audio_cover_strength.info": "1=贴近原曲,0=自由发挥;建议 0.5", + "param.ace_step.bpm.info": "0=不指定", + "param.ace_step.bpm.label": "【曲谱】BPM", + "param.ace_step.cover_noise_strength.info": "保旋律强度;推荐 0.1~0.25", + "param.ace_step.flow_edit_n_avg.info": "每步多次采样取平均(remix 默认 2);1=最快", + "param.ace_step.flow_edit_n_max.info": "唱不出新歌词时降到 0.7~0.9", + "param.ace_step.flow_edit_n_min.info": "调大更保源曲、换词更弱", + "param.ace_step.keyscale.label": "【曲谱】keyscale", + "param.ace_step.keyscale.placeholder": "如 F major", + "param.ace_step.num_inference_steps.info": "扩散步数(turbo 上限 20);remix 路由不填时默认 16,其他路由默认 8", + "param.ace_step.route.info": "cover/remix=换词翻唱,非 text2music 需上传源音频;详见 webui/README.md", + "param.ace_step.route.label": "route(操作类型)", + "param.ace_step.shift.info": "原版 turbo 默认 3.0;1.0 会明显劣化 remix 换词咬字", + "param.ace_step.shift.label": "shift(时间步弯曲)", + "param.ace_step.source_caption.placeholder": "源歌曲描述;『🔍 分析』自动填", + "param.ace_step.source_lyrics.placeholder": "源歌曲原歌词;『🔍 分析』自动填", + "param.ace_step.timesignature.label": "【曲谱】timesignature", + "param.ace_step.timesignature.placeholder": "如 4", + "param.chatterbox.exaggeration.info": "改动后需重新『加载模型』才生效", + "param.chatterbox.guidance_scale.info": "改动后需重新『加载模型』才生效", + "param.chatterbox.num_inference_steps.label": "num_inference_steps(生成步数)", + "param.chatterbox.s3gen_cfg_rate.label": "s3gen_cfg_rate(音色引导强度)", + "param.dramabox.audio_chunk_duration_sec.label": "audio_chunk_duration_sec(长文本分段目标时长)", + "param.dramabox.audio_chunk_threshold_sec.label": "audio_chunk_threshold_sec(长文本阈值)", + "param.dramabox.cross_fade_duration_sec.label": "cross_fade_duration_sec(分段交叉淡化)", + "param.dramabox.duration_scale.label": "duration_scale(自动估时倍率)", + "param.dramabox.duration_sec.label": "duration_sec(0=自动估时)", + "param.dramabox.guidance_rescale.placeholder": "auto 或数值", + "param.dramabox.negative_prompt.label": "negative_prompt(负向提示)", + "param.dramabox.negative_prompt.placeholder": "留空=模型内置质量提示", + "param.dramabox.reference_duration_sec.label": "reference_duration_sec(参考音频裁剪/重复秒数)", + "param.heartmula.infinite_mode.label": "infinite_mode(长输出分段生成)", + "param.heartmula.num_inference_steps.label": "num_inference_steps(codec 步数)", + "param.heartmula.tags.info": "风格/情绪/乐器/人声标签,模型必需", + "param.heartmula.tags.label": "tags(必填,逗号分隔)", + "param.index_tts2.duration_factor.info": "对齐官方 IndexTTS2.5 的 duration_factor:缩放输出时长,不改变音色/内容", + "param.index_tts2.duration_factor.label": "duration_factor(语速/时长倍率,>1 更慢,<1 更快)", + "param.index_tts2.emotion_alpha.label": "emotion_alpha(情感强度)", + "param.index_tts2.emotion_text.info": "填写后自动开启情感条件(use_emotion_text)", + "param.index_tts2.emotion_text.label": "emotion_text(情绪参考文本)", + "param.index_tts2.emotion_text.placeholder": "例:你吓死我了!你是鬼吗?", + "param.index_tts2.interval_silence_ms.label": "interval_silence_ms(分段间静音)", + "param.index_tts2.lang.info": "仅对 IndexTTS2.5(多语种)模型生效:auto 含汉字按中文,否则按英文;日/西/阿建议显式选择", + "param.index_tts2.lang.label": "lang(语种提示, 仅 IndexTTS2.5 模型)", + "param.index_tts2.use_emotion_text.label": "use_emotion_text(从朗读文本推断情感)", + "param.index_tts2.use_random_emotion.label": "use_random_emotion(随机情感)", + "param.inflect_v2.speaking_rate.label": "speaking_rate(语速倍率)", + "param.inflect_v2.text_chunk_size.label": "text_chunk_size(长文本分段字符数)", + "param.inflect_v2.variation.label": "variation(音色变化)", + "param.irodori_tts.duration_scale.label": "duration_scale(语速倒数,越大越慢)", + "param.irodori_tts.duration_sec.label": "duration_sec(0=模型自动预测时长)", + "param.irodori_tts.num_inference_steps.label": "num_inference_steps(RF 扩散步数)", + "param.magpie_tts.voice_id.label": "voice_id(打包音色)", + "param.miotts.best_of_n.label": "best_of_n(候选数,>1 自动开启)", + "param.neutts.emotion.label": "emotion(情绪)", + "param.neutts.voice_id.label": "voice_id(内置音色)", + "param.omnivoice.instruct.label": "instruct(风格/音色指令)", + "param.omnivoice.instruct.placeholder": "如:以轻快的语气朗读", + "param.personaplex.voice_id.label": "voice_id(打包音色)", + "param.pocket_tts.frames_after_eos.label": "frames_after_eos(-1=自动)", + "param.qwen3_tts.instruct.label": "instruct(仅 VoiceDesign/CustomVoice)", + "param.qwen3_tts.instruct.placeholder": "风格/音色指令,Base 版忽略", + "param.qwen3_tts.speaker.label": "speaker(仅 CustomVoice)", + "param.qwen3_tts.speaker.placeholder": "内置音色名,其它版忽略", + "param.rvc.output_sample_rate.label": "output_sample_rate(0=跟随音色)", + "param.rvc.retrieval_index_path.placeholder": "可选 .index 路径", + "param.rvc.semitone_shift.label": "semitone_shift(半音变调)", + "param.rvc.voice_id.label": "voice_id(打包音色)", + "param.rvc.voice_model_path.label": "voice_model_path(自定义 RVC .pth/.pt)", + "param.rvc.voice_model_path.placeholder": "留空=使用打包音色", + "param.seed_vc.inference_cfg_rate.label": "inference_cfg_rate(仅 v1 路径)", + "param.seed_vc.intelligibility_cfg_rate.label": "intelligibility_cfg_rate(仅 v2_vc)", + "param.seed_vc.length_adjust.label": "length_adjust(时长伸缩)", + "param.seed_vc.num_inference_steps.info": "CFM 扩散步数", + "param.seed_vc.route.info": "留空=按任务默认", + "param.seed_vc.route.label": "route(转换路径)", + "param.seed_vc.similarity_cfg_rate.label": "similarity_cfg_rate(仅 v2_vc)", + "param.sense_asr.enable_itn.label": "enable_itn(逆文本规范化)", + "param.sense_asr.keep_tags.label": "keep_tags(保留语言/情绪/事件标签)", + "param.stable_audio.audio_input_kind.label": "audio_input_kind(仅上传源音频时生效)", + "param.stable_audio.init_noise_level.label": "init_noise_level(init_audio 强度)", + "param.stable_audio.num_inference_steps.info": "RF 扩散步数", + "param.supertonic.num_inference_steps.label": "num_inference_steps(流匹配步数)", + "param.supertonic.speaking_rate.label": "speaking_rate(语速倍率)", + "param.supertonic.voice.label": "voice(预置音色:M 男声 / F 女声)", + "param.vevo2.num_inference_steps.info": "流匹配步数", + "param.vevo2.route.info": "留空=按任务默认;详见 webui/README.md", + "param.vevo2.route.label": "route(任务路线)", + "param.vevo2.temperature.info": "默认取自模型 generation_config.json", + "param.vevo2.temperature.label": "temperature(AR 路线用)", + "param.vevo2.top_k.info": "默认取自模型 generation_config.json", + "param.vevo2.top_k.label": "top_k(AR 路线用)", + "param.vevo2.top_p.label": "top_p(AR 路线用)", + "param.vevo2.use_pitch_shift.info": "留空=按路线默认", + "param.vevo2.use_pitch_shift.label": "use_pitch_shift(自动音高对齐)", + "param.vibevoice.guidance_scale.info": "CFG 引导强度", + "param.vibevoice.max_length_times.info": "最大输出长度倍数", + "param.vibevoice.num_inference_steps.info": "扩散步数(官方默认 10),越大越慢越稳", + "param.vibevoice.voice_samples.label": "voice_samples(多说话人,逗号分隔 wav,≤4)", + "param.vibevoice.voice_samples.placeholder": "D:/a.wav,D:/b.wav — 用此项时勿再上传参考音色", + "param.voxcpm1.num_inference_steps.info": "CFM/DiT 步数", + "param.voxcpm1.retry_badcase.label": "retry_badcase(自动重试异常输出)", + "param.voxcpm2.num_inference_steps.info": "CFM/DiT 步数", + "param.voxcpm2.retry_badcase.label": "retry_badcase(自动重试异常输出)", + "request.autoDuration": "-1 = 自动", + "request.rewriteCaption": "重写描述", + "request.rewritingCaption": "正在重写描述...", + "studio.subtitle.conversion": "将录音转换为另一种声音,同时保留原有的说话或演唱表现。", + "studio.subtitle.separation": "将录音分离为人声、乐器或其他可用音轨。", + "task.midi": "音频转 MIDI", + "voice.configured": "已配置音色" } } From ed3ea432600a66695c350abcf1e3bfa07dcb2878 Mon Sep 17 00:00:00 2001 From: Warren B Date: Mon, 31 Aug 2026 01:08:01 +0100 Subject: [PATCH 5/5] tools: validate the WebUI catalog in the sync check The sync check compared registered loaders, model_specs and model_manager_v2 and reported "in sync" while never opening webui/configs/models_catalog.json, the file that decides which models the WebUI shows. That gap is why 46 catalog entries could name a download_id that is not a package id, 40 could name a path no package installs, one could point at the package its own spec demoted, and four complete model families could be invisible -- with no CI signal for any of it. Adds a catalog pass: - entry family exists in model_specs - download_id is an exact packages[].id in that entry's own family - path matches the resolved package's target directory or installed file - every spec family with installable GGUF packages has an entry (warning) - every distinct installable GGUF target directory is reachable (warning), which is what catches shipped-but-unreachable checkpoints - each spec's default and ui.recommended_package can actually be selected - task and mode are in the vocabularies parsed out of session.cpp rather than a hardcoded list - duplicate entry ids Deliberate exclusions are derived, not listed: a package counts as installable only when its effective download.kind is huggingface_snapshot, so the two families that carry kind "unsupported" for licence reasons drop out on their own. Also models the native package manager's kind filter, which the previous summary line did not: it now reports manager_packages and server_packages separately, exposing that the Python tool this check measures knows four more packages than the C++ manager the server actually runs. Finally, cross-checks model_params.json controls against the specs, scoped to families that document their request surface -- an empty options.request means undocumented, not unsupported, and checking those too produces roughly eighty false reports. Bundled families with no spec are skipped, since there is no declared surface to compare against. Validation: python3 tools/check_loader_catalog_sync.py # exit 0, 31 warnings, all # pre-existing informational python3 tools/check_loader_catalog_sync.py --self-test # 10 tests, OK Each new error class was mutation-tested against a deliberately broken copy of the catalog -- unknown family, family-name download_id, cross-family download_id, wrong path, unknown task, unknown mode, bundled entry carrying a download_id, duplicate id, and an entry repointed off its family default -- rather than trusted on a passing run. Known limitations: the reachability model mirrors the front end's target-directory grouping without parsing catalog.ts, so a future change to the resolution rules must be reflected here by hand. The parameter cross-check is a warning, not an error, because option strictness is per-loader and cannot be proven from the specs alone. Self-tests extend the existing in-file --self-test rather than adding a suite, matching how the four CI workflows already invoke this script. --- tools/check_loader_catalog_sync.py | 611 ++++++++++++++++++++++++++++- 1 file changed, 604 insertions(+), 7 deletions(-) diff --git a/tools/check_loader_catalog_sync.py b/tools/check_loader_catalog_sync.py index e22ebf5d8..18dcd3605 100644 --- a/tools/check_loader_catalog_sync.py +++ b/tools/check_loader_catalog_sync.py @@ -1,11 +1,18 @@ #!/usr/bin/env python3 -"""Check sync between runtime loaders, model_specs packages, and model_manager_v2. +"""Check sync between runtime loaders, model_specs packages, model_manager_v2, +and the WebUI catalog. Schema correctness is owned by the typed model-spec validator. This script only checks cross-system drift: - registered loader families vs model_specs families - model_specs packages vs model_manager_v2 package output +- model_specs packages vs the packages the native package manager publishes +- webui/configs/models_catalog.json vs model_specs (family, download id, + install path, task/mode vocabulary) and vs the packages a catalog entry can + actually reach +- webui/configs/model_params.json parameter groups vs the request options the + owning spec documents See docs/maintainers/loader_and_catalog.md. """ @@ -24,14 +31,27 @@ CMAKE_PATH = REPO_ROOT / "CMakeLists.txt" REGISTRY_PATH = REPO_ROOT / "src" / "framework" / "runtime" / "registry.cpp" SPECS_DIR = REPO_ROOT / "model_specs" +CATALOG_PATH = REPO_ROOT / "webui" / "configs" / "models_catalog.json" +MODEL_PARAMS_PATH = REPO_ROOT / "webui" / "configs" / "model_params.json" +SESSION_PATH = REPO_ROOT / "src" / "framework" / "runtime" / "session.cpp" _LOADER_CALL_RE = re.compile(r"\bmake_([a-z0-9_]+)_loader(?:\s*\(\s*\))?") +_VALUE_COMPARE_RE = re.compile(r'value\s*==\s*"([a-z0-9_]+)"') BUNDLED_LOADERS_WITHOUT_SPEC = { "marblenet_vad", "silero_vad", } +# Bundled loaders ship inside the repository instead of a downloadable package, +# so their catalog entries carry no download id and point outside models/. +BUNDLED_CATALOG_PATH_PREFIX = "assets/framework/models/" + +# src/framework/package_manager/manager.cpp drops every package whose download +# kind is not a Hugging Face snapshot, so those packages are invisible to the +# server and to the WebUI install flow no matter what model_manager_v2 lists. +INSTALLABLE_DOWNLOAD_KIND = "huggingface_snapshot" + @dataclass(frozen=True) class SpecPackage: @@ -40,6 +60,43 @@ class SpecPackage: format: str target_directory: str default: bool + download_kind: str = "" + files: tuple[str, ...] = () + strip_prefix: str = "" + + @property + def installable(self) -> bool: + return self.download_kind == INSTALLABLE_DOWNLOAD_KIND + + @property + def install_paths(self) -> set[str]: + """Catalog paths that resolve to this package once it is installed. + + Either the install directory itself or one of the files the package + drops into it. Both spellings appear in models_catalog.json and both + are accepted by the loaders. + """ + root = normalize_catalog_path(f"models/{self.target_directory}") + paths = {root} + prefix = normalize_catalog_path(self.strip_prefix) + for remote in self.files: + relative = normalize_catalog_path(remote) + if prefix and relative.startswith(f"{prefix}/"): + relative = relative[len(prefix) + 1:] + if relative: + paths.add(normalize_catalog_path(f"{root}/{relative}")) + return paths + + +@dataclass(frozen=True) +class CatalogEntry: + index: int + id: str + family: str + path: str + task: str + mode: str + download_id: str def rel(path: Path) -> str: @@ -49,6 +106,27 @@ def rel(path: Path) -> str: return str(path) +def normalize_catalog_path(value: str) -> str: + text = value.strip().replace("\\", "/") + while text.startswith("./"): + text = text[2:] + text = re.sub(r"/+", "/", text) + return text.rstrip("/") + + +def parse_string_vocabulary(text: str, signature: str) -> set[str]: + """Collect the `value == "..."` literals a session.cpp parser accepts.""" + start = text.find(signature) + if start < 0: + raise ValueError(f"'{signature}' not found") + end = text.find("\n}\n", start) + body = text[start:] if end < 0 else text[start:end] + values = set(_VALUE_COMPARE_RE.findall(body)) + if not values: + raise ValueError(f"'{signature}' declares no accepted values") + return values + + def parse_loader_declarations(text: str, comment_prefix: str) -> tuple[set[str], set[str]]: active: set[str] = set() commented: set[str] = set() @@ -110,6 +188,11 @@ def load_spec_packages(specs_dir: Path) -> tuple[dict[str, dict[str, Any]], dict errors.append(f"{rel(path)}: duplicate spec family '{family}'") specs_by_family[family] = spec + spec_defaults = spec.get("package_defaults") + default_download = {} + if isinstance(spec_defaults, dict) and isinstance(spec_defaults.get("download"), dict): + default_download = spec_defaults["download"] + packages = spec.get("packages", []) if packages is None: packages = [] @@ -130,16 +213,72 @@ def load_spec_packages(specs_dir: Path) -> tuple[dict[str, dict[str, Any]], dict f"model_specs/{packages_by_id[package_id].family}.json" ) continue + download = package.get("download") + if not isinstance(download, dict): + download = {} + files = package.get("files") + if not isinstance(files, list): + files = [] packages_by_id[package_id] = SpecPackage( family=family, id=package_id, format=str(package.get("format") or ""), target_directory=str(package.get("target_directory") or ""), default=package.get("default") is True, + download_kind=str(download.get("kind") or default_download.get("kind") or ""), + files=tuple(str(item) for item in files if isinstance(item, str)), + strip_prefix=str(package.get("strip_prefix") or ""), ) return specs_by_family, packages_by_id, errors +def recommended_package_id(spec: dict[str, Any]) -> str: + ui = spec.get("ui") + if not isinstance(ui, dict): + return "" + value = ui.get("recommended_package") + return value if isinstance(value, str) else "" + + +def load_catalog(path: Path) -> tuple[list[CatalogEntry], list[str]]: + errors: list[str] = [] + try: + payload = load_json(path) + except json.JSONDecodeError as exc: + return [], [f"{rel(path)}: invalid JSON: {exc}"] + if not isinstance(payload, dict) or not isinstance(payload.get("models"), list): + return [], [f"{rel(path)}: top-level JSON must be an object with a 'models' list"] + + entries: list[CatalogEntry] = [] + seen_ids: set[str] = set() + for index, row in enumerate(payload["models"]): + if not isinstance(row, dict): + errors.append(f"{rel(path)}: models[{index}] must be an object") + continue + entry_id = row.get("id") + if not isinstance(entry_id, str) or not entry_id: + errors.append(f"{rel(path)}: models[{index}] missing id") + continue + if entry_id in seen_ids: + errors.append(f"{rel(path)}: duplicate catalog entry id '{entry_id}'") + continue + seen_ids.add(entry_id) + family = row.get("family") + if not isinstance(family, str) or not family: + errors.append(f"{rel(path)}: catalog entry '{entry_id}' missing family") + continue + entries.append(CatalogEntry( + index=index, + id=entry_id, + family=family, + path=str(row.get("path") or ""), + task=str(row.get("task") or ""), + mode=str(row.get("mode") or ""), + download_id=str(row.get("download_id") or ""), + )) + return entries, errors + + def load_manager_packages(specs_dir: Path) -> tuple[dict[str, Any], list[str]]: sys.path.insert(0, str(REPO_ROOT / "tools")) import model_manager_v2 # noqa: E402 @@ -210,6 +349,289 @@ def check_manager_sync(spec_packages: dict[str, SpecPackage], manager_packages: return errors, warnings +def check_native_manager_sync(spec_packages: dict[str, SpecPackage]) -> list[str]: + """Report packages model_manager_v2 lists but the native manager drops. + + tools/model_manager_v2.py flattens every declared package; the server keeps + only Hugging Face snapshots (src/framework/package_manager/manager.cpp), so + the two package counts are not interchangeable. + """ + warnings: list[str] = [] + for package in sorted(spec_packages.values(), key=lambda item: item.id): + if package.installable: + continue + warnings.append( + f"model_specs/{package.family}.json package '{package.id}' has download.kind=" + f"'{package.download_kind or 'missing'}' and is not published by the native " + f"package manager (server-side package list excludes it)" + ) + return warnings + + +def check_catalog_sync( + catalog_entries: list[CatalogEntry], + specs_by_family: dict[str, dict[str, Any]], + spec_packages: dict[str, SpecPackage], + task_kinds: set[str], + run_modes: set[str], + catalog_path: Path, +) -> tuple[list[str], list[str]]: + """Check webui/configs/models_catalog.json against model_specs. + + A catalog entry resolves to exactly one package through its download id. + The WebUI then offers every GGUF package that installs into the resolved + package's target directory (webui/native/src/lib/catalog.ts), so a target + directory no entry resolves into is unreachable from the UI. + """ + errors: list[str] = [] + warnings: list[str] = [] + name = rel(catalog_path) + + packages_by_family: dict[str, list[SpecPackage]] = {} + for package in spec_packages.values(): + packages_by_family.setdefault(package.family, []).append(package) + + reachable_ids: set[str] = set() + families_in_catalog: set[str] = set() + + for entry in catalog_entries: + if entry.task and entry.task not in task_kinds: + errors.append( + f"{name}: catalog entry '{entry.id}' task '{entry.task}' is not accepted by " + f"parse_voice_task_kind (expected one of {', '.join(sorted(task_kinds))})" + ) + if entry.mode and entry.mode not in run_modes: + errors.append( + f"{name}: catalog entry '{entry.id}' mode '{entry.mode}' is not accepted by " + f"parse_run_mode (expected one of {', '.join(sorted(run_modes))})" + ) + + if entry.family in BUNDLED_LOADERS_WITHOUT_SPEC: + if entry.download_id: + errors.append( + f"{name}: catalog entry '{entry.id}' is a bundled loader but names " + f"download_id '{entry.download_id}'" + ) + if not normalize_catalog_path(entry.path).startswith(BUNDLED_CATALOG_PATH_PREFIX): + errors.append( + f"{name}: catalog entry '{entry.id}' is bundled and must point inside " + f"{BUNDLED_CATALOG_PATH_PREFIX} (path '{entry.path}')" + ) + continue + + if entry.family not in specs_by_family: + errors.append( + f"{name}: catalog entry '{entry.id}' names family '{entry.family}' " + f"which has no model_specs/{entry.family}.json" + ) + continue + families_in_catalog.add(entry.family) + + family_packages = packages_by_family.get(entry.family, []) + recommended_id = recommended_package_id(specs_by_family[entry.family]) + resolved = spec_packages.get(entry.download_id) if entry.download_id else None + degraded = False + if not entry.download_id: + warnings.append( + f"{name}: catalog entry '{entry.id}' has no download_id; its install location " + f"cannot be checked against model_specs/{entry.family}.json" + ) + elif resolved is None: + errors.append( + f"{name}: catalog entry '{entry.id}' download_id '{entry.download_id}' is not a " + f"packages[].id in model_specs/{entry.family}.json" + ) + elif resolved.family != entry.family: + errors.append( + f"{name}: catalog entry '{entry.id}' download_id '{entry.download_id}' belongs to " + f"model_specs/{resolved.family}.json, not to family '{entry.family}'" + ) + resolved = None + if resolved is None and entry.download_id: + # Fall back to the family recommendation so a broken download id + # does not also suppress the path and reachability checks. The + # download id error above is the one to fix first. + fallback = spec_packages.get(recommended_id) + if fallback is not None and fallback.family == entry.family: + resolved = fallback + degraded = True + + if resolved is None: + continue + + if entry.path: + candidate = normalize_catalog_path(entry.path) + accepted = resolved.install_paths + source = ( + f"model_specs/{entry.family}.json recommended package '{resolved.id}'" + if degraded else f"package '{resolved.id}'" + ) + if candidate not in accepted: + lowered = {value.lower() for value in accepted} + if candidate.lower() in lowered: + warnings.append( + f"{name}: catalog entry '{entry.id}' path '{entry.path}' differs in case " + f"from the install location of {source}" + ) + else: + errors.append( + f"{name}: catalog entry '{entry.id}' path '{entry.path}' is neither the " + f"target_directory nor an installed file of {source} " + f"(expected models/{resolved.target_directory}[/])" + ) + + exposed = [ + package for package in family_packages + if package.target_directory == resolved.target_directory + and package.format == "gguf" and package.installable + ] + if not exposed: + warnings.append( + f"{name}: catalog entry '{entry.id}' resolves to package '{resolved.id}' " + f"(format={resolved.format or 'unknown'}), which the native model manager " + f"cannot install; the entry has no install choice" + ) + reachable_ids.update(package.id for package in exposed) + + if (not degraded and recommended_id and resolved.id != recommended_id + and not resolved.default): + recommended = spec_packages.get(recommended_id) + if recommended is not None and recommended.target_directory == resolved.target_directory: + warnings.append( + f"{name}: catalog entry '{entry.id}' names package '{resolved.id}' while " + f"model_specs/{entry.family}.json recommends '{recommended_id}' from the " + f"same install location" + ) + + for family in sorted(specs_by_family): + family_packages = packages_by_family.get(family, []) + installable = [ + package for package in family_packages + if package.format == "gguf" and package.installable + ] + if not installable: + # Families whose packages are all non-distributable (download.kind + # is not a Hugging Face snapshot) are deliberately absent from the + # UI, so their absence is not reported. + continue + if family not in families_in_catalog: + warnings.append( + f"model_specs/{family}.json publishes {len(installable)} installable GGUF " + f"package(s) but no {name} entry exposes the family" + ) + continue + + directories: dict[str, list[str]] = {} + for package in installable: + directories.setdefault(package.target_directory, []).append(package.id) + for directory in sorted(directories): + if any(package_id in reachable_ids for package_id in sorted(directories[directory])): + continue + warnings.append( + f"model_specs/{family}.json installs {', '.join(sorted(directories[directory]))} " + f"into '{directory}', which no {name} entry reaches" + ) + + recommended_id = recommended_package_id(specs_by_family[family]) + recommended = spec_packages.get(recommended_id) if recommended_id else None + if recommended_id and (recommended is None or recommended.family != family): + errors.append( + f"model_specs/{family}.json ui.recommended_package '{recommended_id}' is not a " + f"packages[].id in that spec" + ) + elif recommended is not None and not (recommended.format == "gguf" and recommended.installable): + errors.append( + f"model_specs/{family}.json ui.recommended_package '{recommended_id}' is " + f"format={recommended.format or 'unknown'} download.kind=" + f"'{recommended.download_kind or 'missing'}' and can never be offered by the UI" + ) + elif recommended_id and recommended_id not in reachable_ids: + errors.append( + f"model_specs/{family}.json ui.recommended_package '{recommended_id}' is not " + f"reachable from any {name} entry" + ) + for package in sorted(installable, key=lambda item: item.id): + if package.default and package.id not in reachable_ids: + errors.append( + f"model_specs/{family}.json default package '{package.id}' is not reachable " + f"from any {name} entry" + ) + return errors, warnings + + +def check_model_params_sync( + params: Any, + specs_by_family: dict[str, dict[str, Any]], + catalog_entries: list[CatalogEntry], + params_path: Path, +) -> tuple[list[str], list[str]]: + """Check webui/configs/model_params.json controls against the specs. + + Parameter groups are keyed by spec family or by catalog entry id. A control + whose name is absent from the owning spec's options.request is dead: strict + loaders reject the unknown key and the request fails, lenient ones drop it + silently. Only families that actually document their request options are + checked, because an empty options.request means "undocumented", not + "unsupported". + """ + errors: list[str] = [] + warnings: list[str] = [] + name = rel(params_path) + if not isinstance(params, dict): + return [f"{name}: top-level JSON must be an object"], warnings + + family_by_entry_id = {entry.id: entry.family for entry in catalog_entries} + for group, controls in params.items(): + if not isinstance(controls, list): + continue + family = group if group in specs_by_family else family_by_entry_id.get(group, "") + if not family and group in BUNDLED_LOADERS_WITHOUT_SPEC: + # Bundled families own no spec, so they are absent from + # specs_by_family, and their catalog ids are hyphenated while the + # group is keyed by the family name. Resolve them by name. + family = group + if not family: + errors.append( + f"{name}: parameter group '{group}' matches no model_specs family and no " + f"catalog entry id" + ) + continue + if family in BUNDLED_LOADERS_WITHOUT_SPEC: + # Bundled loaders have no spec by design, so there is no declared + # request surface to check a control against. They also validate + # nothing at runtime, so an unknown key is dropped rather than + # rejected: the controls are safe, just unverifiable from here. + continue + spec = specs_by_family.get(family) + if spec is None: + errors.append( + f"{name}: parameter group '{group}' resolves to family '{family}' " + f"which has no model_specs/{family}.json" + ) + continue + options = spec.get("options") + request = options.get("request") if isinstance(options, dict) else None + if not isinstance(request, list) or not request: + continue + declared = { + option.get("name") for option in request + if isinstance(option, dict) and isinstance(option.get("name"), str) + } + for control in controls: + if not isinstance(control, dict): + continue + control_name = control.get("name") + if not isinstance(control_name, str) or not control_name: + errors.append(f"{name}: parameter group '{group}' has a control without a name") + continue + if control_name not in declared: + warnings.append( + f"{name}: parameter group '{group}' control '{control_name}' is not in " + f"model_specs/{family}.json options.request" + ) + return errors, warnings + + class _SyncCheckSelfTests(unittest.TestCase): def test_parse_loader_declarations(self) -> None: text = """ @@ -225,12 +647,156 @@ def test_loader_json_family_parse(self) -> None: families = loader_families_from_json([{"family": "a"}, {"family": "b"}]) self.assertEqual(families, {"a", "b"}) + def test_parse_string_vocabulary(self) -> None: + text = ( + 'VoiceTaskKind parse_voice_task_kind(const std::string & value) {\n' + ' if (value == "vad") {\n return VoiceTaskKind::Vad;\n }\n' + ' if (value == "tts") {\n return VoiceTaskKind::Tts;\n }\n' + ' throw std::runtime_error("unsupported task: " + value);\n' + '}\n' + 'RunMode parse_run_mode(const std::string & value) {\n' + ' if (value == "offline") {\n return RunMode::Offline;\n }\n' + '}\n' + ) + self.assertEqual( + parse_string_vocabulary(text, "parse_voice_task_kind(const std::string & value)"), + {"vad", "tts"}, + ) + self.assertEqual( + parse_string_vocabulary(text, "parse_run_mode(const std::string & value)"), + {"offline"}, + ) + with self.assertRaises(ValueError): + parse_string_vocabulary(text, "parse_missing(const std::string & value)") + + def test_package_install_paths(self) -> None: + package = _self_test_package( + "demo_q8_0", + files=("Demo-GGUF/demo-q8_0.gguf", "Demo-GGUF/config.json"), + strip_prefix="Demo-GGUF", + ) + self.assertEqual(package.install_paths, { + "models/Demo-GGUF", + "models/Demo-GGUF/demo-q8_0.gguf", + "models/Demo-GGUF/config.json", + }) + + def test_catalog_download_id_must_be_a_package_id(self) -> None: + errors, _ = _self_test_catalog_check( + [_self_test_entry(download_id="demo")], + {"demo_q8_0": _self_test_package("demo_q8_0")}, + ) + self.assertTrue(any("download_id 'demo' is not a packages[].id" in error for error in errors)) + + def test_catalog_path_must_match_the_package(self) -> None: + packages = {"demo_q8_0": _self_test_package("demo_q8_0", files=("demo-q8_0.gguf",))} + errors, _ = _self_test_catalog_check( + [_self_test_entry(path="models/Demo")], packages) + self.assertTrue(any("path 'models/Demo' is neither" in error for error in errors)) + errors, _ = _self_test_catalog_check( + [_self_test_entry(path="models/Demo-GGUF/demo-q8_0.gguf")], packages) + self.assertEqual(errors, []) + + def test_catalog_rejects_unknown_task_and_family(self) -> None: + errors, _ = _self_test_catalog_check( + [_self_test_entry(task="nope"), _self_test_entry(entry_id="x", family="ghost")], + {"demo_q8_0": _self_test_package("demo_q8_0")}, + ) + self.assertTrue(any("task 'nope' is not accepted" in error for error in errors)) + self.assertTrue(any("no model_specs/ghost.json" in error for error in errors)) + + def test_catalog_reports_unreachable_directory_and_default(self) -> None: + packages = { + "demo_q8_0": _self_test_package("demo_q8_0", default=True), + "demo_extra_q8_0": _self_test_package( + "demo_extra_q8_0", target_directory="Demo-GGUF/extra"), + } + _, warnings = _self_test_catalog_check([_self_test_entry()], packages) + self.assertTrue(any("which no" in warning and "extra" in warning for warning in warnings)) + errors, _ = _self_test_catalog_check( + [_self_test_entry(download_id="demo_extra_q8_0", path="models/Demo-GGUF/extra")], + packages, + ) + self.assertTrue(any("default package 'demo_q8_0' is not reachable" in e for e in errors)) + + def test_catalog_reports_unsupported_downloads_as_not_installable(self) -> None: + packages = {"demo_q8_0": _self_test_package("demo_q8_0", download_kind="unsupported")} + errors, warnings = _self_test_catalog_check([], packages) + self.assertEqual(errors, []) + self.assertEqual([w for w in warnings if "no model_specs" in w], []) + self.assertEqual(check_native_manager_sync(packages) != [], True) + + def test_model_params_group_must_resolve(self) -> None: + specs = {"demo": {"family": "demo", "options": {"request": [{"name": "speed"}]}}} + errors, warnings = check_model_params_sync( + {"demo": [{"name": "speed"}, {"name": "gone"}], "ghost": [{"name": "speed"}]}, + specs, + [], + MODEL_PARAMS_PATH, + ) + self.assertTrue(any("parameter group 'ghost' matches no" in error for error in errors)) + self.assertTrue(any("control 'gone' is not in" in warning for warning in warnings)) + + +def _self_test_package( + package_id: str, + *, + family: str = "demo", + target_directory: str = "Demo-GGUF", + files: tuple[str, ...] = (), + strip_prefix: str = "", + default: bool = False, + download_kind: str = INSTALLABLE_DOWNLOAD_KIND, +) -> SpecPackage: + return SpecPackage( + family=family, + id=package_id, + format="gguf", + target_directory=target_directory, + default=default, + download_kind=download_kind, + files=files, + strip_prefix=strip_prefix, + ) + + +def _self_test_entry( + *, + entry_id: str = "demo", + family: str = "demo", + path: str = "models/Demo-GGUF", + task: str = "tts", + mode: str = "offline", + download_id: str = "demo_q8_0", +) -> CatalogEntry: + return CatalogEntry( + index=0, + id=entry_id, + family=family, + path=path, + task=task, + mode=mode, + download_id=download_id, + ) + + +def _self_test_catalog_check( + entries: list[CatalogEntry], + packages: dict[str, SpecPackage], +) -> tuple[list[str], list[str]]: + specs = {"demo": {"family": "demo", "ui": {"recommended_package": "demo_q8_0"}}} + return check_catalog_sync( + entries, specs, packages, {"tts", "asr"}, {"offline", "streaming"}, CATALOG_PATH) + def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--cmake", type=Path, default=CMAKE_PATH, help="Path to top-level CMakeLists.txt") parser.add_argument("--registry", type=Path, default=REGISTRY_PATH, help="Path to registry.cpp") parser.add_argument("--specs-dir", type=Path, default=SPECS_DIR, help="Directory containing model spec JSON files") + parser.add_argument("--catalog", type=Path, default=CATALOG_PATH, help="Path to models_catalog.json") + parser.add_argument("--model-params", type=Path, default=MODEL_PARAMS_PATH, help="Path to model_params.json") + parser.add_argument("--session", type=Path, default=SESSION_PATH, help="Path to session.cpp") parser.add_argument( "--loader-json", type=Path, @@ -270,35 +836,66 @@ def main() -> int: print("error: no active loaders found", file=sys.stderr) return 2 + for path, label in ((args.catalog, "catalog"), (args.model_params, "model params"), (args.session, "session")): + if not path.is_file(): + print(f"error: {label} not found: {path}", file=sys.stderr) + return 2 + try: + session_text = args.session.read_text(encoding="utf-8") + task_kinds = parse_string_vocabulary(session_text, "parse_voice_task_kind(const std::string & value)") + run_modes = parse_string_vocabulary(session_text, "parse_run_mode(const std::string & value)") + except Exception as exc: + print(f"error: failed to read task kinds from {rel(args.session)}: {exc}", file=sys.stderr) + return 2 + specs_by_family, spec_packages, spec_errors = load_spec_packages(args.specs_dir) manager_packages, manager_errors = load_manager_packages(args.specs_dir) + catalog_entries, catalog_errors = load_catalog(args.catalog) errors.extend(spec_errors) errors.extend(manager_errors) + errors.extend(catalog_errors) errors.extend(check_loader_spec_sync(active_loaders, specs_by_family)) manager_sync_errors, manager_sync_warnings = check_manager_sync(spec_packages, manager_packages) errors.extend(manager_sync_errors) warnings.extend(manager_sync_warnings) - + warnings.extend(check_native_manager_sync(spec_packages)) + catalog_sync_errors, catalog_sync_warnings = check_catalog_sync( + catalog_entries, specs_by_family, spec_packages, task_kinds, run_modes, args.catalog) + errors.extend(catalog_sync_errors) + warnings.extend(catalog_sync_warnings) + try: + params_payload = load_json(args.model_params) + except json.JSONDecodeError as exc: + errors.append(f"{rel(args.model_params)}: invalid JSON: {exc}") + params_payload = {} + params_errors, params_warnings = check_model_params_sync( + params_payload, specs_by_family, catalog_entries, args.model_params) + errors.extend(params_errors) + warnings.extend(params_warnings) + + server_packages = sum(1 for package in spec_packages.values() if package.installable) print( f"active_loaders={len(active_loaders)} commented_loaders={len(commented_loaders)} " f"specs={len(specs_by_family)} packages={len(spec_packages)} " - f"manager_packages={len(manager_packages)}" + f"manager_packages={len(manager_packages)} server_packages={server_packages} " + f"catalog_entries={len(catalog_entries)} task_kinds={len(task_kinds)}" ) for warning in warnings: print(f"warning: {warning}") if errors: - print("loader/spec sync failed:", file=sys.stderr) + print("loader/spec/catalog sync failed:", file=sys.stderr) for error in errors: print(f" - {error}", file=sys.stderr) print( "\nFix: keep model_specs/*.json, model_manager_v2.py, registered loaders, " - "and published default GGUF packages aligned. Schema-level validation " - "belongs to the typed model-spec validator, and WebUI placement is checked separately.", + "published default GGUF packages, and webui/configs/models_catalog.json aligned. " + "A catalog entry must name a real packages[].id and install into that package's " + "location. Schema-level validation belongs to the typed model-spec validator.", file=sys.stderr, ) return 1 - print("ok: runtime loaders, model_specs, and model_manager_v2 are in sync") + print("ok: runtime loaders, model_specs, model_manager_v2, and the WebUI catalog are in sync") return 0