"use client"; import { createClient } from "@/utils/supabase/client"; import React, { createContext, useContext, useState, useEffect, useRef, ReactNode, } from "react"; import removeMarkdown from "remove-markdown"; import { toast } from "sonner"; // More robust sentence splitter using Intl.Segmenter for better accuracy. function splitIntoSentences(text: string): string[] { if (typeof Intl !== "undefined" && Intl.Segmenter) { const segmenter = new Intl.Segmenter("en", { granularity: "sentence" }); return Array.from(segmenter.segment(text)).map( (segment) => segment.segment ); } // Fallback to regex-based splitting if Intl.Segmenter is unavailable. return text.match(/[^\.!\?]+[\.!\?]+/g) || [text]; } interface TTSContextType { sentences: string[]; currentSentence: number; voices: any[]; selectedSpeaker: string; status: "ready" | "running" | null; setSelectedSpeaker: (speaker: string) => void; setCurrentSentence: (index: number) => void; playSentence: (index: number) => void; skipToSentence: (index: number) => void; playInOrder: (index: number) => void; pause: () => void; resume: () => void; stop: () => void; } const TTSContext = createContext(undefined); export const TTSProvider = ({ pages, children, }: { pages: string[]; children: ReactNode; }) => { const supabase = createClient(); // Combine pages and split into sentences. const fullText = pages.join("\n"); const sentences = splitIntoSentences(fullText).filter( (sentence) => sentence.trim() !== "\\n" && sentence.trim() !== "" ); const [currentSentence, setCurrentSentence] = useState(0); const audioRef = useRef(null); const [selectedSpeaker, setSelectedSpeaker] = useState("af_heart"); const [voices, setVoices] = useState([]); const [status, setStatus] = useState<"ready" | "running" | null>("ready"); async function generateTTS(sentence: string, index: number): Promise { try { const { data, error } = await supabase.functions.invoke("generate-tts", { body: { text: sentence, voice: selectedSpeaker, index, }, }); setStatus("running"); const { audioUrl } = data as { audioUrl: string }; return audioUrl; } catch (error) { console.error("Error generating TTS:", error); toast.error("Failed to generate TTS. Please try again."); throw error; } } const playSentence = async (index: number) => { setCurrentSentence(index); const sentence = removeMarkdown(sentences[index]); try { const audioUrl = await generateTTS(sentence, index); if (audioRef.current) { audioRef.current.src = audioUrl; await new Promise((res) => { audioRef.current!.play(); audioRef.current!.onended = () => res(true); }); } } catch (error) { console.error("Error playing sentence:", error); } }; const skipToSentence = (index: number) => { if (index < 0 || index >= sentences.length) return; playSentence(index); }; const playInOrder = async (index: number) => { if (index < 0 || index >= sentences.length) return; setCurrentSentence(index); for (let i = index; i < sentences.length; i++) { console.log("Playing sentence:", i, sentences[i]); try { await playSentence(i); } catch (error) { console.error("Error playing sentence:", error); break; // Stop playback on error } } }; const pause = () => { if (audioRef.current) { audioRef.current.pause(); } }; const resume = () => { if (audioRef.current) { audioRef.current.play(); } }; const stop = () => { if (audioRef.current) { audioRef.current.pause(); audioRef.current.currentTime = 0; } }; const value: TTSContextType = { sentences, currentSentence, voices, playSentence, skipToSentence, selectedSpeaker, setSelectedSpeaker, setCurrentSentence, playInOrder, pause, resume, stop, status, }; return ( {children} ); }; export const useTTS = (): TTSContextType => { const context = useContext(TTSContext); if (!context) { throw new Error("useTTS must be used within a TTSProvider"); } return context; };