Skip to content

feat(webui): add preset save & apply for reusable configs#346

Open
redXpanda wants to merge 2 commits into
OpenBMB:mainfrom
redXpanda:feature/preset-save-apply
Open

feat(webui): add preset save & apply for reusable configs#346
redXpanda wants to merge 2 commits into
OpenBMB:mainfrom
redXpanda:feature/preset-save-apply

Conversation

@redXpanda

Copy link
Copy Markdown

Summary

Add a lightweight preset system to the VoxCPM WebUI, mirroring the IndexTTS workflow: save the current reference audio + all generation parameters as a named preset and reload it later. Also localizes preset toast messages so they match the gr.I18n-driven UI.

What's included

  • presets.py — file-based preset store (presets/<name>/preset.json + copied reference audio), with name sanitization and path resolution.
  • app.py — inline preset UI (name + save / dropdown + apply / delete / refresh) under the reference audio, plus i18n keys (en + zh-CN).
  • .gitignore — ignore presets/ (user data).

Notes

  • A preset stores the reference audio, cloning-mode toggle and transcript, the control instruction, the target text, and the advanced generation parameters.
  • Toast messages (save / apply / delete) were previously hardcoded in Chinese. Since gr.Info/gr.Warning reject I18nData (they require a plain str), the language is resolved from the request's Accept-Language header and the message is looked up in a runtime dict.

Test plan

  • Save a preset from the WebUI and confirm presets/<name>/preset.json + the copied reference audio are created.
  • Apply the saved preset and verify all fields (reference audio, transcript, instruction, target text, advanced params) are restored.
  • Delete / refresh the preset dropdown.
  • Switch browser language (en / zh-CN) and confirm toast messages are localized correctly.

Add a lightweight preset system to the VoxCPM WebUI, mirroring the
IndexTTS workflow: save the current reference audio + all generation
parameters as a named preset and reload it later.

- presets.py: file-based preset store (presets/<name>/preset.json +
  copied reference audio), with name sanitization and path resolution
- app.py: inline preset UI (name + save / dropdown + apply/delete/refresh)
  under the reference audio, plus i18n keys (en + zh-CN)
- .gitignore: ignore presets/ (user data)
Preset save/apply/delete toasts were hardcoded in Chinese, mismatching
the gr.I18n-driven UI. gr.Info/gr.Warning reject I18nData (LogMessage
requires a plain str), so resolve the language from the request's
Accept-Language header and look the message up in a runtime dict instead.
@a710128

a710128 commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Nice work on this — the preset system is clean and well-documented, and I really like that the save inputs / apply outputs share a single source-of-truth ordering so they can't drift apart. I pulled it down and gave it a read + a quick functional roundtrip; everything lines up nicely. 🙌

One small suggestion: would you consider adding a unit test for presets.py?

The repo already has a tests/ directory with a pytest setup (pytest + pytest-cov in pyproject.toml), and presets.py is a great fit for testing since it's pure logic with no heavy deps — no torch, no gradio, no model loading — so the tests would run fast and GPU-free in any environment.

A few high-value cases worth locking down:

  • safe_preset_name edge cases (empty / whitespace-only names, purely-special-char names, / \ : .., and CJK input) — this is effectively the path-traversal guard, so it's the most valuable thing to pin down.
  • save → load roundtrip: confirm all params are preserved and the audio file suffix is kept (e.g. a .flac reference stays .flac).
  • reference_audio=None → stored as "".
  • delete_preset returns the correct bool for existing vs. missing presets.
  • load_preset returns None for a non-existent preset.

Using tmp_path (or monkeypatching get_presets_dir) keeps the tests isolated from the real presets/ directory.

I'd skip testing the app.py handlers themselves — they're thin gradio glue wrapping gr.update() / gr.Warning / gr.Request, so mocking the runtime there isn't worth the cost.

Totally non-blocking from my side — just thought it'd be a nice fit given the existing test conventions. Thanks for the contribution!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants