fix: add baidu audio for youdao dict

This commit is contained in:
Gabe
2025-11-22 00:20:58 +08:00
parent 9d9bbd3821
commit 0705e8a65a
5 changed files with 129 additions and 48 deletions

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { apiBaiduTTS } from "../apis";
import { kissLog } from "../libs/log";
import { logger } from "../libs/log";
import { fetchData } from "../libs/fetch";
/**
* 声音播放hook
@@ -12,50 +12,97 @@ export function useAudio(src) {
const [error, setError] = useState(null);
const [ready, setReady] = useState(false);
const [playing, setPlaying] = useState(false);
const [loading, setLoading] = useState(false);
const onPlay = useCallback(() => {
audioRef.current?.play();
const onPlay = useCallback(async () => {
if (!audioRef.current) return;
try {
await audioRef.current.play();
} catch (err) {
logger.info("Playback failed:", err);
setPlaying(false);
}
}, []);
const onPause = useCallback(() => {
audioRef.current?.pause();
}, []);
useEffect(() => {
if (!src) {
return;
}
const audio = new Audio(src);
audio.addEventListener("error", (err) => setError(err));
audio.addEventListener("canplaythrough", () => setReady(true));
audio.addEventListener("play", () => setPlaying(true));
audio.addEventListener("ended", () => setPlaying(false));
if (!src) return;
let ignore = false;
let objectUrl = null;
setReady(false);
setError(null);
setPlaying(false);
setLoading(true);
const audio = new Audio();
audioRef.current = audio;
const handleCanPlay = () => setReady(true);
const handlePlay = () => setPlaying(true);
const handlePause = () => setPlaying(false);
const handleEnded = () => setPlaying(false);
const handleError = (e) => {
if (!ignore) {
setError(audio.error || e);
setReady(false);
setLoading(false);
}
};
audio.addEventListener("canplaythrough", handleCanPlay);
audio.addEventListener("play", handlePlay);
audio.addEventListener("pause", handlePause);
audio.addEventListener("ended", handleEnded);
audio.addEventListener("error", handleError);
const loadAudio = async () => {
try {
const data = await fetchData(src, {}, { expect: "audio" });
if (ignore) return;
audio.src = data;
setLoading(false);
} catch (err) {
if (!ignore) {
logger.info("Audio fetch failed:", err);
setError(err);
setLoading(false);
}
}
};
loadAudio();
return () => {
ignore = true;
audio.pause();
audio.removeAttribute("src");
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
}
audio.removeEventListener("canplaythrough", handleCanPlay);
audio.removeEventListener("play", handlePlay);
audio.removeEventListener("pause", handlePause);
audio.removeEventListener("ended", handleEnded);
audio.removeEventListener("error", handleError);
};
}, [src]);
return {
loading,
error,
ready,
playing,
onPlay,
onPause,
};
}
/**
* 获取语音hook
* @param {*} text
* @param {*} lan
* @param {*} spd
* @returns
*/
export function useTextAudio(text, lan = "uk", spd = 3) {
const [src, setSrc] = useState("");
useEffect(() => {
(async () => {
try {
setSrc(await apiBaiduTTS(text, lan, spd));
} catch (err) {
kissLog("baidu tts", err);
}
})();
}, [text, lan, spd]);
return useAudio(src);
}

View File

@@ -50,13 +50,13 @@ const newCacheReq = async (input, init) => {
* @param {*} init
* @returns
*/
export const getHttpCache = async ({ input, init }) => {
export const getHttpCache = async ({ input, init, expect }) => {
try {
const request = await newCacheReq(input, init);
const cache = await caches.open(CACHE_NAME);
const response = await cache.match(request);
if (response) {
const res = await parseResponse(response);
const res = await parseResponse(response, expect);
return res;
}
} catch (err) {
@@ -99,7 +99,7 @@ export const putHttpCache = async ({
* @param {*} res
* @returns
*/
export const parseResponse = async (res) => {
export const parseResponse = async (res, expect = null) => {
if (!res) {
throw new Error("Response object does not exist");
}
@@ -108,21 +108,45 @@ export const parseResponse = async (res) => {
const msg = {
url: res.url,
status: res.status,
statusText: res.statusText,
};
if (res.headers.get("Content-Type")?.includes("json")) {
msg.response = await res.json();
try {
const errorText = await res.clone().text();
try {
msg.response = JSON.parse(errorText);
} catch {
msg.response = errorText;
}
} catch (e) {
msg.response = "Unable to read error body";
}
throw new Error(JSON.stringify(msg));
}
const contentType = res.headers.get("Content-Type");
if (contentType?.includes("json")) {
return res.json();
} else if (contentType?.includes("audio")) {
const contentType = res.headers.get("Content-Type") || "";
if (expect === "blob") return res.blob();
if (expect === "text") return res.text();
if (expect === "json") return res.json();
if (
expect === "audio" ||
contentType.includes("audio") ||
contentType.includes("image") ||
contentType.includes("video")
) {
const blob = await res.blob();
return blobToBase64(blob);
}
return res.text();
const text = await res.text();
if (!text) return null;
try {
return JSON.parse(text);
} catch (err) {
return text;
}
};
/**

View File

@@ -99,7 +99,7 @@ export const fetchPatcher = async (input, init = {}, opts) => {
*/
export const fetchHandle = async ({ input, init, opts }) => {
const res = await fetchPatcher(input, init, opts);
return parseResponse(res);
return parseResponse(res, opts.expect);
};
/**

View File

@@ -1,8 +1,9 @@
import IconButton from "@mui/material/IconButton";
import VolumeUpIcon from "@mui/icons-material/VolumeUp";
import { useAudio } from "../../hooks/Audio";
import queryString from "query-string";
export default function AudioBtn({ src }) {
export function AudioBtn({ src }) {
const { error, ready, playing, onPlay } = useAudio(src);
if (error || !ready) {
@@ -27,3 +28,10 @@ export default function AudioBtn({ src }) {
</IconButton>
);
}
export function BaiduAudioBtn({ text, lan = "uk", spd = 3 }) {
if (!text) return null;
const src = `https://fanyi.baidu.com/gettts?${queryString.stringify({ lan, text, spd })}`;
return <AudioBtn src={src} />;
}

View File

@@ -1,5 +1,5 @@
import Typography from "@mui/material/Typography";
import AudioBtn from "./AudioBtn";
import { AudioBtn, BaiduAudioBtn } from "./AudioBtn";
import { OPT_DICT_BING, OPT_DICT_YOUDAO } from "../../config";
import { apiMicrosoftDict, apiYoudaoDict } from "../../apis";
@@ -48,12 +48,14 @@ export const dictHandlers = {
style={{ display: "inline-block", paddingRight: "1em" }}
>
<Typography component="span">{`UK [${data?.ec?.word?.ukphone}]`}</Typography>
<BaiduAudioBtn text={data?.ec?.word?.["return-phrase"]} lan="uk" />
</Typography>
<Typography
component="div"
style={{ display: "inline-block", paddingRight: "1em" }}
>
<Typography component="span">{`US [${data?.ec?.word?.usphone}]`}</Typography>
<BaiduAudioBtn text={data?.ec?.word?.["return-phrase"]} lan="en" />
</Typography>
</Typography>
),