Skip to content

feat: add ReadAloud component with Puter AI and Web Speech API support#2

Merged
programmersd21 merged 3 commits into
programmersd21:masterfrom
Harshjeet:harsh-dev
Jul 22, 2026
Merged

feat: add ReadAloud component with Puter AI and Web Speech API support#2
programmersd21 merged 3 commits into
programmersd21:masterfrom
Harshjeet:harsh-dev

Conversation

@Harshjeet

@Harshjeet Harshjeet commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary by Sourcery

Add a read-aloud capability to documentation pages using a new component integrated into the VitePress theme, with support for both Puter AI (OpenAI-based) and browser-native speech synthesis.

New Features:

  • Introduce a ReadAloud Vue component that reads documentation content aloud with configurable speed and voice options.
  • Enable users to choose between Puter AI TTS and browser-native Web Speech API for audio playback of docs.

Enhancements:

  • Integrate the ReadAloud controls into the VitePress doc layout header area.
  • Improve Mermaid rendering to automatically respect the current light or dark theme and re-render on theme changes.

Build:

  • Add the @heyputer/puter.js dependency and tidy devDependency ordering in package.json.

Summary by CodeRabbit

  • New Features
    • Added read-aloud controls to documentation pages.
    • Supports browser speech and AI-powered narration, with automatic fallback.
    • Includes play, pause, stop, playback speed, voice, and narration engine options.
    • Displays loading and playback progress while reading articles.
    • Mermaid diagrams now update correctly when switching between light and dark themes.

@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@sourcery-ai

sourcery-ai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a ReadAloud component to the VitePress theme that provides text-to-speech for documentation pages using either Puter AI (OpenAI TTS) or the browser’s Web Speech API, wires it into the layout, and introduces the Puter client dependency along with some theme/refactor adjustments.

Sequence diagram for ReadAloud playback with Puter AI vs Web Speech

sequenceDiagram
  actor User
  participant ReadAloud
  participant BrowserSpeech as window_speechSynthesis
  participant Puter as puter_ai_txt2speech

  User->>ReadAloud: togglePlayPause()
  ReadAloud->>ReadAloud: getArticleText()
  ReadAloud->>ReadAloud: splitIntoChunks(text)
  alt ttsMode is puter
    ReadAloud->>ReadAloud: fetchChunkAudio(0), fetchChunkAudio(1)
    ReadAloud->>Puter: txt2speech(chunk,{provider:model:voice})
    Puter-->>ReadAloud: HTMLAudioElement
    ReadAloud->>ReadAloud: speakCurrentPuterChunk()
    ReadAloud->>ReadAloud: audio.play()
    ReadAloud->>ReadAloud: fetchChunkAudio(next chunks)
    ReadAloud-->>User: article audio via Puter AI
  else ttsMode is browser
    ReadAloud->>BrowserSpeech: cancel()
    ReadAloud->>BrowserSpeech: speak(SpeechSynthesisUtterance)
    BrowserSpeech-->>ReadAloud: onend / onerror callbacks
    ReadAloud-->>User: article audio via Web Speech API
  end

  User->>ReadAloud: stopSpeech()
  ReadAloud->>BrowserSpeech: cancel()
  ReadAloud->>ReadAloud: clearAudioCache()
  ReadAloud-->>User: playback stopped
Loading

File-Level Changes

Change Details Files
Introduce ReadAloud Vue component that reads .vp-doc content aloud using Puter AI or browser speech synthesis, with chunking, voice selection, speed control, caching, and route-aware lifecycle management.
  • Implement TTS mode switching between Puter AI (OpenAI tts-1 voices) and browser native SpeechSynthesis
  • Extract article text from the documentation DOM, normalize it, and split into manageable chunks for streaming playback
  • Integrate Puter AI txt2speech calls with background prefetch and an audio cache to minimize playback gaps between chunks
  • Use SpeechSynthesisUtterance for browser-based playback including voice selection, rate control, and robust error handling
  • Manage play/pause/stop behavior, route change handling, and cleanup of audio/speech resources on unmount or navigation
  • Provide a responsive UI bar with play/pause, stop, engine selector, voice selector, speed pills, and loading feedback
docs/.vitepress/theme/components/ReadAloud.vue
Wire ReadAloud into the VitePress theme layout and register it as a global component, while improving Mermaid rendering behavior with dark-mode-aware theme selection.
  • Import ReadAloud component and register it via enhanceApp so it can be used across docs
  • Inject ReadAloud into the DefaultTheme.Layout via the doc-before slot so it appears before document content
  • Refactor Mermaid initialization to render Mermaids with a dark or default theme based on isDark and to re-run rendering on dark-mode changes using nextTick callbacks
  • Adjust Vue App type import style and move theme stylesheet import order for better organization
