Skip to content

feat: direct browser TTS calls via js-tts-wrapper#89

Merged
enaboapps merged 2 commits intomainfrom
feat/88-tts-wrapper-browser
Apr 8, 2026
Merged

feat: direct browser TTS calls via js-tts-wrapper#89
enaboapps merged 2 commits intomainfrom
feat/88-tts-wrapper-browser

Conversation

@enaboapps
Copy link
Copy Markdown
Owner

@enaboapps enaboapps commented Apr 8, 2026

Summary

Notes

The upstream fix is in PR willwade/js-tts-wrapper#33. Once that is merged and published, package.json should be updated to reference the new npm version instead of the local path.

Test plan

  • Build passes (npm run build)
  • ElevenLabs TTS synthesizes speech on the talk page
  • Azure TTS synthesizes speech on the talk page
  • Voices list loads in Settings for both providers
  • After local js-tts-wrapper fix is published, update to npm version

Closes #88

Summary by CodeRabbit

  • Refactor

    • Moved text-to-speech synthesis and voice retrieval to a client-based implementation, replacing prior server-route handling.
  • Chores

    • Updated TTS wrapper dependency (js-tts-wrapper) to ^0.1.72.

Replace API-route-proxied TTS with direct browser calls using
js-tts-wrapper's AzureTTSClient and ElevenLabsTTSClient. The library
now has browser support so we can call synthesize/getVoices directly
without the /api/tts proxy.

Delete app/api/tts/speak and app/api/tts/voices routes.

Closes #88
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 8, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
podium-web Ready Ready Preview, Comment Apr 8, 2026 2:31pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 8, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ab8266ad-afba-4b6e-a8f7-66ec99da6bc0

📥 Commits

Reviewing files that changed from the base of the PR and between dc8c1aa and 76fd5cc.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (1)
  • package.json
✅ Files skipped from review due to trivial changes (1)
  • package.json

📝 Walkthrough

Walkthrough

Removed two server-side TTS API routes and refactored client-side TTS to use a local js-tts-wrapper client via new helpers in lib/tts.ts; dependency version of js-tts-wrapper was updated.

Changes

