This commit is contained in:
2025-10-05 19:43:57 +08:00
parent 658dba8566
commit ff7385cf81
9 changed files with 146 additions and 237 deletions

View File

@@ -22,5 +22,9 @@
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
font-family: var(--font-geist-sans), Arial, Helvetica, sans-serif;
}
.code-block {
font-family: var(--font-geist-mono), monospace;
}

View File

@@ -0,0 +1,91 @@
import Button from "@/components/Button";
import { EdgeTTS } from "edge-tts-universal";
import { useRef, useState } from "react";
import useAudioPlayer from "./useAudioPlayer";
export default function IPAForm(
{ voicesData }: {
voicesData: {
locale: string,
short_name: string
}[]
}
) {
const respref = useRef<HTMLParagraphElement>(null);
const inputref = useRef<HTMLTextAreaElement>(null);
const [reqEnabled, setReqEnabled] = useState<boolean>(true);
const [textInfo, setTextInfo] = useState<{
lang: string,
ipa: string,
locale: string,
text: string
} | null>(null);
const { playAudio, pauseAudio, stopAudio } = useAudioPlayer();
const readIPA = async () => {
if (!textInfo) {
respref.current!.innerText = '请先生成IPA。';
return;
}
const voice = voicesData.find(v => v.locale.startsWith(textInfo.locale));
if (!voice) {
respref.current!.innerText = '暂不支持朗读' + textInfo.lang;
return;
}
const tts = new EdgeTTS(textInfo.text, voice.short_name);
const result = await tts.synthesize();
playAudio(URL.createObjectURL(result.audio));
}
const generateIPA = () => {
if (!reqEnabled) return;
setReqEnabled(false);
respref.current!.innerText = '生成国际音标中,请稍等~';
let timer: NodeJS.Timeout;
(() => {
let count = 0;
timer = setInterval(() => {
respref.current!.innerText = '正在生成国际音标IPA请稍等';
respref.current!.innerText += `\n(waiting for ${++count}s)`
}, 1000);
})();
const text = inputref.current!.value.trim();
if (text.length === 0) return;
const params = new URLSearchParams({ text: text });
fetch(`/api/ipa?${params}`)
.then(response => {
if (!response.ok) {
return response.json().then(resj => {
throw new Error(`HTTP ${response.status}: ${resj.error} ${resj.message}`);
})
}
return response.json();
})
.then(data => {
setTextInfo({ ...data, text: text });
respref.current!.innerText = `LANG: ${data.lang}\nIPA: ${data.ipa}`;
})
.catch(error => {
respref.current!.innerText = `错误: ${error.message}`;
})
.finally(() => {
setReqEnabled(true);
clearInterval(timer);
});
}
return (<>
<div className="flex flex-row">
<textarea ref={inputref}
placeholder="输入任意语言的文本"
className="w-64 h-32 border-gray-300 border rounded focus:outline-blue-400 focus:outline-2">
</textarea>
</div>
<div className="m-2 flex-row flex gap-2">
<Button onClick={generateIPA} label="生成IPA"></Button>
<Button onClick={readIPA} label="朗读IPA"></Button>
</div>
<div ref={respref} className="whitespace-pre-line w-64"></div>
</>)
};

View File

@@ -1,33 +0,0 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "IPA Reader",
description: "read ipa aloud",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}

View File