docs/.vitepress/theme/index.ts
docs/.vitepress/theme/style.css
Add Puter client library dependency and tidy devDependencies ordering in package.json, propagating lockfile changes.
  • Introduce @heyputer/puter.js as a runtime dependency for AI text-to-speech integration
  • Reorder devDependencies in package.json for consistency and regenerate package-lock.json to reflect new dependency graph
package.json
package-lock.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Harshjeet, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 46 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 86679289-ce85-496c-864f-43b8c4541da9

📥 Commits

Reviewing files that changed from the base of the PR and between aedaa2d and 16046cf.

📒 Files selected for processing (5)
  • docs/.vitepress/theme/components/ReadAloud.vue
  • docs/.vitepress/theme/composables/useArticleText.ts
  • docs/.vitepress/theme/composables/useBrowserTTS.ts
  • docs/.vitepress/theme/composables/usePuterTTS.ts
  • docs/.vitepress/theme/composables/useReadAloudEngine.ts
📝 Walkthrough

Walkthrough

Adds a VitePress ReadAloud component that extracts article text, chunks it, and plays it through Puter AI or browser speech synthesis. The theme renders and registers the component, while Mermaid rendering responds to dark-mode changes.

Changes

Read-aloud feature

Layer / File(s) Summary
Article text and voice preparation
docs/.vitepress/theme/components/ReadAloud.vue
Defines playback state, extracts and chunks .vp-doc text, and loads and prioritizes browser voices.
Audio playback engines
docs/.vitepress/theme/components/ReadAloud.vue, package.json
Adds Puter audio prefetching and sequential playback, browser speech synthesis playback, and the Puter SDK dependency.
Playback controls and component UI
docs/.vitepress/theme/components/ReadAloud.vue
Wires play, pause, stop, rate, engine, voice, route, and lifecycle behavior to the rendered controls and responsive styles.
VitePress theme integration
docs/.vitepress/theme/index.ts
Registers ReadAloud, renders it in the doc-before slot, and rerenders Mermaid diagrams when the theme changes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested reviewers: programmersd21

Sequence Diagram(s)

sequenceDiagram
  participant ReadAloud
  participant ArticleDOM
  participant PuterSDK
  participant AudioPlayback
  ReadAloud->>ArticleDOM: Extract and chunk article text
  ReadAloud->>PuterSDK: Generate audio for chunks
  PuterSDK->>AudioPlayback: Provide chunk audio
  AudioPlayback->>ReadAloud: Signal chunk completion
  ReadAloud->>AudioPlayback: Advance to the next chunk
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a ReadAloud component with Puter AI and Web Speech API support.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • The ReadAloud.vue file is quite large and mixes DOM scraping, TTS engine orchestration, caching, and UI; consider splitting it into smaller composables (e.g. useReadAloudEngine, usePuterTTS, useBrowserTTS, useArticleText) to keep concerns separated and make it easier to maintain.
  • In onMounted you attach window.speechSynthesis.onvoiceschanged = loadBrowserVoices but never clear it, which can lead to stale callbacks if the component is mounted/unmounted multiple times; consider resetting this handler in onUnmounted.
  • The isSupported flag disables the entire component when speechSynthesis is not available, even though the Puter AI TTS mode does not depend on it; you may want to decouple support detection per engine so Puter TTS remains available in browsers without Web Speech API.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The ReadAloud.vue file is quite large and mixes DOM scraping, TTS engine orchestration, caching, and UI; consider splitting it into smaller composables (e.g. `useReadAloudEngine`, `usePuterTTS`, `useBrowserTTS`, `useArticleText`) to keep concerns separated and make it easier to maintain.
- In `onMounted` you attach `window.speechSynthesis.onvoiceschanged = loadBrowserVoices` but never clear it, which can lead to stale callbacks if the component is mounted/unmounted multiple times; consider resetting this handler in `onUnmounted`.
- The `isSupported` flag disables the entire component when `speechSynthesis` is not available, even though the Puter AI TTS mode does not depend on it; you may want to decouple support detection per engine so Puter TTS remains available in browsers without Web Speech API.

## Individual Comments

### Comment 1
<location path="docs/.vitepress/theme/components/ReadAloud.vue" line_range="146-148" />
<code_context>
+    return Promise.resolve(null)
+  }
+
+  const cacheKey = `${selectedPuterVoice.value}:${chunkIndex}:${chunks.value[chunkIndex]}`
+
+  if (audioCache.has(cacheKey)) {
+    return Promise.resolve(audioCache.get(cacheKey)!)
+  }
</code_context>
<issue_to_address>
**suggestion (performance):** Audio cache keys include the full chunk text, which is unnecessarily large and may increase memory usage.

