first commit
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-10-16 09:57:36 +08:00
commit a4c051946f
62 changed files with 21882 additions and 0 deletions

243
src/app/translator/page.tsx Normal file
View File

@@ -0,0 +1,243 @@
"use client";
import { ChangeEvent, useEffect, useState } from "react";
import Button from "@/components/Button";
import IconClick from "@/components/IconClick";
import { useAudioPlayer } from "@/hooks/useAudioPlayer";
import IMAGES from "@/config/images";
import { getTTSAudioUrl } from "@/utils";
export default function Home() {
const [ipaEnabled, setIPAEnabled] = useState(true);
const [voicesData, setVoicesData] = useState<{
locale: string,
short_name: string
}[] | null>(null);
const [loading, setLoading] = useState(true);
const [targetLang, setTargetLang] = useState('Italian');
const [sourceText, setSourceText] = useState('');
const [targetText, setTargetText] = useState('');
const [sourceIPA, setSourceIPA] = useState('');
const [targetIPA, setTargetIPA] = useState('');
const [sourceLocale, setSourceLocale] = useState<string | null>(null);
const [targetLocale, setTargetLocale] = useState<string | null>(null);
const [translating, setTranslating] = useState(false);
const { playAudio } = useAudioPlayer();
useEffect(() => {
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 tl = ['English', 'Italian', 'Japanese'];
const inputLanguage = () => {
const lang = prompt('Input a language.')?.trim();
if (lang) {
setTargetLang(lang);
}
}
const translate = () => {
if (translating) return;
if (sourceText.length === 0) return;
setTranslating(true);
setTargetText('');
setSourceLocale(null);
setTargetLocale(null);
setSourceIPA('');
setTargetIPA('');
const params = new URLSearchParams({
text: sourceText,
target: targetLang
})
fetch(`/api/translate?${params}`)
.then(res => res.json())
.then(obj => {
setSourceLocale(obj.source_locale);
setTargetLocale(obj.target_locale);
setTargetText(obj.target_text);
if (ipaEnabled) {
const params = new URLSearchParams({
text: sourceText
});
fetch(`/api/ipa?${params}`)
.then(res => res.json())
.then(data => {
setSourceIPA(data.ipa);
}).catch(e => {
console.error(e);
setSourceIPA('');
})
const params2 = new URLSearchParams({
text: obj.target_text
});
fetch(`/api/ipa?${params2}`)
.then(res => res.json())
.then(data => {
setTargetIPA(data.ipa);
}).catch(e => {
console.error(e);
setTargetIPA('');
})
}
}).catch(r => {
console.error(r);
setSourceLocale('');
setTargetLocale('');
setTargetText('');
}).finally(() => setTranslating(false));
}
const handleInputChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
setSourceText(e.target.value.trim());
setTargetText('');
setSourceLocale(null);
setTargetLocale(null);
setSourceIPA('');
setTargetIPA('');
}
const readSource = async () => {
if (sourceText.length === 0) return;
if (sourceIPA.length === 0 && ipaEnabled) {
const params = new URLSearchParams({
text: sourceText
});
fetch(`/api/ipa?${params}`)
.then(res => res.json())
.then(data => {
setSourceIPA(data.ipa);
}).catch(e => {
console.error(e);
setSourceIPA('');
})
}
if (!sourceLocale) {
try {
const params = new URLSearchParams({
text: sourceText.slice(0, 30)
});
const res = await fetch(`/api/locale?${params}`);
const info = await res.json();
setSourceLocale(info.locale);
const voice = voicesData.find(v => v.locale.startsWith(info.locale));
if (!voice) {
return;
}
const url = await getTTSAudioUrl(sourceText, voice.short_name);
await playAudio(url);
URL.revokeObjectURL(url);
} catch (e) {
console.error(e);
setSourceLocale(null);
return;
}
} else {
const voice = voicesData.find(v => v.locale.startsWith(sourceLocale!));
if (!voice) {
return;
}
const url = await getTTSAudioUrl(sourceText, voice.short_name);
await playAudio(url);
URL.revokeObjectURL(url);
}
}
const readTarget = async () => {
if (targetText.length === 0) return;
if (targetIPA.length === 0 && ipaEnabled) {
const params = new URLSearchParams({
text: targetText
});
fetch(`/api/ipa?${params}`)
.then(res => res.json())
.then(data => {
setTargetIPA(data.ipa);
}).catch(e => {
console.error(e);
setTargetIPA('');
})
}
const voice = voicesData.find(v => v.locale.startsWith(targetLocale!));
if (!voice) return;
const url = await getTTSAudioUrl(targetText, voice.short_name);
await playAudio(url);
URL.revokeObjectURL(url);
}
return (
<>
<div className="w-screen flex flex-col md:flex-row md:justify-between gap-2 p-2">
<div className="card1 w-full md:w-1/2 flex flex-col-reverse gap-2">
<div className="textarea1 border-1 border-gray-200 rounded-2xl w-full h-64 p-2">
<textarea onChange={handleInputChange} className="resize-none h-8/12 w-full focus:outline-0"></textarea>
<div className="ipa w-full h-2/12 overflow-auto text-gray-600">
{sourceIPA}
</div>
<div className="h-2/12 w-full flex justify-end items-center">
<IconClick onClick={async () => {
if (sourceText.length !== 0)
await navigator.clipboard.writeText(sourceText);
}} src={IMAGES.copy_all} alt="copy"></IconClick>
<IconClick onClick={readSource} src={IMAGES.play_arrow} alt="play"></IconClick>
</div>
</div>
<div className="option1 w-full flex flex-row justify-between items-center">
<span>detect language</span>
<Button label="generate ipa" selected={ipaEnabled} onClick={() => setIPAEnabled(!ipaEnabled)}></Button>
</div>
</div>
<div className="card2 w-full md:w-1/2 flex flex-col-reverse gap-2">
<div className="textarea2 bg-gray-100 rounded-2xl w-full h-64 p-2">
<div className="h-8/12 w-full">{
targetText
}</div>
<div className="ipa w-full h-2/12 overflow-auto text-gray-600">
{targetIPA}
</div>
<div className="h-2/12 w-full flex justify-end items-center">
<IconClick onClick={async () => {
if (targetText.length !== 0)
await navigator.clipboard.writeText(targetText);
}} src={IMAGES.copy_all} alt="copy"></IconClick>
<IconClick onClick={readTarget} src={IMAGES.play_arrow} alt="play"></IconClick>
</div>
</div>
<div className="option2 w-full flex gap-1 items-center flex-wrap">
<span>translate into</span>
<Button onClick={() => { setTargetLang('English') }} label="English" selected={targetLang === 'English'}></Button>
<Button onClick={() => { setTargetLang('Italian') }} label="Italian" selected={targetLang === 'Italian'}></Button>
<Button onClick={() => { setTargetLang('Japanese') }} label="Japanese" selected={targetLang === 'Japanese'}></Button>
<Button onClick={inputLanguage} label={'Other' + (tl.includes(targetLang) ? '' : ': ' + targetLang)} selected={!(tl.includes(targetLang))}></Button>
</div>
</div>
</div>
<div className="button-area w-screen flex justify-center items-center">
<button onClick={translate} className={`text-xl font-extrabold border rounded-4xl p-3 border-gray-200 h-16 ${translating ? 'bg-gray-200' : 'bg-white hover:bg-gray-200 hover:cursor-pointer'}`}>
{translating ? 'translating...' : 'translate'}
</button>
</div>
</>
);
}