Conversation
PR Review — Loreframe StudioRisk: medium Automated review from Findings
Changed files
CONTRIBUTING checklist
Posted by the repo PR review workflow. Re-runs on each push to the PR. |
Code healthQuality score: 64.5/100Higher is better. The score is a trend dashboard; the independent ratchet below remains the CI gate.
Change vs PR base: +0.0 points.
Markdown, JSON catalogs and tests are out of this table. Only Most complex functions
Trend vs baseline
Warnings
Ratchet passed. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Full recordings fail duration check
- Se confirmó el desajuste entre el tope de 30s y duration > 30; ahora se acepta hasta 30.5s para el overshoot de timeslice/decode y se sigue rechazando 31s+.
Or push these changes by commenting:
@cursor push 8b7b8875fa
Preview (8b7b8875fa)
diff --git a/ui/src/features/characters/characterVoiceReference.ts b/ui/src/features/characters/characterVoiceReference.ts
--- a/ui/src/features/characters/characterVoiceReference.ts
+++ b/ui/src/features/characters/characterVoiceReference.ts
@@ -9,7 +9,7 @@
try { buffer = await new OfflineAudioContext(1, 1, 24000).decodeAudioData(await blob.arrayBuffer()) }
catch { throw new Error('voiceFileDecode') }
signal.throwIfAborted()
- if (!Number.isFinite(buffer.duration) || buffer.duration < 3 || buffer.duration > 30) throw new Error('voiceFileDuration')
+ if (!Number.isFinite(buffer.duration) || buffer.duration < 3 || buffer.duration > 30.5) throw new Error('voiceFileDuration')
const wav = await voiceWav(buffer)
signal.throwIfAborted()
const uploaded = await uploadLocalAsset(new File([wav], 'voice-reference.wav', { type: 'audio/wav' }), signal)
diff --git a/ui/tests/customCharacterVoiceFields.test.tsx b/ui/tests/customCharacterVoiceFields.test.tsx
--- a/ui/tests/customCharacterVoiceFields.test.tsx
+++ b/ui/tests/customCharacterVoiceFields.test.tsx
@@ -61,6 +61,24 @@
assert.equal(uploads, 1, 'selecting a saved voice neither uploads nor generates')
})
+test('a decoded duration just over the 30s stop timer still uploads the recording', async t => {
+ const { render, fireEvent, waitFor, cleanup } = await import('@testing-library/react')
+ const { CustomCharacterVoiceFields } = await import('../src/features/characters/CustomCharacterVoiceFields')
+ mockDecode(t, 30.25)
+ const original = globalThis.fetch; let uploads = 0
+ globalThis.fetch = async () => {
+ uploads++
+ return json({ filename: 'cap.wav', url: '/api/v1/uploads/cap.wav', path: '/server/private/cap.wav' })
+ }
+ t.after(() => { cleanup(); globalThis.fetch = original })
+ const changes: CustomCharacterVoice[] = []
+ const view = render(<CustomCharacterVoiceFields value={saved} onChange={value => changes.push(value)} />)
+ fireEvent.change(view.getByLabelText('Import audio sample'), { target: { files: [new File(['audio'], 'cap.wav', { type: 'audio/wav' })] } })
+ await waitFor(() => assert.equal(uploads, 1))
+ assert.equal(changes[0].referenceAudio, '/api/v1/uploads/cap.wav')
+ assert.equal(view.queryByText(/The sample must last 3–30 seconds/), null)
+})
+
test('invalid duration leaves the previous voice intact and does not upload a silently cropped recording', async t => {
const { render, fireEvent, cleanup } = await import('@testing-library/react')
const { CustomCharacterVoiceFields } = await import('../src/features/characters/CustomCharacterVoiceFields')You can send follow-ups to the cloud agent here.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 3363f69. Configure here.
| try { buffer = await new OfflineAudioContext(1, 1, 24000).decodeAudioData(await blob.arrayBuffer()) } | ||
| catch { throw new Error('voiceFileDecode') } | ||
| signal.throwIfAborted() | ||
| if (!Number.isFinite(buffer.duration) || buffer.duration < 3 || buffer.duration > 30) throw new Error('voiceFileDuration') |
There was a problem hiding this comment.
Full recordings fail duration check
Medium Severity
A microphone take that runs until the advertised 30-second cap is rejected as too long. recordMicrophone stops on a 30s timer, then MediaRecorder still emits its current 250ms timeslice, so decoded buffer.duration often lands just above 30 and uploadVoiceReference throws voiceFileDuration. The user loses the recording after waiting the full allowed time.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 3363f69. Configure here.



Character Creator could only store Qwen3 presets and hid their original language and timbre. It now supports reusable voices from imported audio or microphone recordings, plus Spanish/English auditions before configuring mouths or saving a character.
Recording requires HTTPS or localhost; importing also works over LAN HTTP.
Validation: the full 1,825-test UI suite, 93 focused Python tests, production build, catalogs, bundle budget, changed-source lint and the exact development code-health comparison passed. Follow-up coverage includes 89 CI inventory/architecture/voice contracts and six import/capture/timing regression tests; latest TypeScript and lint pass. The new Python test is registered in CI and the architecture reader inventory. GitHub CI is rerunning the final capture-timing adjustment.
A real browser imported the bundled speech sample, stored it through the existing upload API and played the resulting 4.27-second WAV. Real TTS audition was blocked by CUDA out of memory while another process occupied the GPU. Microphone permission/recording/cancellation and persistence contracts are covered by automated tests.
Integration with the independent release fix in #418 was checked in an isolated checkout: #417 at
8cdc68c4and #418 at5a5a069ecombine without conflicts and pass TypeScript plus 49 focused tests. Independent review identified the capture/switching race, which is fixed and covered by a regression test.