diff --git a/components/AudioControls.tsx b/components/AudioControls.tsx index 13be6a8..c2092f1 100644 --- a/components/AudioControls.tsx +++ b/components/AudioControls.tsx @@ -8,19 +8,17 @@ interface AudioControlsProps { } export default function AudioControls({ isPlaying, isLoading, isSynthesizingAudio, handlePlayPause }: AudioControlsProps) { - const [isPaused, setIsPaused] = useState(false); const [buttonState, setButtonState] = useState<'play' | 'pause' | 'resume' | 'loading'>('play'); useEffect(() => { - console.log('isLoading:', isLoading, 'isSynthesizingAudio:', isSynthesizingAudio, 'isPlaying:', isPlaying); if (isLoading || isSynthesizingAudio) { setButtonState('loading'); - } else if (isPlaying && !isPaused) { + } else if (isPlaying) { setButtonState('pause'); } else { setButtonState(buttonState === 'pause' ? 'resume' : 'play'); } - }, [isPlaying, isLoading, isSynthesizingAudio, isPaused]); + }, [isPlaying, isLoading, isSynthesizingAudio]); const getButtonText = () => { switch (buttonState) { @@ -32,10 +30,7 @@ export default function AudioControls({ isPlaying, isLoading, isSynthesizingAudi }; const handleClick = () => { - if (isPlaying || isPaused) { - setIsPaused(!isPaused); - } - handlePlayPause(); + handlePlayPause(); }; return ( diff --git a/hooks/useAudioControls.ts b/hooks/useAudioControls.ts index 700e1d2..8e10ee1 100644 --- a/hooks/useAudioControls.ts +++ b/hooks/useAudioControls.ts @@ -1,50 +1,54 @@ -import { - hasAudioStarted, - isAudioLoading, - isPlaying, - pauseResumeAudio, - playFullDebate, - resetAudioState, - isSynthesizing -} from 'lib/audioUtils.ts'; -import type { Personality } from 'lib/debate/personalities.ts'; -import { useCallback, useEffect, useState } from 'preact/hooks'; +import { pauseResumeAudio, playFullDebate, processQueue } from "lib/audioUtils.ts"; +import { Personality } from "lib/debate/personalities.ts"; +import { useCallback, useEffect } from "preact/hooks"; +import { useAudioState } from "./useAudioState.ts"; -export function useAudioControls(debate: Array<{ role: string; content: string }>, agentDetails: Required[]) { - const [isLoading, setIsLoading] = useState(false); - const [isSynthesizingAudio, setIsSynthesizingAudio] = useState(false); +export const useAudioControls = ( + debate: Array<{ role: string; content: string }>, + agentDetails: Required[] +) => { + const audioState = useAudioState(); + const { + isProcessingQueue, + isPaused, + isLoading, + isSynthesizingAudio, + audioQueue, + currentQueueIndex, + resetAudioState, + setIsPaused, + setIsProcessingQueue, + } = audioState; + + const isPlaying = isProcessingQueue && !isPaused; useEffect(() => { - const checkStatus = () => { - setIsLoading(isAudioLoading()); - setIsSynthesizingAudio(isSynthesizing()); + return () => { + resetAudioState(); }; - - const intervalId = setInterval(checkStatus, 100); - return () => clearInterval(intervalId); }, []); - const handlePlayPause = useCallback(async () => { - if (isPlaying()) { - pauseResumeAudio(); + useEffect(() => { + if (audioQueue.length > 0 && !isProcessingQueue && !isPaused) { + processQueue(audioState); + } + }, [audioQueue, isProcessingQueue, isPaused]); + + const handlePlayPause = useCallback(() => { + if (audioQueue.length === 0 && currentQueueIndex === 0) { + playFullDebate(debate, agentDetails, audioState).then(() => { + setIsProcessingQueue(true); + processQueue(audioState); + }); } else { - setIsLoading(true); - setIsSynthesizingAudio(true); - if (!hasAudioStarted()) { - resetAudioState(); - await playFullDebate(debate, agentDetails); - } else { - pauseResumeAudio(); - } - setIsLoading(false); - setIsSynthesizingAudio(false); + pauseResumeAudio(audioState); } - }, [debate, agentDetails]); + }, [debate, agentDetails, audioState, audioQueue, currentQueueIndex]); return { - isPlaying: isPlaying(), + isPlaying, isLoading, isSynthesizingAudio, handlePlayPause, }; -} +}; diff --git a/hooks/useAudioState.ts b/hooks/useAudioState.ts new file mode 100644 index 0000000..be36709 --- /dev/null +++ b/hooks/useAudioState.ts @@ -0,0 +1,42 @@ +import { useState } from "preact/hooks"; + +export const useAudioState = () => { + const [audioQueue, setAudioQueue] = useState>([]); + const [isProcessingQueue, setIsProcessingQueue] = useState(false); + const [currentAudio, setCurrentAudio] = useState(null); + const [isPaused, setIsPaused] = useState(false); + const [currentPlaybackPosition, setCurrentPlaybackPosition] = useState(0); + const [isLoading, setIsLoading] = useState(false); + const [currentQueueIndex, setCurrentQueueIndex] = useState(0); + const [isSynthesizingAudio, setIsSynthesizingAudio] = useState(false); + + const resetAudioState = () => { + setAudioQueue([]); + setIsProcessingQueue(false); + setCurrentAudio(null); + setIsPaused(false); + setCurrentPlaybackPosition(0); + setCurrentQueueIndex(0); + setIsLoading(false); + }; + + return { + audioQueue, + setAudioQueue, + isProcessingQueue, + setIsProcessingQueue, + currentAudio, + setCurrentAudio, + isPaused, + setIsPaused, + currentPlaybackPosition, + setCurrentPlaybackPosition, + isLoading, + setIsLoading, + currentQueueIndex, + setCurrentQueueIndex, + isSynthesizingAudio, + setIsSynthesizingAudio, + resetAudioState, + }; +}; diff --git a/hooks/useDebateState.ts b/hooks/useDebateState.ts index 9adfb9c..d401a38 100644 --- a/hooks/useDebateState.ts +++ b/hooks/useDebateState.ts @@ -177,8 +177,7 @@ export function useDebateState() { setIsDebateFinished(true); } catch (error) { if (error.name === "AbortError") { - console.log("Fetch aborted"); - } else { + } else { console.error("Error in debate:", error); setErrors(["An error occurred while debating. Please try again."]); } diff --git a/islands/DebateForm.tsx b/islands/DebateForm.tsx index 7a4ba0a..377edc1 100644 --- a/islands/DebateForm.tsx +++ b/islands/DebateForm.tsx @@ -28,11 +28,11 @@ export default function DebateForm() { } = useDebateState(); const { - isPlaying, - isLoading: isAudioLoading, - isSynthesizingAudio, - handlePlayPause, -} = useAudioControls(debate, agentDetails); + isPlaying, + isLoading: isAudioLoading, + isSynthesizingAudio, + handlePlayPause, + } = useAudioControls(debate, agentDetails); return (
diff --git a/lib/audioUtils.ts b/lib/audioUtils.ts index b0968f6..86d2ab6 100644 --- a/lib/audioUtils.ts +++ b/lib/audioUtils.ts @@ -1,25 +1,16 @@ +import { useAudioState } from "hooks/useAudioState.ts"; import { Personality } from "lib/debate/personalities.ts"; import { VoiceType, isValidVoice } from "routes/api/voicesynth.tsx"; -let audioQueue: Array<{ content: string; voice: string }> = []; -let isProcessingQueue = false; -let currentAudio: HTMLAudioElement | null = null; -let isPaused = false; -let currentPlaybackPosition = 0; -let isLoading = false; -let currentQueueIndex = 0; - -let isSynthesizingAudio = false; - -export const isSynthesizing = (): boolean => { - return isSynthesizingAudio; -}; - -export const handleAudioSynthesis = async (content: string, voice: string): Promise => { - isLoading = true; - isSynthesizingAudio = true; - console.log(`Synthesizing audio for voice: ${voice}`); - try { +export const handleAudioSynthesis = async ( + content: string, + voice: string, + setIsLoading: (value: boolean) => void, + setIsSynthesizingAudio: (value: boolean) => void +): Promise => { + setIsLoading(true); + setIsSynthesizingAudio(true); + try { const response = await fetch("/api/voicesynth", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -36,121 +27,127 @@ export const handleAudioSynthesis = async (content: string, voice: string): Prom await new Promise((resolve, reject) => { audio.addEventListener('loadedmetadata', () => { - console.log(`Audio synthesized for ${voice}, length:`, audio.duration); - resolve(audio); + resolve(audio); }); audio.addEventListener('error', (e) => reject(new Error(`Audio failed to load: ${e.message}`))); }); - isLoading = false; - isSynthesizingAudio = false; + setIsLoading(false); + setIsSynthesizingAudio(false); return audio; } catch (error) { - isLoading = false; - isSynthesizingAudio = false; + setIsLoading(false); + setIsSynthesizingAudio(false); console.error("Error in handleAudioSynthesis:", error); throw error; } }; +export const processQueue = async (audioState: ReturnType) => { + const { + audioQueue, + isProcessingQueue, + isPaused, + currentQueueIndex, + setIsProcessingQueue, + setCurrentAudio, + setCurrentPlaybackPosition, + setCurrentQueueIndex, + setIsPaused, + } = audioState; - -const processQueue = async () => { if (isProcessingQueue || audioQueue.length === 0) { return; } - isProcessingQueue = true; + setIsProcessingQueue(true); for (let i = currentQueueIndex; i < audioQueue.length; i++) { - if (isPaused) break; + if (isPaused) { + setIsProcessingQueue(false); + break; + } const { content, voice } = audioQueue[i]; try { - const audio = await handleAudioSynthesis(content, voice); - if (isPaused) break; - currentAudio = audio; - await playAudio(audio); - if (isPaused) break; - currentAudio = null; - currentPlaybackPosition = 0; - currentQueueIndex = i + 1; + const audio = await handleAudioSynthesis(content, voice, audioState.setIsLoading, audioState.setIsSynthesizingAudio); + if (isPaused) { + setIsProcessingQueue(false); + break; + } + setCurrentAudio(audio); + await playAudio(audio, audioState); + if (isPaused) { + setIsProcessingQueue(false); + break; + } + setCurrentAudio(null); + setCurrentPlaybackPosition(0); + setCurrentQueueIndex(i + 1); } catch (error) { console.error("Error processing audio queue:", error); } } - isProcessingQueue = false; + if (!isPaused) { + setIsProcessingQueue(false); + setIsPaused(true); + } }; -const playAudio = (audio: HTMLAudioElement): Promise => { +const playAudio = (audio: HTMLAudioElement, audioState: ReturnType): Promise => { return new Promise((resolve) => { audio.addEventListener('ended', () => resolve(), { once: true }); - audio.currentTime = currentPlaybackPosition; + audio.currentTime = audioState.currentPlaybackPosition; audio.play().catch(console.error); }); }; -export const pauseResumeAudio = () => { +export const pauseResumeAudio = (audioState: ReturnType) => { + const { currentAudio, isPaused, audioQueue, setIsPaused, setCurrentPlaybackPosition } = audioState; + if (currentAudio) { if (isPaused) { currentAudio.play().catch(console.error); } else { currentAudio.pause(); - currentPlaybackPosition = currentAudio.currentTime; + setCurrentPlaybackPosition(currentAudio.currentTime); } - isPaused = !isPaused; + setIsPaused(!isPaused); } else if (isPaused) { - isPaused = false; - processQueue(); + setIsPaused(false); + processQueue(audioState); } else if (audioQueue.length > 0) { - isPaused = true; + setIsPaused(true); } }; -export const isPlaying = (): boolean => { - return isProcessingQueue && !isPaused; -}; - -export const isAudioLoading = (): boolean => { - return isLoading; -}; - -export const hasAudioStarted = (): boolean => { - return audioQueue.length > 0 || currentQueueIndex > 0; -}; - export const playFullDebate = async ( debate: Array<{ role: string; content: string }>, agentDetails: Required[], + audioState: ReturnType ) => { - isPaused = false; - currentPlaybackPosition = 0; - currentQueueIndex = 0; - audioQueue = []; - isLoading = true; - isSynthesizingAudio = true; - debate.forEach((message) => { + const { setIsPaused, setCurrentPlaybackPosition, setCurrentQueueIndex, setAudioQueue, setIsLoading, setIsSynthesizingAudio } = audioState; + + setIsPaused(false); + setCurrentPlaybackPosition(0); + setCurrentQueueIndex(0); + setIsLoading(true); + setIsSynthesizingAudio(true); + + const newAudioQueue = debate.reduce((queue, message) => { if (message.role !== "user" && message.role !== "system") { const voice: VoiceType = agentDetails.find((agent) => agent.name === message.role)?.voice || "nova"; if (isValidVoice(voice)) { - audioQueue.push({ content: message.content, voice }); + queue.push({ content: message.content, voice }); } else { console.error(`Invalid voice type: ${voice}`); } } - }); - isLoading = false; - isSynthesizingAudio = false; - await processQueue(); -}; + return queue; + }, [] as Array<{ content: string; voice: string }>); -export const resetAudioState = () => { - audioQueue = []; - isProcessingQueue = false; - currentAudio = null; - isPaused = false; - currentPlaybackPosition = 0; - currentQueueIndex = 0; - isLoading = false; + setAudioQueue(newAudioQueue); + setIsLoading(false); + setIsSynthesizingAudio(false); };