The key `${voice}:${idx}:${chunks[idx]}` embeds the full chunk text, which can be very large for long articles and duplicates data in memory. Because voice + chunk index already uniquely identify an entry for a given document load, consider using a shorter key like `${voice}:${idx}` to reduce memory usage and avoid repeatedly hashing large strings.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread docs/.vitepress/theme/components/ReadAloud.vue Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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 `@docs/.vitepress/theme/components/ReadAloud.vue`:
- Around line 384-390: Update onVoiceChange to capture the current chunk index
before stopSpeech resets it, then restore that position after the article is
re-extracted and re-chunked, clamping it to chunks.value.length - 1 when
boundaries or length change. Preserve the existing voice-switch restart flow
without forcing playback to begin at chunk 0.
- Around line 51-68: Introduce a playback-session token in the ReadAloud
component and advance it in stopSpeech and whenever a new playback session
starts. Capture the token and relevant chunk context before
speakCurrentPuterChunk awaits fetchChunkAudio(idx), then verify the token and
current playback state after every asynchronous continuation before assigning
currentAudio, wiring handlers, or calling play; stale continuations must exit
without affecting the new session’s chunks or currentChunkIndex.
- Line 16: Update the ttsMode initialization in ReadAloud to default to
'browser' instead of 'puter', keeping Puter available only through an explicit
user choice or opt-in path.
🪄 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 Plus

Run ID: f3f99335-97be-4cd3-b8c9-41075b45d57c

📥 Commits

Reviewing files that changed from the base of the PR and between eb3c44f and aedaa2d.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (3)
  • docs/.vitepress/theme/components/ReadAloud.vue
  • docs/.vitepress/theme/index.ts
  • package.json

Comment thread docs/.vitepress/theme/components/ReadAloud.vue Outdated
Comment on lines +51 to +68
const stopSpeech = () => {
isPlaying.value = false
isPaused.value = false
isLoadingModel.value = false
loadingProgress.value = ''
currentChunkIndex.value = 0
chunks.value = []

if (currentAudio) {
currentAudio.pause()
currentAudio = null
}

if (typeof window !== 'undefined' && 'speechSynthesis' in window) {
window.speechSynthesis.cancel()
}
currentUtterance = null
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Stale async continuation can hijack playback after stop/restart or fast route navigation.

speakCurrentPuterChunk awaits fetchChunkAudio(idx) and, on resume, only checks !isPlaying.value before assigning currentAudio, wiring onended/onerror, and calling .play(). isPlaying is a plain boolean, not scoped to a playback session. If a user clicks Stop then Listen again (or navigates to a new page and clicks Listen) while an older fetchChunkAudio promise is still resolving, that stale continuation resumes with isPlaying.value === true (now true again for the new session) and plays the old chunk's audio, attaching handlers that increment currentChunkIndex against the new chunks array — causing overlapping audio, skipped/duplicated chunks, or playing stale content from a previous page.

🔒️ Suggested fix: guard continuations with a session token
+let sessionId = 0
+
 const stopSpeech = () => {
+  sessionId++
   isPlaying.value = false
   ...
 }
 
 const speakCurrentPuterChunk = async () => {
+  const mySession = sessionId
   ...
   const audio = await fetchChunkAudio(idx)
 
-  if (!audio || !isPlaying.value) {
+  if (!audio || !isPlaying.value || mySession !== sessionId) {
     ...
   }

Also applies to: 181-252, 311-363

🤖 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 `@docs/.vitepress/theme/components/ReadAloud.vue` around lines 51 - 68,
Introduce a playback-session token in the ReadAloud component and advance it in
stopSpeech and whenever a new playback session starts. Capture the token and
relevant chunk context before speakCurrentPuterChunk awaits
fetchChunkAudio(idx), then verify the token and current playback state after
every asynchronous continuation before assigning currentAudio, wiring handlers,
or calling play; stale continuations must exit without affecting the new
session’s chunks or currentChunkIndex.

Comment on lines +384 to +390
const onVoiceChange = () => {
clearAudioCache()
if (isPlaying.value || isPaused.value) {
stopSpeech()
setTimeout(() => togglePlayPause(), 50)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Changing the voice restarts the entire article instead of resuming at the current position.

onVoiceChange calls stopSpeech() — which resets currentChunkIndex.value = 0 and clears chunks.value — then togglePlayPause(), which re-extracts and re-chunks the article and starts from chunk 0. Any reader who switches voice mid-article loses their place and must relisten from the beginning.

Capture the current chunk index before stopping and clamp/restore it after re-chunking (chunk boundaries may shift slightly, so clamp to chunks.value.length - 1) instead of forcing a full restart.

🤖 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 `@docs/.vitepress/theme/components/ReadAloud.vue` around lines 384 - 390,
Update onVoiceChange to capture the current chunk index before stopSpeech resets
it, then restore that position after the article is re-extracted and re-chunked,
clamping it to chunks.value.length - 1 when boundaries or length change.
Preserve the existing voice-switch restart flow without forcing playback to
begin at chunk 0.

@programmersd21
programmersd21 merged commit e0bb8a1 into programmersd21:master Jul 22, 2026
4 checks passed
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.

2 participants