feat(design): seed pin / re-roll for designed voices (#526) - #577
Conversation
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>
📝 WalkthroughWalkthroughAdds reproducible voice-design seed support across the full stack. A new ChangesVoice-Design Seed Pinning
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
Panel ReviewML inference / backend —
Audio DSP —
Desktop systems / hook — const seed = parseInt(response.headers.get("X-Seed"));
if (!isNaN(seed)) setDesignSeed(seed);
Product polish — When a user clears the seed input, Nit —
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes 🚥 Pre-merge checks | ✅ 7 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (7 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
|
| 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
%%{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
Reviews (1): Last reviewed commit: "feat(design): seed pin / re-roll for des..." | Re-trigger Greptile
| const xSeed = parseInt(response.headers.get('X-Seed') || '', 10); | ||
| if (Number.isInteger(xSeed)) setDesignSeed(xSeed); |
There was a problem hiding this comment.
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);
| const n = parseInt(v, 10); | ||
| if (Number.isInteger(n)) { setDesignSeed(n); setKeepSeed(true); } |
There was a problem hiding this comment.
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.
| 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); } |
| */ | ||
| export function pickDesignSeed(keepSeed, designSeed, rng = Math.random) { | ||
| if (keepSeed && Number.isInteger(designSeed)) return designSeed; | ||
| return Math.floor(rng() * MAX_SEED); |
There was a problem hiding this comment.
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].
| 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!
There was a problem hiding this comment.
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
📒 Files selected for processing (8)
backend/api/routers/generation.pyfrontend/src/hooks/useTTS.jsfrontend/src/i18n/locales/en.jsonfrontend/src/pages/CloneDesignTab.cssfrontend/src/pages/CloneDesignTab.jsxfrontend/src/store/generateSlice.tsfrontend/src/test/seed.test.jsfrontend/src/utils/seed.js
| "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", |
There was a problem hiding this comment.
🧩 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
doneRepository: 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
| {/* #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> | ||
| )} | ||
|
|
There was a problem hiding this comment.
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): theRemove 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.
| {/* #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); }} |
There was a problem hiding this comment.
🛠️ 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.
| 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); | ||
| }); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
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 acceptedseedand echoedX-Seed, but leftused_seed=Nonewhen nothing supplied one — non-deterministic, unreproducible, emptyX-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:
designSeed+keepSeed.pickDesignSeed), else rolls a fresh one, and reads the authoritative seed back from theX-Seedresponse header.Tests
seed.test.js(pickDesignSeed): pin when kept+valid, re-roll when off / nothing valid pinned, 31-bit range guard.typecheck:cigreen.Notes
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:
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 stylingSeed 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"]Key Changes
Backend (
backend/api/routers/generation.py):used_seedwhen no seed is suppliedX-SeedheaderFrontend State (
generateSlice.ts):designSeed: number | null— stores the current pinned seedkeepSeed: boolean— toggles whether to reuse the pinned seedSeed Logic (
seed.js):pickDesignSeed(keepSeed, designSeed, rng)utilityMAX_SEED = 2147483647) for display and storageHook Integration (
useTTS.js):pickDesignSeedfor design-mode synthesisX-Seedresponse header after generation to update UI with authoritative seed valuekeepSeed,designSeed, andsetDesignSeedto dependency listUI Controls (
CloneDesignTab.jsx):nullwhen emptied)Localization (
en.json):clonelocaleTesting
seed.test.jsvalidatespickDesignSeedbehavior:Full vitest suite (557 tests) and typecheck:ci pass locally.