From 0fcc662464adaee77b423f6dcbce16b368d73f34 Mon Sep 17 00:00:00 2001 From: yhy Date: Sat, 29 Aug 2026 23:23:16 +0800 Subject: [PATCH] Fix overlapping audio when explaining multi-line PDF selections. Use exclusive playback sessions so popup and pane do not play simultaneously, and stop standalone audio when the lecture pane is closed. Co-authored-by: Cursor --- .../application/playSelectionExplain.ts | 8 +++++ src/modules/lecture/ui/lecturePane.ts | 36 ++++++++++++++----- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/modules/lecture/application/playSelectionExplain.ts b/src/modules/lecture/application/playSelectionExplain.ts index fc69c2e..1f2bbdb 100644 --- a/src/modules/lecture/application/playSelectionExplain.ts +++ b/src/modules/lecture/application/playSelectionExplain.ts @@ -136,6 +136,14 @@ export async function playSelectionExplainStandalone( await playSelectionExplainExclusive(result); } +export function pauseStandalonePlayback(): void { + standaloneAudio?.pause(); + if (standaloneAudio) { + standaloneAudio.onended = null; + standaloneAudio.removeAttribute("src"); + } +} + export function stopSelectionExplainStandalone(): void { stopAllLecturePlayback(); } diff --git a/src/modules/lecture/ui/lecturePane.ts b/src/modules/lecture/ui/lecturePane.ts index 4a78a64..5045d3f 100644 --- a/src/modules/lecture/ui/lecturePane.ts +++ b/src/modules/lecture/ui/lecturePane.ts @@ -50,9 +50,9 @@ import { import { beginSelectionExplainSession, isSelectionExplainSessionActive, - playSelectionExplainAudio, playSelectionExplainExclusive, registerLecturePlaybackHandle, + pauseStandalonePlayback, stopAllLecturePlayback, } from "../application/playSelectionExplain"; import { getString } from "../../../utils/locale"; @@ -264,10 +264,23 @@ export class LecturePaneView { this.updateActionButton(); this.bindEvents(); + this.unregisterPlayback = registerLecturePlaybackHandle({ + pause: () => { + this.paragraphPlayer?.pause(); + this.player?.pause(); + this.audioEl?.pause(); + }, + }); await this.refreshFromStore(); } destroy(): void { + this.unregisterPlayback?.(); + this.unregisterPlayback = undefined; + this.paragraphPlayer?.pause(); + this.player?.pause(); + this.audioEl?.pause(); + pauseStandalonePlayback(); for (const off of this.unsubscribers) off(); this.unsubscribers = []; this.player?.destroy(); @@ -313,7 +326,7 @@ export class LecturePaneView { }), lectureEvents.on("lecture:selection_explain", (d) => { if (String(getRegularItem(this.item).id) !== d.itemId) return; - void this.applySelectionExplainResult(d.result); + void this.applySelectionExplainResult(d.result, { play: false }); }), ); } @@ -828,6 +841,8 @@ export class LecturePaneView { this.explainingSelection = true; this.explainSelectionBtn.disabled = true; + this.explainSessionId = beginSelectionExplainSession(); + const sessionId = this.explainSessionId; this.setStatus("summarizing", getString("lecture-selection-explaining")); try { @@ -836,7 +851,9 @@ export class LecturePaneView { selectedText, onProgress: (message) => this.setStatus("summarizing", message), }); - await this.applySelectionExplainResult(result); + if (!isSelectionExplainSessionActive(sessionId)) return; + await this.applySelectionExplainResult(result, { play: true }); + if (!isSelectionExplainSessionActive(sessionId)) return; this.setStatus("ready", getString("lecture-selection-done")); } catch (e) { const msg = e instanceof Error ? e.message : String(e); @@ -849,12 +866,17 @@ export class LecturePaneView { private async applySelectionExplainResult( result: SelectionExplainResult, + options?: { play?: boolean }, ): Promise { this.lastSelectionResult = result; this.renderSelectionExplain(result); await this.syncPdfHighlight(result.original, result.page); + if (options?.play === false) return; + + stopAllLecturePlayback(); + if (result.fromPrepared && result.matched) { if (!this.paragraphPlayer) { await this.refreshPlayer(); @@ -881,12 +903,10 @@ export class LecturePaneView { } if (!this.audioEl) return; - this.paragraphPlayer?.pause(); - this.player?.pause(); this.playBtn.hidden = true; this.pauseBtn.hidden = false; try { - await playSelectionExplainAudio(this.audioEl, result); + await playSelectionExplainExclusive(result, this.audioEl); } catch (e) { ztoolkit.log("[ChatPapers:Lecture] selection audio failed", e); } finally { @@ -938,8 +958,8 @@ export class LecturePaneView { this.doc.createTextNode(getString("lecture-selection-replay")), ); replayBtn.addEventListener("click", () => { - if (!this.audioEl || !this.lastSelectionResult) return; - void playSelectionExplainAudio(this.audioEl, this.lastSelectionResult); + if (!this.lastSelectionResult) return; + void playSelectionExplainExclusive(this.lastSelectionResult, this.audioEl); }); this.selectionExplainEl.append(replayBtn); }