Refactor PK page and add Trellis setup
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import type { WordItem } from "../types";
|
||||
|
||||
type UsePkAudioOptions = {
|
||||
onGeneratedAudio: (word: WordItem) => void;
|
||||
};
|
||||
|
||||
export function usePkAudio({ onGeneratedAudio }: UsePkAudioOptions) {
|
||||
const [speechTip, setSpeechTip] = useState("点击开始后可朗读单词。");
|
||||
const audioContextRef = useRef<AudioContext | null>(null);
|
||||
|
||||
function ensureAudioContext() {
|
||||
if (!audioContextRef.current) {
|
||||
audioContextRef.current = new window.AudioContext();
|
||||
}
|
||||
return audioContextRef.current;
|
||||
}
|
||||
|
||||
async function playTone(correct: boolean) {
|
||||
const audioContext = ensureAudioContext();
|
||||
if (audioContext.state === "suspended") {
|
||||
await audioContext.resume();
|
||||
}
|
||||
|
||||
const osc = audioContext.createOscillator();
|
||||
const gain = audioContext.createGain();
|
||||
osc.connect(gain);
|
||||
gain.connect(audioContext.destination);
|
||||
|
||||
const duration = correct ? 0.4 : 0.3;
|
||||
|
||||
if (correct) {
|
||||
osc.frequency.setValueAtTime(523.25, audioContext.currentTime);
|
||||
osc.frequency.setValueAtTime(659.25, audioContext.currentTime + 0.1);
|
||||
osc.frequency.setValueAtTime(783.99, audioContext.currentTime + 0.2);
|
||||
gain.gain.setValueAtTime(0.25, audioContext.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.4);
|
||||
} else {
|
||||
osc.frequency.setValueAtTime(200, audioContext.currentTime);
|
||||
osc.frequency.setValueAtTime(150, audioContext.currentTime + 0.15);
|
||||
gain.gain.setValueAtTime(0.25, audioContext.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
|
||||
}
|
||||
|
||||
osc.start(audioContext.currentTime);
|
||||
osc.stop(audioContext.currentTime + duration);
|
||||
}
|
||||
|
||||
async function speakWord(word: WordItem) {
|
||||
if (word.audio) {
|
||||
const audio = new Audio(word.audio);
|
||||
audio.play().catch(() => speakSystemWord(word.english));
|
||||
setSpeechTip(`正在播放:${word.english}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setSpeechTip(`正在生成语音:${word.english}`);
|
||||
const response = await fetch(`/api/words/${word.id}/generate-audio`, { method: "POST" });
|
||||
if (!response.ok) throw new Error("generate failed");
|
||||
const data = (await response.json()) as { word: WordItem };
|
||||
const audio = new Audio(data.word.audio);
|
||||
audio.play().catch(() => speakSystemWord(word.english));
|
||||
setSpeechTip(`正在播放:${word.english}`);
|
||||
onGeneratedAudio(data.word);
|
||||
} catch {
|
||||
speakSystemWord(word.english);
|
||||
}
|
||||
}
|
||||
|
||||
function speakSystemWord(word: string) {
|
||||
if (!("speechSynthesis" in window)) {
|
||||
setSpeechTip("当前浏览器不支持英文朗读,请老师带读。");
|
||||
return;
|
||||
}
|
||||
|
||||
window.speechSynthesis.cancel();
|
||||
const utterance = new SpeechSynthesisUtterance(word);
|
||||
const voices = window.speechSynthesis.getVoices();
|
||||
utterance.voice =
|
||||
voices.find((voice) => /^en[-_]?US/i.test(voice.lang)) ??
|
||||
voices.find((voice) => /^en/i.test(voice.lang)) ??
|
||||
null;
|
||||
utterance.lang = utterance.voice?.lang ?? "en-US";
|
||||
utterance.rate = 0.85;
|
||||
utterance.pitch = 1;
|
||||
window.speechSynthesis.speak(utterance);
|
||||
setSpeechTip(`正在朗读:${word}`);
|
||||
}
|
||||
|
||||
return {
|
||||
speechTip,
|
||||
setSpeechTip,
|
||||
playTone,
|
||||
speakWord,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user