"use client"; import Button from "@/components/Button"; import IconClick from "@/components/IconClick"; import IMAGES from "@/config/images"; import { useAudioPlayer } from "@/hooks/useAudioPlayer"; import { getTTSAudioUrl } from "@/utils"; import { ChangeEvent, useEffect, useRef, useState } from "react"; import z from "zod"; export default function Home() { const [showSpeedAdjust, setShowSpeedAdjust] = useState(false); const [ipaEnabled, setIPAEnabled] = useState(false); const [speed, setSpeed] = useState(1); const [pause, setPause] = useState(true); const [autopause, setAutopause] = useState(true); const textRef = useRef(''); // const localeRef = useRef(null); const [locale, setLocale] = useState(null); const [ipa, setIPA] = useState(''); const objurlRef = useRef(null); const [processing, setProcessing] = useState(false); const [voicesData, setVoicesData] = useState<{ locale: string, short_name: string }[] | null>(null); const [loading, setLoading] = useState(true); const { playAudio, stopAudio, audioRef } = useAudioPlayer(); useEffect(() => { fetch('/list_of_voices.json') .then(res => res.json()) .then(setVoicesData) .catch(() => setVoicesData(null)) .finally(() => setLoading(false)); }, []); useEffect(() => { const audio = audioRef.current; if (!audio) return; const handleEnded = () => { if (autopause) { setPause(true); } else { playAudio(objurlRef.current!); } } audio.addEventListener('ended', handleEnded); return () => { audio.removeEventListener('ended', handleEnded); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [audioRef, autopause]); if (loading) return
加载中...
; if (!voicesData) return
加载失败
; const speak = async () => { if (processing) return; setProcessing(true); if (ipa.length === 0 && ipaEnabled && textRef.current.length !== 0) { const params = new URLSearchParams({ text: textRef.current }); fetch(`/api/ipa?${params}`) .then(res => res.json()) .then(data => { setIPA(data.ipa); }).catch(e => { console.error(e); setIPA(''); }) } if (pause) { // 如果没在读 if (textRef.current.length === 0) { // 没文本咋读 } else { setPause(false); if (objurlRef.current) { // 之前有播放 playAudio(objurlRef.current); } else { // 第一次播放 console.log('downloading text info'); const params = new URLSearchParams({ text: textRef.current.slice(0, 30) }); try { const textinfo = await (await fetch(`/api/locale?${params}`)).json(); setLocale(textinfo.locale); const voice = voicesData.find(v => v.locale.startsWith(textinfo.locale)); if (!voice) throw 'Voice not found.'; objurlRef.current = await getTTSAudioUrl( textRef.current, voice.short_name, (() => { if (speed === 1) return {}; else if (speed < 1) return { rate: `-${100 - speed * 100}%` }; else return { rate: `+${speed * 100 - 100}%` }; })() ); playAudio(objurlRef.current); } catch (e) { console.error(e); setPause(true); setLocale(null); setProcessing(false); } } } } else { // 如果在读就暂停 setPause(true); stopAudio(); } setProcessing(false); } const handleInputChange = (e: ChangeEvent) => { textRef.current = e.target.value.trim(); setLocale(null); setIPA(''); if (objurlRef.current) URL.revokeObjectURL(objurlRef.current); objurlRef.current = null; stopAudio(); setPause(true); } const letMeSetSpeed = (new_speed: number) => { return () => { setSpeed(new_speed); if (objurlRef.current) URL.revokeObjectURL(objurlRef.current); objurlRef.current = null; stopAudio(); setPause(true); } } const TextSpeakerItemSchema = z.object({ text: z.string(), ipa: z.string().optional(), locale: z.string() }); const TextSpeakerArraySchema = z.array(TextSpeakerItemSchema); const save = () => { if (!locale) return; const getTextSpeakerData = () => { try { const item = localStorage.getItem('text-speaker'); if (!item) return []; const rawData = JSON.parse(item); const result = TextSpeakerArraySchema.safeParse(rawData); if (result.success) { return result.data; } else { console.error('Invalid data structure in localStorage:', result.error); return []; } } catch (e) { console.error('Failed to parse text-speaker data:', e); return []; } } const save = getTextSpeakerData(); const oldIndex = save.findIndex(v => v.text === textRef.current); if (oldIndex !== -1) { const oldItem = save[oldIndex]; if ((ipa && !oldItem.ipa) || (ipa && oldItem.ipa !== ipa)) { oldItem.ipa = ipa; localStorage.setItem('text-speaker', JSON.stringify(save)); return; } } if (ipa.length === 0) { save.push({ text: textRef.current, locale: locale }); } else { save.push({ text: textRef.current, locale: locale, ipa: ipa }); } localStorage.setItem('text-speaker', JSON.stringify(save)); } return (<>
{ipa}
{ setAutopause(!autopause); if (objurlRef) { stopAudio(); } setPause(true); }} src={ autopause ? IMAGES.autoplay : IMAGES.autopause } alt="autoplayorpause" > {localStorage.getItem('text-speaker')} setShowSpeedAdjust(!showSpeedAdjust)} src={IMAGES.more_horiz} alt="more"> {locale ? () : (<>)}
{ showSpeedAdjust ? (<> ) : <> }
); }