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

Merged
brother7 merged 1 commits from codex/bundled-audio-announcements into main 2026-08-01 12:18:12 +08:00
17 changed files with 47 additions and 77 deletions
Showing only changes of commit 4405bf96f7 - Show all commits

View File

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

View File

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