feat: add ReadAloud component with Puter AI and Web Speech API support#2
Conversation
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
Reviewer's GuideAdds 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 SpeechsequenceDiagram
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
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Warning Review limit reached
Next review available in: 46 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (5)
📝 WalkthroughWalkthroughAdds a VitePress ChangesRead-aloud feature
Estimated code review effort: 4 (Complex) | ~45 minutes Suggested reviewers: 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
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 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 |
There was a problem hiding this comment.
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
onMountedyou attachwindow.speechSynthesis.onvoiceschanged = loadBrowserVoicesbut never clear it, which can lead to stale callbacks if the component is mounted/unmounted multiple times; consider resetting this handler inonUnmounted. - The
isSupportedflag disables the entire component whenspeechSynthesisis 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>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
…rt via new composables
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (3)
docs/.vitepress/theme/components/ReadAloud.vuedocs/.vitepress/theme/index.tspackage.json
| 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 | ||
| } |
There was a problem hiding this comment.
🩺 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.
| const onVoiceChange = () => { | ||
| clearAudioCache() | ||
| if (isPlaying.value || isPaused.value) { | ||
| stopSpeech() | ||
| setTimeout(() => togglePlayPause(), 50) | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 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.
… read-aloud engine
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:
Enhancements:
Build:
Summary by CodeRabbit