diff --git a/App.tsx b/App.tsx
index 7b117cf..23e77d4 100644
--- a/App.tsx
+++ b/App.tsx
@@ -91,6 +91,8 @@ export default function App() {
const segmentsToBuffer = article.segments.slice(currentIndex, currentIndex + 5);
for (const seg of segmentsToBuffer) {
+ // Only process if we don't have audio URL, and it's not currently loading.
+ // This handles cases where we cleared the URL to force regeneration.
if (!seg.audioUrl && !seg.isLoading && !seg.hasError) {
processSegmentAudio(article.id, seg.id, seg.text, playerState.selectedVoice);
}
@@ -99,6 +101,38 @@ export default function App() {
// -- Handlers --
+ const handleVoiceChange = useCallback((newVoice: VoiceName) => {
+ setPlayerState(prev => ({ ...prev, selectedVoice: newVoice }));
+
+ // Force flush future buffer so new voice is applied immediately
+ setQueue(prevQueue => prevQueue.map(article => {
+ // If this is the currently active article
+ if (article.id === playerState.currentArticleId) {
+ return {
+ ...article,
+ segments: article.segments.map((seg, idx) => {
+ // Keep the current segment (and past ones) to avoid cutting off mid-speech abruptly
+ if (idx <= article.currentSegmentIndex) {
+ return seg;
+ }
+ // Invalidate all future segments
+ return { ...seg, audioUrl: undefined, isLoading: false, hasError: false };
+ })
+ };
+ }
+ // For inactive articles, invalidate everything
+ return {
+ ...article,
+ segments: article.segments.map(seg => ({
+ ...seg,
+ audioUrl: undefined,
+ isLoading: false,
+ hasError: false
+ }))
+ };
+ }));
+ }, [playerState.currentArticleId]);
+
const handleAddUrl = async () => {
if (!inputUrl.trim()) return;
@@ -373,8 +407,8 @@ export default function App() {
setPlayerState(prev => ({ ...prev, selectedVoice: v }))}
- disabled={playerState.isPlaying}
+ onVoiceChange={handleVoiceChange}
+ // Removed disabled prop to allow switching while playing
/>