@@ -1,155 +1,32 @@
"use client";
import Button from "@/components/Button";
import { useEffect, useRef, useState } from "react";
import { EdgeTTS } from "edge-tts-universal/browser";
import { useEffect, useState } from "react";
import IPAForm from "./IPAForm";
const useAudioPlayer = () => {
const audioRef = useRef<HTMLAudioElement | null>(null);
useEffect(() => {
audioRef.current = new Audio();
return () => {
if (audioRef.current) {
audioRef.current.pause();
audioRef.current = null;
}
};
}, []);
const playAudio = (audioUrl: string) => {
if (audioRef.current) {
audioRef.current.src = audioUrl;
audioRef.current.play().catch(error => {
console.error('播放失败:', error);
});
}
};
const pauseAudio = () => {
if (audioRef.current) {
audioRef.current.pause();
}
};
const stopAudio = () => {
if (audioRef.current) {
audioRef.current.pause();
audioRef.current.currentTime = 0;
}
};
return {
playAudio,
pauseAudio,
stopAudio
};
};
export default function Home() {
const respref = useRef<HTMLParagraphElement>(null);
const inputref = useRef<HTMLTextAreaElement>(null);
const [reqEnabled, setReqEnabled] = useState<boolean>(true);
const [textInfo, setTextInfo] = useState<{
lang: string,
ipa: string,
locale: string,
text: string
} | null>(null);
const { playAudio, pauseAudio, stopAudio } = useAudioPlayer();
const [voicesData, setVoicesData] = useState<{
locale: string,
short_name: string
}[] | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('/list_of_voices.json');
const jsonData = await response.json();
setVoicesData(jsonData);
} catch (error) {
console.error('加载JSON失败:', error);
} finally {
setLoading(false);
}
};
fetchData();
fetch('/list_of_voices.json')
.then(res => res.json())
.then(setVoicesData)
.catch(() => setVoicesData(null))
.finally(() => setLoading(false));
}, []);
if (loading) return <div>...</div>;
if (!voicesData) return <div></div>;
const readIPA = async () => {
if (!textInfo) {
respref.current!.innerText = '请先生成IPA。';
return;
}
const voice = voicesData.find(v => v.locale.startsWith(textInfo.locale));
if (!voice) {
respref.current!.innerText = '暂不支持朗读' + textInfo.lang;
return;
}
const tts = new EdgeTTS(textInfo.text, voice.short_name);
const result = await tts.synthesize();
playAudio(URL.createObjectURL(result.audio));
}
const generateIPA = () => {
if (!reqEnabled) return;
setReqEnabled(false);
respref.current!.innerText = '生成国际音标中,请稍等~';
let timer: NodeJS.Timeout;
(() => {
let count = 0;
timer = setInterval(() => {
respref.current!.innerText = '正在生成国际音标IPA请稍等';
respref.current!.innerText += `\n(waiting for ${++count}s)`
}, 1000);
})();
const text = inputref.current!.value.trim();
if (text.length === 0) return;
const params = new URLSearchParams({ text: text });
fetch(`/api/ipa?${params}`)
.then(response => {
if (!response.ok) {
return response.json().then(resj => {
throw new Error(`HTTP ${response.status}: ${resj.error} ${resj.message}`);
})
}
return response.json();
})
.then(data => {
setTextInfo({ ...data, text: text });
respref.current!.innerText = `LANG: ${data.lang}\nIPA: ${data.ipa}`;
})
.catch(error => {
respref.current!.innerText = `错误: ${error.message}`;
})
.finally(() => {
setReqEnabled(true);
clearInterval(timer);
});
}
return (
<div className="flex w-screen justify-center">
<div className="mt-8 bg-gray-100 shadow-xl rounded-xl p-4 flex items-center flex-col">
<h1 className="text-5xl mb-4">IPA Reader</h1>
<div className="flex flex-row">
<textarea ref={inputref}
placeholder="输入任意语言的文本"
className="w-64 h-32 border-gray-300 border rounded focus:outline-blue-400 focus:outline-2">
</textarea>
</div>
<div className="m-2 flex-row flex gap-2">
<Button onClick={generateIPA} label="生成IPA"></Button>
<Button onClick={readIPA} label="朗读IPA"></Button>
</div>
<div ref={respref} className="whitespace-pre-line w-64"></div>
<IPAForm voicesData={voicesData}></IPAForm>
</div>
</div>
);

View File

@@ -0,0 +1,39 @@
import { useRef, useEffect } from "react";
export default function useAudioPlayer() {
const audioRef = useRef<HTMLAudioElement | null>(null);
useEffect(() => {
audioRef.current = new Audio();
return () => {
if (audioRef.current) {
audioRef.current.pause();
audioRef.current = null;
}
};
}, []);
const playAudio = (audioUrl: string) => {
if (audioRef.current) {
audioRef.current.src = audioUrl;
audioRef.current.play().catch(error => {
console.error('播放失败:', error);
});
}
};
const pauseAudio = () => {
if (audioRef.current) {
audioRef.current.pause();
}
};
const stopAudio = () => {
if (audioRef.current) {
audioRef.current.pause();
audioRef.current.currentTime = 0;
}
};
return {
playAudio,
pauseAudio,
stopAudio
};
};

View File

@@ -14,7 +14,7 @@ const geistMono = Geist_Mono({
export const metadata: Metadata = {
title: "Learn Languages",
description: "A Website to learn languages",
description: "A Website to Learn Languages",
};
export default function RootLayout({

View File

@@ -1,33 +0,0 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "SRT Video Player",
description: "Practice spoken English",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}

View File

@@ -1,36 +0,0 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Word Board",
description: "Word board page",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
style={{
}}
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}

View File

@@ -142,7 +142,7 @@ export default function Home() {
<div className="flex w-screen h-screen justify-center items-center">
<div onKeyDown={handleKeyDown} className="p-5 bg-gray-200 rounded shadow-2xl">
<WordBoard selectWord={selectWord} words={words as [Word]} setWords={setWords} />
<div className="flex justify-center rounded mt-3">
<div className="flex justify-center rounded mt-3 gap-1">
<input ref={inputRef} placeholder="word to operate" type="text" className="focus:outline-none border-b-2 border-black" />
<Button label="插入" onClick={insertWord}></Button>
<Button label="删除" onClick={deleteWord}></Button>