增加语音对话功能

This commit is contained in:
yhy
2026-08-29 22:13:55 +08:00
parent 1ffa069151
commit 3a2438f566
51 changed files with 6207 additions and 3 deletions
@@ -0,0 +1,124 @@
import type { StoredBeat } from "../infrastructure/storage/lectureStore";
import { pathToFileUrl } from "../infrastructure/platform/paths";
export interface PlayLectureHandle {
play: () => Promise<void>;
pause: () => void;
next: () => Promise<void>;
prev: () => Promise<void>;
jumpToBeatId: (beatId: string) => Promise<void>;
getCurrentBeatId: () => string | undefined;
isPlaying: () => boolean;
destroy: () => void;
setBeats: (beats: StoredBeat[]) => void;
}
export function createPlayLecture(options: {
audio: HTMLAudioElement;
beats: StoredBeat[];
onBeatChange?: (beatId: string | undefined) => void;
onPlayingChange?: (playing: boolean) => void;
onComplete?: () => void;
}): PlayLectureHandle {
let beats = options.beats.filter((b) => b.ttsStatus === "ready" && b.audioPath);
let index = 0;
let playing = false;
const notifyBeat = () => {
options.onBeatChange?.(beats[index]?.id);
};
const loadIndex = (i: number): boolean => {
if (i < 0 || i >= beats.length) return false;
index = i;
const beat = beats[i];
if (!beat.audioPath) return false;
options.audio.src = pathToFileUrl(beat.audioPath);
options.audio.load();
notifyBeat();
return true;
};
options.audio.onended = () => {
void (async () => {
if (index + 1 < beats.length) {
index += 1;
if (loadIndex(index)) {
try {
await options.audio.play();
} catch {
playing = false;
options.onPlayingChange?.(false);
}
}
} else {
playing = false;
options.onPlayingChange?.(false);
options.onComplete?.();
}
})();
};
return {
setBeats(next) {
beats = next.filter((b) => b.ttsStatus === "ready" && b.audioPath);
if (index >= beats.length) index = Math.max(0, beats.length - 1);
notifyBeat();
},
async play() {
if (!beats.length) return;
if (!options.audio.src || options.audio.paused) {
loadIndex(index);
}
try {
await options.audio.play();
playing = true;
options.onPlayingChange?.(true);
} catch (e) {
ztoolkit.log("[ChatPapers:Lecture] audio play failed", e);
playing = false;
options.onPlayingChange?.(false);
}
},
pause() {
options.audio.pause();
playing = false;
options.onPlayingChange?.(false);
},
async next() {
if (index + 1 >= beats.length) return;
index += 1;
if (loadIndex(index)) await this.play();
},
async prev() {
if (index <= 0) return;
index -= 1;
if (loadIndex(index)) await this.play();
},
async jumpToBeatId(beatId: string) {
const i = beats.findIndex((b) => b.id === beatId);
if (i < 0) return;
index = i;
if (loadIndex(index)) await this.play();
},
getCurrentBeatId() {
return beats[index]?.id;
},
isPlaying() {
return playing && !options.audio.paused;
},
destroy() {
options.audio.onended = null;
options.audio.pause();
playing = false;
},
};
}