修复:将叫号音频内联到前端包

问题:音频使用 /audio 路径,浏览器预加载仍会向服务端发起独立请求。

实现:将 15 个 WAV 作为 Vite 内联资源编入前端 JS,后端继续仅返回叫号数据/文本。

验证:前端 66 项测试与生产构建通过;dist 中 WAV 文件为 0,含 15 个内联音频数据。
This commit is contained in:
2026-08-01 12:16:54 +08:00
parent 6d07612bc9
commit 4405bf96f7
17 changed files with 47 additions and 77 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,53 +1,32 @@
import { renderHook } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import { announcementAudioQueue, audioPreloadSources, useAudioAnnouncements } from "./useAudioAnnouncements";
import { describe, expect, it } from "vitest";
import { announcementAudioQueue } from "./useAudioAnnouncements";
afterEach(() => {
document.head.querySelectorAll("link[data-queue-voice-preload]").forEach((link) => link.remove());
});
const inlineAudio = (source: string) => source.startsWith("data:audio/wav;base64,");
describe("announcementAudioQueue", () => {
it("忽略项目名并把单号码播报素材重复三遍", () => {
const once = [
"/audio/queue-voice/please.wav",
"/audio/queue-voice/digit-0.wav",
"/audio/queue-voice/digit-0.wav",
"/audio/queue-voice/digit-0.wav",
"/audio/queue-voice/digit-1.wav",
"/audio/queue-voice/digit-6.wav",
"/audio/queue-voice/number.wav",
"/audio/queue-voice/entrance.wav",
"/audio/queue-voice/gap.wav",
];
const queue = announcementAudioQueue(["东门观光车,请零零零一六号,前往入口。"]);
const once = queue.slice(0, 9);
expect(announcementAudioQueue(["东门观光车,请零零零一六号,前往入口。"]))
.toEqual([...once, ...once, ...once]);
expect(once).toHaveLength(9);
expect(once.every(inlineAudio)).toBe(true);
expect(once[1]).toBe(once[2]);
expect(once[2]).toBe(once[3]);
expect(queue).toEqual([...once, ...once, ...once]);
});
it("支持连续号码范围", () => {
const queue = announcementAudioQueue(["请一七号至一九号,前往入口。"]);
expect(queue.slice(0, 9)).toEqual([
"/audio/queue-voice/please.wav",
"/audio/queue-voice/digit-1.wav",
"/audio/queue-voice/digit-7.wav",
"/audio/queue-voice/number.wav",
"/audio/queue-voice/through.wav",
"/audio/queue-voice/digit-1.wav",
"/audio/queue-voice/digit-9.wav",
"/audio/queue-voice/number.wav",
"/audio/queue-voice/entrance.wav",
]);
expect(queue.slice(0, 9).every(inlineAudio)).toBe(true);
expect(queue[1]).not.toBe(queue[2]);
expect(queue[3]).not.toBe(queue[4]);
});
it("打开页面时预加载全部声音素材", () => {
const { unmount } = renderHook(() => useAudioAnnouncements());
const links = Array.from(document.head.querySelectorAll<HTMLLinkElement>("link[data-queue-voice-preload]"));
it("音频素材不使用独立 HTTP 路径", () => {
const queue = announcementAudioQueue(["请一号,前往入口。"]);
expect(links.map((link) => link.getAttribute("href"))).toEqual(audioPreloadSources);
expect(links.every((link) => link.rel === "preload" && link.getAttribute("as") === "audio")).toBe(true);
unmount();
expect(document.head.querySelector("link[data-queue-voice-preload]")).toBeNull();
expect(queue).not.toContain("/audio/queue-voice/please.wav");
expect(queue.every(inlineAudio)).toBe(true);
});
});

View File

@@ -1,35 +1,43 @@
import { useCallback, useEffect, useRef } from "react";
import digit0Audio from "../assets/queue-voice/digit-0.wav?inline";
import digit1Audio from "../assets/queue-voice/digit-1.wav?inline";
import digit2Audio from "../assets/queue-voice/digit-2.wav?inline";
import digit3Audio from "../assets/queue-voice/digit-3.wav?inline";
import digit4Audio from "../assets/queue-voice/digit-4.wav?inline";
import digit5Audio from "../assets/queue-voice/digit-5.wav?inline";
import digit6Audio from "../assets/queue-voice/digit-6.wav?inline";
import digit7Audio from "../assets/queue-voice/digit-7.wav?inline";
import digit8Audio from "../assets/queue-voice/digit-8.wav?inline";
import digit9Audio from "../assets/queue-voice/digit-9.wav?inline";
import entranceAudio from "../assets/queue-voice/entrance.wav?inline";
import gapAudio from "../assets/queue-voice/gap.wav?inline";
import numberAudio from "../assets/queue-voice/number.wav?inline";
import pleaseAudio from "../assets/queue-voice/please.wav?inline";
import throughAudio from "../assets/queue-voice/through.wav?inline";
const AUDIO_BASE_PATH = "/audio/queue-voice";
const ANNOUNCEMENT_REPEAT_COUNT = 3;
const GAP_AUDIO = `${AUDIO_BASE_PATH}/gap.wav`;
const GAP_AUDIO = gapAudio;
const DIGIT_AUDIO: Record<string, string> = {
: `${AUDIO_BASE_PATH}/digit-0.wav`,
: `${AUDIO_BASE_PATH}/digit-1.wav`,
: `${AUDIO_BASE_PATH}/digit-2.wav`,
: `${AUDIO_BASE_PATH}/digit-3.wav`,
: `${AUDIO_BASE_PATH}/digit-4.wav`,
: `${AUDIO_BASE_PATH}/digit-5.wav`,
: `${AUDIO_BASE_PATH}/digit-6.wav`,
: `${AUDIO_BASE_PATH}/digit-7.wav`,
: `${AUDIO_BASE_PATH}/digit-8.wav`,
: `${AUDIO_BASE_PATH}/digit-9.wav`,
: digit0Audio,
: digit1Audio,
: digit2Audio,
: digit3Audio,
: digit4Audio,
: digit5Audio,
: digit6Audio,
: digit7Audio,
: digit8Audio,
: digit9Audio,
};
const PHRASE_AUDIO = [
["前往入口", `${AUDIO_BASE_PATH}/entrance.wav`],
["请", `${AUDIO_BASE_PATH}/please.wav`],
["号", `${AUDIO_BASE_PATH}/number.wav`],
["至", `${AUDIO_BASE_PATH}/through.wav`],
["前往入口", entranceAudio],
["请", pleaseAudio],
["号", numberAudio],
["至", throughAudio],
] as const;
export const audioPreloadSources = [
...Object.values(DIGIT_AUDIO),
...PHRASE_AUDIO.map(([, source]) => source),
GAP_AUDIO,
];
function clipsForAnnouncement(announcement: string): string[] {
const callStart = announcement.indexOf("请");
if (callStart < 0) return [];
@@ -67,23 +75,6 @@ export function useAudioAnnouncements() {
const audioRef = useRef<HTMLAudioElement | null>(null);
const playbackIdRef = useRef(0);
useEffect(() => {
if (typeof document === "undefined") return;
const links = audioPreloadSources.map((source) => {
const link = document.createElement("link");
link.rel = "preload";
link.setAttribute("as", "audio");
link.type = "audio/wav";
link.href = source;
link.dataset.queueVoicePreload = "true";
document.head.appendChild(link);
return link;
});
return () => links.forEach((link) => link.remove());
}, []);
const playQueue = useCallback((sources: string[]) => {
const audio = audioRef.current;
if (!audio || !sources.length) return;