"use client"; import { LightButton } from "@/components/ui/buttons"; import { IconClick } from "@/components/ui/buttons"; import IMAGES from "@/config/images"; import { useAudioPlayer } from "@/hooks/useAudioPlayer"; import { TextSpeakerArraySchema, TextSpeakerItemSchema, } from "@/lib/interfaces"; import { ChangeEvent, useEffect, useRef, useState } from "react"; import z from "zod"; import SaveList from "./SaveList"; import { useTranslations } from "next-intl"; import { getLocalStorageOperator } from "@/lib/browser/localStorageOperators"; import { genIPA, genLanguage } from "@/modules/translator/translator-action"; import { logger } from "@/lib/logger"; import PageLayout from "@/components/ui/PageLayout"; import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/bigmodel/tts"; export default function TextSpeakerPage() { const t = useTranslations("text_speaker"); const textareaRef = useRef(null); const [showSpeedAdjust, setShowSpeedAdjust] = useState(false); const [showSaveList, setShowSaveList] = useState(false); const [saving, setSaving] = 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 [language, setLanguage] = useState(null); const [ipa, setIPA] = useState(""); const objurlRef = useRef(null); const [processing, setProcessing] = useState(false); const { play, stop, load, audioRef } = useAudioPlayer(); const { get: getFromLocalStorage, set: setIntoLocalStorage } = getLocalStorageOperator( "text-speaker", TextSpeakerArraySchema, ); useEffect(() => { const audio = audioRef.current; if (!audio) return; const handleEnded = () => { if (autopause) { setPause(true); } else { load(objurlRef.current!); play(); } }; audio.addEventListener("ended", handleEnded); return () => { audio.removeEventListener("ended", handleEnded); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [audioRef, autopause]); 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) => { logger.error("生成 IPA 失败", e); setIPA(""); }); } if (pause) { // 如果没在读 if (textRef.current.length === 0) { // 没文本咋读 } else { setPause(false); if (objurlRef.current) { // 之前有播放 load(objurlRef.current); play(); } else { // 第一次播放 try { let theLanguage = language; if (!theLanguage) { const tmp_language = await genLanguage(textRef.current.slice(0, 30)); setLanguage(tmp_language); theLanguage = tmp_language; } theLanguage = theLanguage.toLowerCase().replace(/[^a-z]/g, '').replace(/^./, match => match.toUpperCase()); // 检查语言是否在 TTS 支持列表中 const supportedLanguages: TTS_SUPPORTED_LANGUAGES[] = [ "Auto", "Chinese", "English", "German", "Italian", "Portuguese", "Spanish", "Japanese", "Korean", "French", "Russian" ]; if (!supportedLanguages.includes(theLanguage as TTS_SUPPORTED_LANGUAGES)) { theLanguage = "Auto"; } objurlRef.current = await getTTSUrl( textRef.current, theLanguage as TTS_SUPPORTED_LANGUAGES ); load(objurlRef.current); play(); } catch (e) { logger.error("播放音频失败", e); setPause(true); setLanguage(null); setProcessing(false); } } } } else { // 如果在读就暂停 setPause(true); stop(); } setProcessing(false); }; const handleInputChange = (e: ChangeEvent) => { textRef.current = e.target.value.trim(); setLanguage(null); setIPA(""); if (objurlRef.current) URL.revokeObjectURL(objurlRef.current); objurlRef.current = null; stop(); setPause(true); }; const letMeSetSpeed = (new_speed: number) => { return () => { setSpeed(new_speed); if (objurlRef.current) URL.revokeObjectURL(objurlRef.current); objurlRef.current = null; stop(); setPause(true); }; }; const handleUseItem = (item: z.infer) => { if (textareaRef.current) textareaRef.current.value = item.text; textRef.current = item.text; setLanguage(item.language); setIPA(item.ipa || ""); if (objurlRef.current) URL.revokeObjectURL(objurlRef.current); objurlRef.current = null; stop(); setPause(true); }; const save = async () => { if (saving) return; if (textRef.current.length === 0) return; setSaving(true); try { let theLanguage = language; if (!theLanguage) { const tmp_language = await genLanguage(textRef.current.slice(0, 30)); setLanguage(tmp_language); theLanguage = tmp_language; } let theIPA = ipa; if (ipa.length === 0 && ipaEnabled) { const tmp_ipa = await genIPA(textRef.current); setIPA(tmp_ipa); theIPA = tmp_ipa; } const save = getFromLocalStorage(); const oldIndex = save.findIndex((v) => v.text === textRef.current); if (oldIndex !== -1) { const oldItem = save[oldIndex]; if (theIPA) { if (!oldItem.ipa || oldItem.ipa !== theIPA) { oldItem.ipa = theIPA; setIntoLocalStorage(save); } } } else if (theIPA.length === 0) { save.push({ text: textRef.current, language: theLanguage as string, }); } else { save.push({ text: textRef.current, language: theLanguage as string, ipa: theIPA, }); } setIntoLocalStorage(save); } catch (e) { logger.error("保存到本地存储失败", e); setLanguage(null); } finally { setSaving(false); } }; return ( {/* 文本输入区域 */}
{/* 文本输入框 */} {/* IPA 显示区域 */} {(ipa.length !== 0 && (
{ipa}
)) ||
} {/* 控制按钮区域 */}
{/* 速度调节面板 */} {showSpeedAdjust && (
)} {/* 播放/暂停按钮 */} {/* 自动暂停按钮 */} { setAutopause(!autopause); if (objurlRef) { stop(); } setPause(true); }} src={autopause ? IMAGES.autoplay : IMAGES.autopause} alt="autoplayorpause" > {/* 速度调节按钮 */} setShowSpeedAdjust(!showSpeedAdjust)} src={IMAGES.speed} alt="speed" className={`${showSpeedAdjust ? "bg-gray-200" : ""}`} > {/* 保存按钮 */} {/* 功能开关按钮 */}
setIPAEnabled(!ipaEnabled)} > {t("generateIPA")} { setShowSaveList(!showSaveList); }} selected={showSaveList} > {t("viewSavedItems")}
{/* 保存列表 */} {showSaveList && (
)}
); }