Cohort / File(s) Summary
API Route Removal
app/api/tts/speak/route.ts, app/api/tts/voices/route.ts
Deleted POST route handlers that proxied TTS synth and voices-list requests to ElevenLabs/Azure.
TTS Client Refactor
lib/tts.ts
Added createClient(config) and rewrote fetchTTSBlob() and fetchVoices() to call local TTS client methods (synthToBytes, getVoices) instead of calling internal /api/tts/* endpoints.
Dependency Update
package.json
Bumped js-tts-wrapper dependency from ^0.1.69^0.1.72.

Sequence Diagram(s)

sequenceDiagram
  participant UI as Browser UI
  participant Lib as lib/tts.ts
  participant Client as js-tts-wrapper Client
  participant Provider as External TTS Provider

  UI->>Lib: fetchTTSBlob(text, config)
  Lib->>Client: createClient(config) / synthToBytes(text)
  Client->>Provider: HTTP TTS request (ElevenLabs/Azure)
  Provider-->>Client: audio bytes
  Client-->>Lib: audio bytes
  Lib-->>UI: Blob (audio/mpeg)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hopped from routes to local code,
No more proxy paths on the road.
I hum to wrappers, voices near,
Bytes in my paws, a melody clear. 🎶

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: moving from API route proxies to direct browser TTS calls using js-tts-wrapper.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/88-tts-wrapper-browser

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
lib/tts.ts (1)

17-26: Avoid applying voice selection during voice catalog fetch.

fetchVoices() only needs provider credentials, but lines 20 and 24 apply config.voiceId for both providers. When switching providers, the persisted settings.voiceId from the previous provider is passed to the new client before getVoices() runs, creating unnecessary coupling between voice lookups and selection state.

Voice selection should be opt-in for synthesis only. Only fetchTTSBlob() (line 29) should apply the voice ID; fetchVoices() (line 35) should fetch without it.

♻️ Suggested refactor
-function createClient(config: TTSConfig) {
+function createClient(config: TTSConfig, applyVoice = false) {
   if (config.provider === 'azure') {
     const client = new AzureTTSClient({ subscriptionKey: config.subscriptionKey, region: config.region });
-    if (config.voiceId) client.setVoice(config.voiceId);
+    if (applyVoice && config.voiceId) client.setVoice(config.voiceId);
     return client;
   }
   const client = new ElevenLabsTTSClient({ apiKey: config.apiKey });
-  if (config.voiceId) client.setVoice(config.voiceId);
+  if (applyVoice && config.voiceId) client.setVoice(config.voiceId);
   return client;
 }
@@
-  const client = createClient(config);
+  const client = createClient(config, true);
@@
   const client = createClient(config);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/tts.ts` around lines 17 - 26, The createClient function is applying
config.voiceId to new provider clients, coupling voice selection to voice
catalog fetch; remove the voiceId application from createClient so it returns
clients created only with credentials (AzureTTSClient in createClient and
ElevenLabsTTSClient in createClient). Instead, apply voice selection only when
performing synthesis in fetchTTSBlob (call setVoice(config.voiceId) on the
client returned for use in fetchTTSBlob), while fetchVoices should call
createClient and call getVoices() without setting any voiceId. Update references
to createClient, fetchVoices, and fetchTTSBlob accordingly so voice selection is
opt-in for synthesis only.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/tts.ts`:
- Around line 29-31: The Blob construction uses bytes.buffer which can include
data outside the Uint8Array view; change the return to pass the returned byte
view directly into Blob (i.e., use new Blob([bytes]) instead of new
Blob([bytes.buffer])) after obtaining the bytes from client.synthToBytes(text)
in the function that calls createClient and synthToBytes so sliced views keep
correct byteOffset/byteLength.

In `@package.json`:
- Line 19: The package.json currently depends on a non-portable sibling path
"js-tts-wrapper" via "file:../js-tts-wrapper"; replace that entry for the
"js-tts-wrapper" dependency with a portable spec (for example a published semver
version, a git URL, or a tarball URL / temporary fork) so installs work on clean
checkouts/CI; update the "js-tts-wrapper" value in package.json accordingly and
run npm install / npm run build locally to verify resolution.

---

Nitpick comments:
In `@lib/tts.ts`:
- Around line 17-26: The createClient function is applying config.voiceId to new
provider clients, coupling voice selection to voice catalog fetch; remove the
voiceId application from createClient so it returns clients created only with
credentials (AzureTTSClient in createClient and ElevenLabsTTSClient in
createClient). Instead, apply voice selection only when performing synthesis in
fetchTTSBlob (call setVoice(config.voiceId) on the client returned for use in
fetchTTSBlob), while fetchVoices should call createClient and call getVoices()
without setting any voiceId. Update references to createClient, fetchVoices, and
fetchTTSBlob accordingly so voice selection is opt-in for synthesis only.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: c3852e7c-5b23-4253-ac67-39567fee1a9e

📥 Commits

Reviewing files that changed from the base of the PR and between 6f92381 and dc8c1aa.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • app/api/tts/speak/route.ts
  • app/api/tts/voices/route.ts
  • lib/tts.ts
  • package.json
💤 Files with no reviewable changes (2)
  • app/api/tts/voices/route.ts
  • app/api/tts/speak/route.ts

Comment on lines +29 to +31
const client = createClient(config);
const bytes = await client.synthToBytes(text);
return new Blob([bytes.buffer as ArrayBuffer]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Pass the returned byte view directly into Blob.

Line 31 uses bytes.buffer, which ignores byteOffset and byteLength. If synthToBytes() returns a sliced Uint8Array/Buffer, the blob will include bytes outside the real payload and can corrupt playback. Blob already accepts the view itself.

🛠️ Proposed fix
 export async function fetchTTSBlob(text: string, config: TTSConfig): Promise<Blob> {
   const client = createClient(config);
   const bytes = await client.synthToBytes(text);
-  return new Blob([bytes.buffer as ArrayBuffer]);
+  return new Blob([bytes]);
 }
📝 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
const client = createClient(config);
const bytes = await client.synthToBytes(text);
return new Blob([bytes.buffer as ArrayBuffer]);
export async function fetchTTSBlob(text: string, config: TTSConfig): Promise<Blob> {
const client = createClient(config);
const bytes = await client.synthToBytes(text);
return new Blob([bytes]);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/tts.ts` around lines 29 - 31, The Blob construction uses bytes.buffer
which can include data outside the Uint8Array view; change the return to pass
the returned byte view directly into Blob (i.e., use new Blob([bytes]) instead
of new Blob([bytes.buffer])) after obtaining the bytes from
client.synthToBytes(text) in the function that calls createClient and
synthToBytes so sliced views keep correct byteOffset/byteLength.

@enaboapps enaboapps merged commit 9fe974c into main Apr 8, 2026
3 checks passed
@enaboapps enaboapps deleted the feat/88-tts-wrapper-browser branch April 8, 2026 14:34
enaboapps pushed a commit that referenced this pull request Apr 9, 2026
…te (#99)

After the js-tts-wrapper migration (PR #89), browsers with the service
worker installed were serving old cached chunks that still called the
removed /api/tts/speak route, causing 404 errors and TTS failure.

Bumping CACHE to podium-v4 forces all clients to fetch fresh bundles.
Added old-cache cleanup in the activate handler so stale caches are
automatically deleted on each SW update going forward.
enaboapps added a commit that referenced this pull request Apr 9, 2026
* fix: fetch audio cache segments sequentially to avoid rate limits (#97)

* fix: bump service worker cache to v4 and evict stale caches on activate (#99)

After the js-tts-wrapper migration (PR #89), browsers with the service
worker installed were serving old cached chunks that still called the
removed /api/tts/speak route, causing 404 errors and TTS failure.

Bumping CACHE to podium-v4 forces all clients to fetch fresh bundles.
Added old-cache cleanup in the activate handler so stale caches are
automatically deleted on each SW update going forward.

* fix: use package.json version as service worker cache key

Instead of a manually-bumped hardcoded version string, the SW cache key
is now derived from the app version:

- next.config.ts exposes npm_package_version as NEXT_PUBLIC_APP_VERSION
- layout.tsx registers /sw.js?v=<version> so the query string changes
  automatically on each release
- sw.js reads self.location.search to build its cache key (podium-<version>)

Cache busting now happens automatically whenever package.json version is
bumped — no manual SW edits required.

---------

Co-authored-by: Owen McGirr <o.a.mcgirr@gmail.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.

feat: use js-tts-wrapper browser support directly

2 participants