Skip to content

fix(voice-design): validator-safe instruct builder (plan-05, closes #114 #115) - #141

Merged
debpalash merged 2 commits into
mainfrom
005-voice-design-validator
May 29, 2026
Merged

fix(voice-design): validator-safe instruct builder (plan-05, closes #114 #115)#141
debpalash merged 2 commits into
mainfrom
005-voice-design-validator

Conversation

@debpalash

@debpalash debpalash commented May 29, 2026

Copy link
Copy Markdown
Owner

plan-05Closes #114, #115. Addresses #132.

Voice Design Synthesize failed with "Unsupported instruct items" (#115) or "conflicting instruct items within the same category" (#114).

Root cause (investigated, not guessed)

The engine validator (omnivoice/models/omnivoice.py::_resolve_instruct) is whitelist-strict by design — it maps tags to canonical form the engine consumes, so free-text can't be passed through. The presets already emit valid tags. The failures came from the frontend assembly in useTTS.js: it merged the free-text instruct field with the category dropdowns —

Fix — option A (frontend guard, your call)

  • frontend/src/utils/voiceInstruct.js buildDesignInstruct(vdStates, freeText): dropdowns win their category; free-text accepted only as a known tag in an open category; unknown/duplicate items are dropped and returned so the UI can warn. TAG_TO_CATEGORY is derived from CATEGORIES (single source of truth).
  • useTTS.js design mode uses it instead of the raw merge; toasts the dropped items so the user sees what was ignored.

Engine validator untouched — the whitelist contract is preserved, no vendored-engine change, no audio-quality risk.

Tests (TDD, vitest — Constitution V)

voiceInstruct.test.js (6): one-per-category, prose dropped (#115), category-duplicate dropped (#114), valid free-text accepted, casing/full-width-comma, Auto/empty. Full frontend suite: 72 passed; typecheck + build green.

Notes

  • Declined option B (relax the engine validator) / C (per-engine free-text) to preserve the whitelist contract and avoid silently altering user input.
  • Could not reproduce Voice Design generates unsupported instructions on macOS Apple Silicon #115's "non-English language injects free-text" claim — language is sent as a separate param, not into instruct. The reproducible path (free-text + dropdowns) is fixed; if a non-English repro surfaces, we'll revisit.

Spec/plan/tasks in specs/005-voice-design-validator/.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Validator-safe handling of voice-design inputs: dropdown choices are preferred, at most one tag per category is accepted, and unsupported or duplicate free-text items are dropped and surfaced as warnings. Finalized instruct is now used for speech requests.
  • Tests

    • Added comprehensive tests covering dropdown vs free-text merging, normalization, unsupported items, and duplicates.
  • Documentation

    • Added spec and plan documents describing the validator approach and rollout steps.

Review Change Stack

plan-05 (option A — frontend guard). The engine validator is whitelist-strict
by design; the #114/#115 failures came from useTTS.js merging the free-text
instruct field with the category dropdowns, producing unsupported items (#115)
or two items in one category (#114).

- voiceInstruct.js buildDesignInstruct(vdStates, freeText): dropdowns win their
  category; free-text accepted only as a known tag in an open category;
  unknown/duplicate items are dropped and returned so the UI can warn. Derives
  TAG_TO_CATEGORY from CATEGORIES (single source of truth).
- useTTS.js design mode uses it instead of the raw merge; toasts dropped items.

Engine validator (_resolve_instruct) untouched — whitelist contract preserved,
no vendored-engine change.

Tests (TDD, vitest): voiceInstruct.test.js (6). Full frontend suite 72 passed;
typecheck + build green.

Closes #114, #115. Addresses #132.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1859ad37-e537-415f-a5e9-d6dc18912ca1

📥 Commits

Reviewing files that changed from the base of the PR and between d8dc60f and 13a5f9c.

📒 Files selected for processing (3)
  • frontend/src/hooks/useTTS.js
  • frontend/src/utils/voiceInstruct.js
  • frontend/src/utils/voiceInstruct.test.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/src/hooks/useTTS.js
  • frontend/src/utils/voiceInstruct.test.js

📝 Walkthrough

Walkthrough

Adds a frontend whitelist-safe builder buildDesignInstruct(vdStates, freeText), test coverage, spec/docs, and integrates the builder into the useTTS hook to validate instruct parameters and show warnings for unsupported or duplicate items.

Changes

Voice Design Validator Implementation

Layer / File(s) Summary
Investigation, findings, and implementation plan
specs/005-voice-design-validator/spec.md, specs/005-voice-design-validator/plan.md, specs/005-voice-design-validator/tasks.md
Documents engine whitelist behavior, root-cause analysis for mixed free-text + dropdown instruct assembly, the decision to add a frontend guard, and a TDD plan for buildDesignInstruct.
Validator implementation and tests
frontend/src/utils/voiceInstruct.js, frontend/src/utils/voiceInstruct.test.js
Adds buildDesignInstruct(vdStates, freeText) that builds a whitelist-only, comma-joined instruct by mapping dropdown selections first and then accepting only whitelisted free-text tags whose categories are still open. Returns { instruct, unsupported: string[], duplicates: string[] }. Vitest suite covers dropdown precedence, unsupported/free-text rejection, duplicates handling, normalization (casing/full-width comma), and Auto/empty handling.
TTS hook integration
frontend/src/hooks/useTTS.js
Imports and calls buildDesignInstruct(vdStates, instruct) when generating TTS payloads (non-clone mode). Shows warning toasts when unsupported or duplicates arrays are non-empty and appends the validated finalInstruct to the request FormData.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: implementing a validator-safe instruct builder to fix voice design issues (#114, #115).
Description check ✅ Passed The PR description is comprehensive and follows the template structure, covering root cause analysis, the fix approach, testing, and relevant notes.
Linked Issues check ✅ Passed The changes directly address issue #114 by preventing conflicting instruct items within the same category through the buildDesignInstruct function that enforces one tag per category.
Out of Scope Changes check ✅ Passed All changes are scoped to the frontend instruct building logic, tests, and documentation. The engine validator remains unchanged as intended.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 005-voice-design-validator

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes Voice Design synthesis failures (#114, #115) by replacing the raw Object.values(vdStates) + instruct.trim() merge in useTTS.js with a new buildDesignInstruct() utility that enforces exactly one whitelisted tag per category before the payload reaches the engine validator.

  • voiceInstruct.js derives TAG_TO_CATEGORY from CATEGORIES (single source of truth), processes dropdowns first (they win their category), and buckets free-text items into unsupported (unknown prose — Voice Design generates unsupported instructions on macOS Apple Silicon #115 root cause) or duplicates (valid tag outranked by a dropdown — [Bug] Bad request - conflicting instruct items within the same category #114 root cause), returning all three for the caller to act on.
  • useTTS.js wires in buildDesignInstruct in design mode and shows a separate, accurately-worded toast per bucket — resolving the misleading-message concern from the prior review cycle.
  • voiceInstruct.test.js adds 7 vitest tests covering the full contract: per-category enforcement, prose rejection, dropdown-outranks-typed-tag, valid free-text acceptance, casing/full-width-comma normalization, Auto/empty input, and CATEGORIES↔dropdown drift detection.

Confidence Score: 5/5

Safe to merge — the change is self-contained to the frontend instruct assembly path, the engine validator and all audio-generation parameters are untouched, and the fix is backed by 7 targeted unit tests.

The root causes of both bugs (#114 conflicting category, #115 unsupported free-text) are directly addressed by the new builder. The prior review threads (misleading toast wording, silent dropdown drift) are fully resolved. The implementation is defensive (null/empty guards, casing normalization, drift warning), the tests are representative, and no existing behavior outside design-mode instruct assembly is affected.

No files require special attention.

Important Files Changed

Filename Overview
frontend/src/utils/voiceInstruct.js New validator-safe instruct builder: derives TAG_TO_CATEGORY from CATEGORIES at module load, enforces one tag per category with dropdowns winning, and cleanly buckets free-text into unsupported vs duplicates. Logic is correct and edge cases (null/empty, casing, full-width comma, unknown dropdown drift) are all handled.
frontend/src/hooks/useTTS.js Design-mode instruct assembly replaced with buildDesignInstruct; separate toasts for unsupported vs duplicate items address the misleading-message concern from the prior review. vdStates correctly included in useCallback dependency array.
frontend/src/utils/voiceInstruct.test.js 7 vitest tests covering the full contract: one-per-category from dropdowns, free-text prose → unsupported, dropdown outranks typed tag → duplicates, valid free-text in open category, casing/full-width-comma normalization, Auto/empty, and CATEGORIES↔dropdown drift. Good coverage of all the root-cause scenarios.

Sequence Diagram

sequenceDiagram
    participant UI as User (Design Mode)
    participant Hook as useTTS.js
    participant Builder as buildDesignInstruct()
    participant Toast as react-hot-toast
    participant API as generateSpeech()
    participant Engine as Engine Validator<br/>(_resolve_instruct)

    UI->>Hook: handleGenerate()
    Hook->>Builder: buildDesignInstruct(vdStates, instruct)
    Note over Builder: 1. Dropdowns win their category<br/>2. Free-text: valid+open → accept<br/>   unknown → unsupported[]<br/>   valid+taken → duplicates[]
    Builder-->>Hook: "{ instruct, unsupported[], duplicates[] }"
    alt unsupported not empty
        Hook->>Toast: Ignored unsupported instruct: ...
    end
    alt duplicates not empty
        Hook->>Toast: Ignored (category already set): ...
    end
    Hook->>API: "POST formData (instruct = validated string)"
    API->>Engine: "instruct = one valid tag per category"
    Engine-->>API: valid
    API-->>Hook: audio response
    Hook-->>UI: plays audio + loads history
Loading

Reviews (2): Last reviewed commit: "fix(voice-design): split unsupported vs ..." | Re-trigger Greptile

Comment thread frontend/src/hooks/useTTS.js Outdated
Comment on lines +113 to +115
if (dropped.length) {
toast(`Ignored instruct not in the supported set: ${dropped.join(', ')}`, { icon: '⚠️' });
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Misleading toast message for valid-but-conflicting free-text tags

dropped conflates two distinct cases: (1) genuinely unsupported prose, and (2) a recognized tag whose category was already claimed by a dropdown. The shared toast message "not in the supported set" is factually wrong for case 2 — high pitch is in the supported set; it was simply outranked by the dropdown's low pitch. A user seeing this toast may incorrectly conclude the tag itself is invalid and start troubleshooting the wrong thing.

Consider splitting the return value of buildDesignInstruct (or the toast call) into two arrays — one for truly unknown items and one for category-duplicate items — so each gets an accurate message.

Fix in Claude Code

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — split the return into unsupported (free-text prose) and duplicates (valid tag outranked by a dropdown), and the UI now shows an accurate toast per bucket. A valid-but-outranked tag like high pitch no longer reads as "unsupported."

Comment thread frontend/src/utils/voiceInstruct.js Outdated
Comment on lines +50 to +51
// Dropdowns first — they win their category (not tracked as "dropped").
for (const v of Object.values(vdStates || {})) add(v, null);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Invalid dropdown values are silently discarded

Dropdown values are passed as add(v, null), so if any dropdown option is not found in TAG_TO_CATEGORY (e.g., if CATEGORIES and the actual dropdown options drift apart), the value is quietly dropped — no toast, no console warning, no entry in dropped. The resulting finalInstruct will just be missing that category with no indication to the user or developer. Adding at least a console.warn for the null-dropped case would make silent desync easier to catch in development.

Fix in Claude Code

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — an unknown dropdown value (CATEGORIES↔option-list drift) now triggers a console.warn instead of being silently dropped, so the desync is visible in dev. Added a test asserting it's excluded from the instruct and not mislabeled as unsupported free-text.

…ropdown drift (Greptile #141)

- buildDesignInstruct now returns { instruct, unsupported, duplicates }:
  `unsupported` = free-text prose (not a known tag, #115); `duplicates` = a
  valid tag whose category was already set (e.g. dropdown low pitch outranks a
  typed high pitch, #114). useTTS shows an accurate toast per bucket instead of
  calling a valid-but-outranked tag "unsupported".
- console.warn when a *dropdown* value isn't in CATEGORIES (option-list ↔
  whitelist drift) instead of silently dropping it.

Tests updated + 1 added (7/7); typecheck + build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@debpalash
debpalash merged commit c37a932 into main May 29, 2026
18 of 19 checks passed
@debpalash
debpalash deleted the 005-voice-design-validator branch May 29, 2026 05:42
debpalash added a commit that referenced this pull request May 29, 2026
Bring the branch up to date with main and resolve 5 conflicts as
feature-unions so nothing shipped since #133 was opened regresses:

- useTTS.js: take main's #141 validator-safe instruct (buildDesignInstruct);
  #133 held only the stale pre-#141 dedup logic.
- dub_pipeline.py: UNION — keep #133's download-task cancel cleanup AND
  main's plan-04 logging + structured failure event (build_failure).
- dubSlice.ts / useDubWorkflow.js: UNION — keep both #133's dub
  download-progress state (setDubPrepProgress / setDubCurrentSegId) and
  main's pipeline-error-transparency state (setDubFailure).
- bootstrap.rs: take main's shipped plan-03 network-resilience cascade
  (#140/#142); #133's region-based mirror approach was the superseded
  alternative for the same concern. get_effective_region stays live
  (shared via config.rs, used by tools.rs).

Verified: frontend typecheck + build clean; 90 backend tests pass
(dub / failure / timing / onboarding / personalities), 0 failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

[Bug] Bad request - conflicting instruct items within the same category

1 participant