Uh oh!
There was an error while loading. Please reload this page.
Add optional ElevenLabs speech with personal voices - #600
Conversation
A failing ElevenLabs socket said only that it had closed, which named a failure nobody could act on. The two places its own account was being dropped now carry it: An error frame states its failure twice, as a sentence in `message` and as an identifier in `error`, and either may be absent; only `error` was read, so a frame carrying the sentence alone fell through unread and the turn ended on the silent close behind it. A close carries a code and, where the service refused something, its reason. Both were discarded, and the error event that precedes every close reported first with neither. Reporting now waits for the close, which is the event that knows why. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A speech socket that says nothing is unreadable from outside: the words are drawn, no failure is reported, and whether the frames never arrived or arrived and were never heard cannot be told apart from the panel. The tap is the realtime call's own, so one reply's speech frames read in order beside the events that drove them, behind the same unpackaged gate and recorded nowhere else. Audio is counted rather than carried, and each frame notes what is scheduled but unheard, because a playhead that never advances is silence with nothing wrong on the wire. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… it cannot The renderer's connect policy named no ElevenLabs host, so the browser refused the speech socket before it was ever built. The policy now names the one address the renderer reaches there: a websocket, because the key stays in the main process and no https request is ever made from this side. A refused socket throws out of the constructor rather than failing on an event, and the open runs detached from any caller, so the throw was rejecting a promise nobody held. The reply kept its words, reported nothing, and owed a drain that no close and no error would arrive to settle — a reply that simply stopped. It is now the failure it always was. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A single-use token names the socket it may be spent on, and the dialogue socket refuses a text-to-speech one by name. The mint now tracks the socket it is minted for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The settings path moved to Developers, and the address Luke opened was answering with a permanent redirect. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The dialogue socket was the expressive model's, and Luke does not need what it charges for. Flash v2.5 costs half the credits per character and begins speaking in about a quarter of the time, and Luke speaks unprompted all day, so both compound. The socket is a different one, not the same one renamed. The voice moves into the address, the opening frame is a bare space, a delta is its own text, and an empty text both flushes and closes — so the reply's last words never need a flush of their own. The server answers in camelCase here, and its ending is one field rather than two, a turn having no meaning where there is no dialogue. A voice id reaching a path segment is encoded, because a separator inside one would address something else entirely. The keepalive goes with it: this socket documents no such frame, and the only gaps it has to survive are between one generated token and the next. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The latest updates on your projects. Learn more about Vercel for GitHub. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
| }); | ||
| this.#ended = true; | ||
| this.#armDrain(); | ||
| } |
There was a problem hiding this comment.
Malformed audio frames can throw
Medium Severity
#readFrame passes frame.audio to decodeSpeechAudio, and atob throws on invalid base64. That throw is uncaught, so a bad or final-and-bad frame never reaches the final handling or #fail. The reply can miss its last words, surface a close error, or sit on the settle backstop instead of draining cleanly.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 4fcb3f8. Configure here.
…elds Both the voice list and the token mint failed in the same five ways, so they now share ELEVENLABS_OUTCOME and explain only the failures. The detail field neither IPC handler forwarded is gone, as is the outcome the renderer never read off the voices answer, the cursor-cycle guard the page cap already covers, and the barrel exports only tests imported. PCM decoding rides Int16Array instead of hand-rolled sign math. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 2798b6f. Configure here.
| } | ||
| if (page.nextPageToken === undefined || voices.length >= MAXIMUM_VOICES) { | ||
| return { outcome: ELEVENLABS_OUTCOME.OK, voices }; |
There was a problem hiding this comment.
Voice list pagination can hang
Medium Severity
Removing the seen-cursor guard means listElevenlabsVoices now stops only when a page has no token or voices hits MAXIMUM_VOICES. A repeating next_page_token on pages that add no valid voices never reaches that cap, so the main-process read loops and the Voice page never leaves refreshing.
Reviewed by Cursor Bugbot for commit 2798b6f. Configure here.


Summary
Evidence
./scripts/check.shpassed./scripts/verify.sh):passedAutomated visual evidence
Download the deterministic macOS evidence · workflow run
4fcb3f88eafbe51db8756ab30b60037b49b2c7c7smokePhysical-device evidence
not attachednot performednot recordedNotes
Open in Alchemize