Skip to content

feat(design): seed pin / re-roll for designed voices (#526) - #577

Merged
debpalash merged 1 commit into
mainfrom
feat/design-seed-pin-reroll
Jun 20, 2026
Merged

feat(design): seed pin / re-roll for designed voices (#526)#577
debpalash merged 1 commit into
mainfrom
feat/design-seed-pin-reroll

Conversation

@debpalash

@debpalash debpalash commented Jun 20, 2026

Copy link
Copy Markdown
Owner

What & why

#526: Voice design rolled a brand-new random seed on every synth, so tweaking one attribute also re-rolled the whole base timbre — you could never iterate on "the same voice, slightly different." The ask: show the seed + a "keep this seed" control.

The fix

Backend (/generate): it already accepted seed and echoed X-Seed, but left used_seed=None when nothing supplied one — non-deterministic, unreproducible, empty X-Seed. Now it materializes a concrete random seed when none resolves, so every take is reproducible and the real seed is always returned + stored in history. (Bonus: this also makes the clone/profile paths reproducible, not just design.)

Frontend:

  • New store slice fields designSeed + keepSeed.
  • The design synth reuses the pinned seed when "keep this seed" is on (pickDesignSeed), else rolls a fresh one, and reads the authoritative seed back from the X-Seed response header.
  • Design tab gains a Seed field + "keep this seed" checkbox + "New seed" (re-roll 🎲) button.

Tests

  • seed.test.js (pickDesignSeed): pin when kept+valid, re-roll when off / nothing valid pinned, 31-bit range guard.
  • Local: full vitest (557) + typecheck:ci green.

Notes

  • i18n keys added to en.json; other locales fall back to English (the i18n parity probe gates only on valid JSON — coverage is advisory, matching the repo's existing approach).

Post-v0.3.7 sweep. (#572/#573/#574 merged; #576 = #486.)

🤖 Generated with Claude Code

Seed Pinning & Re-roll for Voice Design (#526)

This PR enables iterative voice refinement by introducing seed pinning and re-roll functionality. Previously, every synthesis operation would generate a new random voice timbre, making it impossible to iterate on "the same voice with slight variations."

UI Changes

CloneDesignTab Component Layout:

BEFORE:
┌──────────────────────────────────┐
│ Voice Design Controls            │
│ - Identity (categories)          │
│ - Language, steps, speed, etc.   │
│ - Synthesis controls             │
└──────────────────────────────────┘

AFTER:
┌──────────────────────────────────┐
│ Voice Design Controls            │
│ - Identity (categories)          │
│ - Language, steps, speed, etc.   │
│ - Synthesis controls             │
├──────────────────────────────────┤
│ ▪ Design Seed (NEW)              │
│  ┌────────────────────────────┐  │
│  │ [123456789   ] [⚄ Reroll] │  │
│  │ ☐ Keep this seed           │  │
│  └────────────────────────────┘  │
└──────────────────────────────────┘

New CSS classes:

  • .design-seed: Container with top margin
  • .design-seed__row: Flex row with gap and center alignment for seed input + reroll button
  • .design-seed__input: Fixed-width (9rem) seed number field
  • .design-seed__keep: Inline-flex checkbox with muted styling

Seed Pinning Mechanism

graph TD
    A["User clicks Generate"] --> B["useTTS calls pickDesignSeed"]
    B --> C{"keepSeed enabled?"}
    C -->|Yes & designSeed is valid integer| D["Reuse pinned seed"]
    C -->|No or invalid seed| E["Generate fresh seed"]
    D --> F["Send seed to /generate endpoint"]
    E --> F
    F --> G["Backend returns X-Seed header"]
    G --> H["UI parses X-Seed header"]
    H --> I["setDesignSeed updates store"]
    I --> J["Displayed seed now authoritative"]
Loading

Key Changes

Backend (backend/api/routers/generation.py):

  • Now materializes a concrete used_seed when no seed is supplied
  • Ensures every synthesis is reproducible and the actual seed is returned via X-Seed header

Frontend State (generateSlice.ts):

  • Added designSeed: number | null — stores the current pinned seed
  • Added keepSeed: boolean — toggles whether to reuse the pinned seed
  • Corresponding setter functions for store updates

Seed Logic (seed.js):

  • Exported pickDesignSeed(keepSeed, designSeed, rng) utility
  • Returns pinned seed when "keep" is enabled and seed is a valid integer; otherwise generates fresh seed via RNG
  • Uses 31-bit positive range (MAX_SEED = 2147483647) for display and storage

Hook Integration (useTTS.js):

  • Imports and uses pickDesignSeed for design-mode synthesis
  • Reads X-Seed response header after generation to update UI with authoritative seed value
  • Passes keepSeed, designSeed, and setDesignSeed to dependency list

UI Controls (CloneDesignTab.jsx):

  • New seed input field displaying current pinned seed (clears to null when emptied)
  • Reroll button (dice icon) to generate a fresh seed
  • "Keep this seed" checkbox to enable/disable pinning

Localization (en.json):

  • Added i18n keys for seed field labels and helper text under the clone locale

Testing

seed.test.js validates pickDesignSeed behavior:

  • Returns pinned seed when "keep" is enabled and seed is a valid integer
  • Generates fresh seed when "keep" is disabled
  • Falls back to fresh seed generation when pinned seed is missing or invalid
  • Ensures generated seeds stay within 31-bit range

Full vitest suite (557 tests) and typecheck:ci pass locally.

Voice design rolled a brand-new random seed on every synth, so tweaking an
attribute also re-rolled the whole base timbre — you could never iterate on the
"same voice, slightly different". #526 asks for the seed to be shown with a
"keep this seed" control.

- Backend: `/generate` already accepted `seed` and echoed `X-Seed`, but left
  `used_seed=None` when nothing supplied one (non-deterministic, unreproducible,
  empty X-Seed). Now it materializes a concrete random seed when none resolves,
  so every take is reproducible and the real seed is always returned and stored
  — this also helps the clone/profile paths, not just design.
- Frontend: new store slice (`designSeed`, `keepSeed`); the design synth reuses
  the pinned seed when "keep this seed" is on (via `pickDesignSeed`) and reads
  the authoritative seed back from `X-Seed`. Design tab gains a Seed field +
  "keep this seed" checkbox + "New seed" (re-roll) button.

Test: `pickDesignSeed` (pin when kept+valid, re-roll otherwise, range guard).
i18n keys added to en.json (other locales fall back; parity probe is advisory).

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

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds reproducible voice-design seed support across the full stack. A new pickDesignSeed utility and MAX_SEED constant handle seed selection logic. The backend materializes a concrete seed when none is provided and echoes it via X-Seed. useTTS submits the chosen seed and round-trips the echoed value into Zustand state. CloneDesignTab exposes reroll/keep UI controls, CSS layout, and five i18n strings.

Changes

Voice-Design Seed Pinning

Layer / File(s) Summary
Seed utility and Zustand state contracts
frontend/src/utils/seed.js, frontend/src/store/generateSlice.ts, frontend/src/test/seed.test.js
MAX_SEED (2^31−1) and pickDesignSeed implement pinned-or-rolled selection. GenerateSlice gains designSeed: number | null, keepSeed: boolean, and their Zustand setters. Tests cover pinned reuse, RNG rolling, and edge cases including 0 as a valid seed.
Backend seed materialization
backend/api/routers/generation.py
generate_speech fills used_seed via random.randint(0, 2**31−1) when it remains None, guaranteeing the X-Seed response header always carries a concrete value.
useTTS seed round-trip
frontend/src/hooks/useTTS.js
Selects keepSeed, designSeed, and setDesignSeed from the store; appends pickDesignSeed output as the seed form field; parses the X-Seed response header and writes it back via setDesignSeed; updates the useCallback dependency array.
CloneDesignTab seed UI
frontend/src/pages/CloneDesignTab.jsx, frontend/src/pages/CloneDesignTab.css, frontend/src/i18n/locales/en.json
Adds Dice5 icon, store selectors, a numeric seed input (clears to null), a reroll button toggling keepSeed on, and a keep-seed checkbox. CSS defines .design-seed flex layout; five clone.seed_* i18n keys provide English labels.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CloneDesignTab
  participant useTTS
  participant pickDesignSeed
  participant Backend

  User->>CloneDesignTab: click Dice5 reroll
  CloneDesignTab->>CloneDesignTab: setDesignSeed(random int), setKeepSeed(true)
  User->>CloneDesignTab: click Generate
  CloneDesignTab->>useTTS: handleGenerate()
  useTTS->>pickDesignSeed: pickDesignSeed(keepSeed=true, designSeed)
  pickDesignSeed-->>useTTS: pinned seed
  useTTS->>Backend: POST /generate {seed: pinned_seed}
  Backend->>Backend: used_seed = pinned_seed (or randint if None)
  Backend-->>useTTS: stream + X-Seed: used_seed
  useTTS->>CloneDesignTab: setDesignSeed(parseInt(X-Seed))
  CloneDesignTab-->>User: seed input reflects echoed value
Loading

Panel Review

ML inference / backend — generation.py:468-475

random.randint is seeded from the process PRNG, which is not reset per request, so two concurrent calls racing through the None branch can produce collisions in logs but not in audio. The real risk: if upstream code ever sets used_seed to 0 legitimately, the if used_seed is None guard passes and the branch is skipped correctly — that's fine. What is not fine is that the branch fires after the generation call has already executed (based on surrounding context). Verify the seed is materialized before the synth call so the header reflects the seed that actually governed the generation, not a post-hoc random number that has no relationship to the audio produced.

Audio DSP — seed.js:1 and generation.py:468

MAX_SEED = 2147483647 (2³¹ − 1) on the frontend, 2**31 - 1 on the backend — both are inclusive upper bounds. pickDesignSeed computes Math.floor(rng() * MAX_SEED), which produces [0, 2147483646], excluding 2147483647. The backend uses random.randint(0, 2**31 - 1), which is inclusive on both ends. The ranges are almost identical but not identical. Decide on a single canonical range and share it; or at minimum document the asymmetry deliberately.

Desktop systems / hook — useTTS.js:160-165

const seed = parseInt(response.headers.get("X-Seed"));
if (!isNaN(seed)) setDesignSeed(seed);

parseInt("3.7")3, which is a silent truncation. If the backend ever sends a float (unlikely but defensive), the stored seed silently mismatches what governed the audio. Use Number(...) and add an Number.isInteger guard, or assert the header is always integral in the backend before relying on parseInt here.

Product polish — CloneDesignTab.jsx:436-470

When a user clears the seed input, designSeed is set to null but keepSeed remains true (nothing resets it). The next generate call hits pickDesignSeed(true, null) → falls through to RNG (correct by the utility's contract), but the "Keep" checkbox stays checked, which is visually contradictory. Either uncheck keepSeed when the input is cleared, or disable the keep checkbox when designSeed is null.

Nit — en.json:200-204

seed_placeholder is present in i18n but it is not obvious from the diff that the input renders it via placeholder={t("clone.seed_placeholder")}. Confirm the JSX passes it; if it doesn't, the key is dead.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 7 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
I18n Completeness (21 Locales) ⚠️ Warning 5 new i18n keys (seed_label, seed_placeholder, seed_keep, seed_reroll, seed_reroll_hint) added only to en.json, missing from all 20 non-English locale files. Repository guideline requires all keys... Add the 5 seed keys to ar.json, de.json, es.json, fr.json, hi.json, id.json, it.json, ja.json, ko.json, nl.json, pl.json, pt.json, ru.json, sv.json, th.json, tr.json, uk.json, vi.json, zh-CN.json, and zh-TW.json.
✅ Passed checks (7 passed)
Check name Status Explanation
Title check ✅ Passed Title follows conventional-commit style with scope (design) and issue reference (#526); accurately summarizes the seed pinning and re-roll feature.
Description check ✅ Passed Description comprehensively covers what, why, the fix, tests, and notes; maps to required template sections with substance (Summary, Changes implicit, Type context, Testing detail provided).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cross-Platform Default Parity ✅ Passed PR materializes seeds using Python's random.randint() and JS Math.random() — both platform-invariant standard library functions with identical behavior on macOS, Windows, Linux. No OS detection...
Local-First Guarantee ✅ Passed PR adds only local seed generation/selection logic (Python random.randint, JavaScript Math.random, local store state, UI). No cloud calls, external APIs, accounts, API keys, or telemetry introduced...
Backward Compatibility ✅ Passed PR uses pre-existing seed columns in both generation_history and voice_profiles tables; no DB migrations required. Backend materializes random seeds only when needed. Frontend adds transient-only s...

✏️ 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 feat/design-seed-pin-reroll

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 Jun 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a reproducible seed control to the voice-design flow: the backend now materialises a concrete seed for every synthesis (not just design), echoes it via X-Seed, and the frontend stores + optionally pins it so iterative voice tweaks stay on the same base timbre.

  • Backend (generation.py): one random.randint call fills used_seed when it is still None after all profile/request resolution, making every path reproducible and ensuring X-Seed is never empty.
  • Frontend (generateSlice.ts, useTTS.js, seed.js): new designSeed/keepSeed store fields, a pickDesignSeed utility (with injected-RNG test coverage), and X-Seed header parsing to update designSeed from the authoritative backend value after each synth.
  • UI (CloneDesignTab.jsx/.css): a Seed row (number input + 🎲 re-roll button + "Keep this seed" checkbox) rendered only in design mode, with i18n keys in en.json.
BEFORE (Design tab, bottom of controls)
────────────────────────────────────────
  Style: [________________]
  ─────────────────────────
  [Save as Profile]
  [Synthesize ▶]

AFTER
────────────────────────────────────────
  Style: [________________]
  Seed
  [123456789_] [🎲 New seed] [☑ Keep this seed]
  ─────────────────────────
  [Save as Profile]
  [Synthesize ▶]

Confidence Score: 3/5

Safe to merge after fixing the unconditional seed store write in useTTS.js; all other changes are additive and well-contained.

The setDesignSeed(xSeed) call in useTTS.js runs for every synthesis — clone/audio included. A user who pins a design seed, switches to audio mode to do a quick clone synth, then returns to design mode will find their seed silently replaced by the clone's backend-generated value. With keepSeed = true this is a persistent regression: the very guarantee the feature advertises stays on the same base timbre breaks the moment the user touches the audio tab.

frontend/src/hooks/useTTS.js — the X-Seed read-back needs a defineMethod === 'design' guard; frontend/src/pages/CloneDesignTab.jsx — inline magic number and missing seed range validation.

Important Files Changed

Filename Overview
frontend/src/hooks/useTTS.js Reads X-Seed from response headers unconditionally — overwrites the pinned design seed even during clone/audio synthesis.
frontend/src/pages/CloneDesignTab.jsx Adds Seed control (input + re-roll button + keep checkbox) in design mode; re-roll button hardcodes 2147483647 instead of importing MAX_SEED, and seed input lacks range validation.
frontend/src/utils/seed.js New seed utility with injectable RNG — clean and well-tested; minor off-by-one means client fresh-rolls can never produce the max seed value.
backend/api/routers/generation.py Materializes a concrete random seed when none was supplied, making all paths reproducible and ensuring X-Seed is always populated.
frontend/src/store/generateSlice.ts Adds designSeed and keepSeed state fields with typed setters — clean, minimal, no issues.
frontend/src/test/seed.test.js Good coverage of pin/re-roll/range cases with injected RNG; existing tests don't catch the off-by-one since they never assert the result equals MAX_SEED.
frontend/src/pages/CloneDesignTab.css Adds CSS for the new seed control row — straightforward, no issues.
frontend/src/i18n/locales/en.json Adds 5 i18n keys for the seed control — correct and complete for the English locale.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant UI as CloneDesignTab
    participant Hook as useTTS
    participant BE as backend /generate

    UI->>Hook: synthesize (keepSeed, designSeed)
    Note over Hook: pickDesignSeed(keepSeed, designSeed)<br/>returns pinned seed OR fresh random
    Hook->>BE: "POST /generate seed=N"
    Note over BE: used_seed = seed (or profile seed)<br/>if still None → random.randint(0,2³¹-1)
    BE-->>Hook: audio stream + X-Seed: N
    Note over Hook: ⚠ setDesignSeed(xSeed) always runs<br/>(not guarded to design mode)
    Hook->>UI: designSeed updated in store
    UI->>UI: display new seed in Seed input
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant UI as CloneDesignTab
    participant Hook as useTTS
    participant BE as backend /generate

    UI->>Hook: synthesize (keepSeed, designSeed)
    Note over Hook: pickDesignSeed(keepSeed, designSeed)<br/>returns pinned seed OR fresh random
    Hook->>BE: "POST /generate seed=N"
    Note over BE: used_seed = seed (or profile seed)<br/>if still None → random.randint(0,2³¹-1)
    BE-->>Hook: audio stream + X-Seed: N
    Note over Hook: ⚠ setDesignSeed(xSeed) always runs<br/>(not guarded to design mode)
    Hook->>UI: designSeed updated in store
    UI->>UI: display new seed in Seed input
Loading

Fix All in Claude Code

Reviews (1): Last reviewed commit: "feat(design): seed pin / re-roll for des..." | Re-trigger Greptile

Comment on lines +163 to +164
const xSeed = parseInt(response.headers.get('X-Seed') || '', 10);
if (Number.isInteger(xSeed)) setDesignSeed(xSeed);

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.

P1 Design seed overwritten by clone synthesis

setDesignSeed(xSeed) runs unconditionally for every synthesis call — audio/clone included. A user who pins a seed (keepSeed = true) and then does a single clone synthesis will have designSeed silently overwritten with the clone's backend-materialized seed. When they return to design mode, their pinned seed is gone and the next synth fires with a stale or unexpected value. Guard this update to design mode: if (defineMethod === 'design' && Number.isInteger(xSeed)) setDesignSeed(xSeed);

Fix in Claude Code

Comment on lines +450 to +451
const n = parseInt(v, 10);
if (Number.isInteger(n)) { setDesignSeed(n); setKeepSeed(true); }

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 No upper-bound guard on manually entered seed

parseInt(v, 10) accepts any integer, including negative numbers and values above MAX_SEED. The backend's torch.manual_seed handles out-of-range values differently than random.randint(0, 2**31-1), and an out-of-range seed won't round-trip cleanly through the X-Seed header display.

Suggested change
const n = parseInt(v, 10);
if (Number.isInteger(n)) { setDesignSeed(n); setKeepSeed(true); }
const n = parseInt(v, 10);
if (Number.isInteger(n) && n >= 0 && n <= MAX_SEED) { setDesignSeed(n); setKeepSeed(true); }

Fix in Claude Code

*/
export function pickDesignSeed(keepSeed, designSeed, rng = Math.random) {
if (keepSeed && Number.isInteger(designSeed)) return designSeed;
return Math.floor(rng() * MAX_SEED);

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 Math.floor(rng() * MAX_SEED) produces [0, MAX_SEED-1], never MAX_SEED itself

The backend uses random.randint(0, 2**31-1) (inclusive upper bound) and can return 2147483647 as X-Seed. The client stores and reuses that value fine, but can never independently generate it during a fresh roll. Using MAX_SEED + 1 as the multiplier aligns both ranges to [0, 2147483647].

Suggested change
return Math.floor(rng() * MAX_SEED);
return Math.floor(rng() * (MAX_SEED + 1));

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/src/i18n/locales/en.json`:
- Around line 200-204: The five new seed-related i18n keys seed_label,
seed_placeholder, seed_keep, seed_reroll, and seed_reroll_hint have been added
to en.json but are missing from all 20 non-English locale files (ar.json,
de.json, es.json, fr.json, hi.json, id.json, it.json, ja.json, ko.json, nl.json,
pl.json, pt.json, ru.json, sv.json, th.json, tr.json, uk.json, vi.json,
zh-CN.json, and zh-TW.json). Add these five keys with appropriate translations
to each of these 20 locale files, ensuring the key names remain identical across
all files while only the values are translated. Alternatively, if translations
are not ready, defer this feature behind a feature flag until all translations
are complete.

In `@frontend/src/pages/CloneDesignTab.jsx`:
- Line 457: The onClick handler in CloneDesignTab.jsx hardcodes the value
2147483647 instead of using the exported MAX_SEED constant from
../utils/seed.js. Import MAX_SEED at the top of the file from ../utils/seed.js,
then replace the hardcoded magic number 2147483647 with (MAX_SEED + 1) in the
setDesignSeed call within the onClick handler to ensure the range is inclusive
of MAX_SEED, matching the backend behavior.
- Around line 436-470: The seed UI block containing the design-seed div with
input, Button, and checkbox is currently inside the audio branch but
conditionally checks for design mode, making it unreachable since both
conditions cannot be true simultaneously. Move the entire block (lines 436-470)
from the audio branch to the design branch by cutting it and pasting it at an
appropriate location such as after the "Starting points" section around line 554
or before the "Save design as profile" section around line 632. When moving it
to the design branch, remove the outer conditional wrapper {defineMethod ===
'design' && ( and its closing )} since the block will already be inside the
design branch, keeping only the inner structure with the design-seed div, input,
Button with Dice5 icon, and the keepSeed checkbox.

In `@frontend/src/test/seed.test.js`:
- Around line 23-26: The boundary assertion in the test for pickDesignSeed is
incorrectly checking for values less than or equal to MAX_SEED, but since the
implementation uses Math.floor(rng() * MAX_SEED), it can only produce values in
the range [0, MAX_SEED - 1]. In the "keeps generated seeds in the 31-bit range"
test, change the second expect statement from toBeLessThanOrEqual(MAX_SEED) to
toBeLessThan(MAX_SEED) to accurately reflect the actual range of values that
pickDesignSeed can generate.

In `@frontend/src/utils/seed.js`:
- Line 17: The seed generation in seed.js uses Math.floor(rng() * MAX_SEED)
which produces an exclusive upper bound, excluding the maximum valid seed value.
Modify the calculation to make the upper bound inclusive by changing it to
Math.floor(rng() * (MAX_SEED + 1)), so the frontend can generate all valid seed
values up to and including 2147483647 to match the backend's inclusive range
defined in generation.py.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 315ec929-8d8f-48de-af72-7b4f36648c2a

📥 Commits

Reviewing files that changed from the base of the PR and between 0e17caa and 082a6ab.

📒 Files selected for processing (8)
  • backend/api/routers/generation.py
  • frontend/src/hooks/useTTS.js
  • frontend/src/i18n/locales/en.json
  • frontend/src/pages/CloneDesignTab.css
  • frontend/src/pages/CloneDesignTab.jsx
  • frontend/src/store/generateSlice.ts
  • frontend/src/test/seed.test.js
  • frontend/src/utils/seed.js

Comment on lines +200 to +204
"seed_label": "Seed",
"seed_placeholder": "random each time",
"seed_keep": "Keep this seed",
"seed_reroll": "New seed",
"seed_reroll_hint": "Roll a new random seed and keep it",

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find locale files missing the new seed-related keys.

cd frontend/src/i18n/locales || exit 1

for key in seed_label seed_placeholder seed_keep seed_reroll seed_reroll_hint; do
  echo "=== Checking key: clone.$key ==="
  for locale in *.json; do
    if ! grep -q "\"$key\"" "$locale"; then
      echo "  MISSING in $locale"
    fi
  done
done

Repository: debpalash/OmniVoice-Studio

Length of output: 2407


Add the 5 new seed-related keys to all 20 non-English locale files.

The guideline is strict: every new i18n key must be present in all 21 frontend/src/i18n/locales/*.json files. The PR adds seed_label, seed_placeholder, seed_keep, seed_reroll, and seed_reroll_hint to en.json only; they're missing from ar.json, de.json, es.json, fr.json, hi.json, id.json, it.json, ja.json, ko.json, nl.json, pl.json, pt.json, ru.json, sv.json, th.json, tr.json, uk.json, vi.json, zh-CN.json, and zh-TW.json.

Either translate and add the keys to all 20 files, or defer this feature behind a feature flag until translations are ready.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/i18n/locales/en.json` around lines 200 - 204, The five new
seed-related i18n keys seed_label, seed_placeholder, seed_keep, seed_reroll, and
seed_reroll_hint have been added to en.json but are missing from all 20
non-English locale files (ar.json, de.json, es.json, fr.json, hi.json, id.json,
it.json, ja.json, ko.json, nl.json, pl.json, pt.json, ru.json, sv.json, th.json,
tr.json, uk.json, vi.json, zh-CN.json, and zh-TW.json). Add these five keys with
appropriate translations to each of these 20 locale files, ensuring the key
names remain identical across all files while only the values are translated.
Alternatively, if translations are not ready, defer this feature behind a
feature flag until all translations are complete.

Source: Coding guidelines

Comment on lines +436 to +470
{/* #526: voice-design seed — show + pin + re-roll so tweaks can
stay on the same base timbre. Design mode only. */}
{defineMethod === 'design' && (
<div className="design-seed">
<div className="label-row">{t('clone.seed_label')}</div>
<div className="design-seed__row">
<input
type="number"
className="input-base design-seed__input"
value={designSeed ?? ''}
placeholder={t('clone.seed_placeholder')}
onChange={e => {
const v = e.target.value.trim();
if (v === '') { setDesignSeed(null); return; }
const n = parseInt(v, 10);
if (Number.isInteger(n)) { setDesignSeed(n); setKeepSeed(true); }
}}
/>
<Button
variant="subtle"
size="sm"
onClick={() => { setDesignSeed(Math.floor(Math.random() * 2147483647)); setKeepSeed(true); }}
leading={<Dice5 size={12} />}
title={t('clone.seed_reroll_hint')}
>
{t('clone.seed_reroll')}
</Button>
<label className="design-seed__keep">
<input type="checkbox" checked={keepSeed} onChange={e => setKeepSeed(e.target.checked)} />
<span>{t('clone.seed_keep')}</span>
</label>
</div>
</div>
)}

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.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Seed UI is in the wrong branch and will never render.

Lines 436–470 are inside the defineMethod === 'audio' branch (which starts at line 369), yet line 438 gates the block with {defineMethod === 'design' && (. Both conditions can never be true at the same time, so this UI is unreachable.

The comment on line 437 confirms "Design mode only" — the entire block should be in the design branch (after line 498, inside the ) : ( block that runs from lines 499–658).

🔧 Move the block to the design branch

Cut lines 436–470 and paste them into the design branch. A natural placement would be after the "Starting points" section (after line 554) and before the "Identity recipe" collapsible (before line 559), or immediately before the "Save design as profile" section (before line 632).

Example diff (placing it before the save-profile section):

In the audio branch (line 435 area), remove:

            </div>

-            {/* `#526`: voice-design seed — show + pin + re-roll so tweaks can
-                stay on the same base timbre. Design mode only. */}
-            {defineMethod === 'design' && (
-              <div className="design-seed">
-                ...
-              </div>
-            )}
-
            {/* Save as profile */}

In the design branch (before line 632), add:

            </div>
            )}

+            {/* `#526`: voice-design seed — show + pin + re-roll so tweaks can
+                stay on the same base timbre. Design mode only. */}
+            <div className="design-seed">
+              <div className="label-row">{t('clone.seed_label')}</div>
+              <div className="design-seed__row">
+                <input
+                  type="number"
+                  className="input-base design-seed__input"
+                  value={designSeed ?? ''}
+                  placeholder={t('clone.seed_placeholder')}
+                  onChange={e => {
+                    const v = e.target.value.trim();
+                    if (v === '') { setDesignSeed(null); return; }
+                    const n = parseInt(v, 10);
+                    if (Number.isInteger(n)) { setDesignSeed(n); setKeepSeed(true); }
+                  }}
+                />
+                <Button
+                  variant="subtle"
+                  size="sm"
+                  onClick={() => { setDesignSeed(Math.floor(Math.random() * 2147483647)); setKeepSeed(true); }}
+                  leading={<Dice5 size={12} />}
+                  title={t('clone.seed_reroll_hint')}
+                >
+                  {t('clone.seed_reroll')}
+                </Button>
+                <label className="design-seed__keep">
+                  <input type="checkbox" checked={keepSeed} onChange={e => setKeepSeed(e.target.checked)} />
+                  <span>{t('clone.seed_keep')}</span>
+                </label>
+              </div>
+            </div>
+
            {/* Save the current design as a reusable profile (0005): the

Remove the {defineMethod === 'design' && ( wrapper and its closing )} since we're already in the design branch.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{/* #526: voice-design seed show + pin + re-roll so tweaks can
stay on the same base timbre. Design mode only. */}
{defineMethod === 'design' && (
<div className="design-seed">
<div className="label-row">{t('clone.seed_label')}</div>
<div className="design-seed__row">
<input
type="number"
className="input-base design-seed__input"
value={designSeed ?? ''}
placeholder={t('clone.seed_placeholder')}
onChange={e => {
const v = e.target.value.trim();
if (v === '') { setDesignSeed(null); return; }
const n = parseInt(v, 10);
if (Number.isInteger(n)) { setDesignSeed(n); setKeepSeed(true); }
}}
/>
<Button
variant="subtle"
size="sm"
onClick={() => { setDesignSeed(Math.floor(Math.random() * 2147483647)); setKeepSeed(true); }}
leading={<Dice5 size={12} />}
title={t('clone.seed_reroll_hint')}
>
{t('clone.seed_reroll')}
</Button>
<label className="design-seed__keep">
<input type="checkbox" checked={keepSeed} onChange={e => setKeepSeed(e.target.checked)} />
<span>{t('clone.seed_keep')}</span>
</label>
</div>
</div>
)}
{/* Save as profile */}
{designProfile && (
🧰 Tools
🪛 ast-grep (0.43.0)

[warning] 450-450: Avoid using the initial state variable in setState
Context: setDesignSeed(n)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/CloneDesignTab.jsx` around lines 436 - 470, The seed UI
block containing the design-seed div with input, Button, and checkbox is
currently inside the audio branch but conditionally checks for design mode,
making it unreachable since both conditions cannot be true simultaneously. Move
the entire block (lines 436-470) from the audio branch to the design branch by
cutting it and pasting it at an appropriate location such as after the "Starting
points" section around line 554 or before the "Save design as profile" section
around line 632. When moving it to the design branch, remove the outer
conditional wrapper {defineMethod === 'design' && ( and its closing )} since the
block will already be inside the design branch, keeping only the inner structure
with the design-seed div, input, Button with Dice5 icon, and the keepSeed
checkbox.

<Button
variant="subtle"
size="sm"
onClick={() => { setDesignSeed(Math.floor(Math.random() * 2147483647)); setKeepSeed(true); }}

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.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Import MAX_SEED instead of hardcoding 2147483647.

Line 457 hardcodes the constant that's already exported from ../utils/seed.js.

♻️ Use the exported constant

At the top of the file, extend the seed import:

+import { MAX_SEED } from '../utils/seed';

Then replace the magic number:

-                  onClick={() => { setDesignSeed(Math.floor(Math.random() * 2147483647)); setKeepSeed(true); }}
+                  onClick={() => { setDesignSeed(Math.floor(Math.random() * (MAX_SEED + 1))); setKeepSeed(true); }}

(Note: the + 1 makes the range inclusive of MAX_SEED, matching the backend — see the earlier seed.js boundary comment.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/CloneDesignTab.jsx` at line 457, The onClick handler in
CloneDesignTab.jsx hardcodes the value 2147483647 instead of using the exported
MAX_SEED constant from ../utils/seed.js. Import MAX_SEED at the top of the file
from ../utils/seed.js, then replace the hardcoded magic number 2147483647 with
(MAX_SEED + 1) in the setDesignSeed call within the onClick handler to ensure
the range is inclusive of MAX_SEED, matching the backend behavior.

Comment on lines +23 to +26
it('keeps generated seeds in the 31-bit range', () => {
expect(pickDesignSeed(false, null, () => 0)).toBeGreaterThanOrEqual(0);
expect(pickDesignSeed(false, null, () => 0.9999999999)).toBeLessThanOrEqual(MAX_SEED);
});

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Boundary test doesn't match implementation ceiling.

Line 25 checks ≤ MAX_SEED, but pickDesignSeed with Math.floor(rng() * MAX_SEED) can only produce [0, MAX_SEED - 1] — the test should assert < MAX_SEED or ≤ MAX_SEED - 1.

🧪 Tighten the assertion
-    expect(pickDesignSeed(false, null, () => 0.9999999999)).toBeLessThanOrEqual(MAX_SEED);
+    expect(pickDesignSeed(false, null, () => 0.9999999999)).toBeLessThan(MAX_SEED);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/test/seed.test.js` around lines 23 - 26, The boundary assertion
in the test for pickDesignSeed is incorrectly checking for values less than or
equal to MAX_SEED, but since the implementation uses Math.floor(rng() *
MAX_SEED), it can only produce values in the range [0, MAX_SEED - 1]. In the
"keeps generated seeds in the 31-bit range" test, change the second expect
statement from toBeLessThanOrEqual(MAX_SEED) to toBeLessThan(MAX_SEED) to
accurately reflect the actual range of values that pickDesignSeed can generate.

*/
export function pickDesignSeed(keepSeed, designSeed, rng = Math.random) {
if (keepSeed && Number.isInteger(designSeed)) return designSeed;
return Math.floor(rng() * MAX_SEED);

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Seed range boundary inconsistency with backend.

Math.floor(rng() * MAX_SEED) generates [0, 2147483646] (max is MAX_SEED - 1), but the backend's random.randint(0, 2**31 - 1) in generation.py:474 is inclusive of 2147483647. A backend-materialized seed of 2147483647 is valid but unreachable from client-side generation.

🔧 Align the range

Option 1 (preferred): Make the frontend inclusive by adding 1 before flooring:

-  return Math.floor(rng() * MAX_SEED);
+  return Math.floor(rng() * (MAX_SEED + 1));

Option 2: Change the backend to match the frontend's exclusive upper bound:

-        used_seed = random.randint(0, 2**31 - 1)
+        used_seed = random.randint(0, 2**31 - 2)

and update the constant doc in seed.js line 6-7 to reflect 2**31 - 2.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/utils/seed.js` at line 17, The seed generation in seed.js uses
Math.floor(rng() * MAX_SEED) which produces an exclusive upper bound, excluding
the maximum valid seed value. Modify the calculation to make the upper bound
inclusive by changing it to Math.floor(rng() * (MAX_SEED + 1)), so the frontend
can generate all valid seed values up to and including 2147483647 to match the
backend's inclusive range defined in generation.py.

@debpalash
debpalash merged commit 7393ae8 into main Jun 20, 2026
15 checks passed
@debpalash
debpalash deleted the feat/design-seed-pin-reroll branch June 20, 2026 23:09
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.

1 participant