fix: handle Stop transcription (already running), click Show transcript to open panel, fix wildcard container matching to exclude buttons

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ValueOn AG 2026-02-17 22:38:57 +01:00
parent c36c954ac8
commit 4c0afa3a12

View file

@ -155,37 +155,84 @@ export class CaptionsProcedure {
this._logger.info(`Clicked "Record and transcribe": ${selector}`); this._logger.info(`Clicked "Record and transcribe": ${selector}`);
await this._page.waitForTimeout(1500); await this._page.waitForTimeout(1500);
// Log the submenu items
await this._logVisibleMenuItems(); await this._logVisibleMenuItems();
// Click "Start transcription" // Check if transcription is ALREADY running ("Stop transcription" visible)
const transcriptionSelectors = [ const stopSelectors = [
'[role="menuitem"]:has-text("Start transcription")', '[data-tid="call-transcript-button"]:has-text("Stop")',
'[role="menuitem"]:has-text("Transkription starten")', '[role="menuitem"]:has-text("Stop transcription")',
'[role="menuitem"]:has-text("transcription")', '[role="menuitem"]:has-text("Transkription beenden")',
'[role="menuitem"]:has-text("Transkription")', '[role="menuitem"]:has-text("Transkription stoppen")',
'button:has-text("Start transcription")',
'button:has-text("Transkription starten")',
'div:has-text("Start transcription")[role="menuitem"]',
]; ];
for (const transSel of transcriptionSelectors) { let alreadyRunning = false;
for (const stopSel of stopSelectors) {
try { try {
const transBtn = await this._page.$(transSel); const stopBtn = await this._page.$(stopSel);
if (transBtn) { if (stopBtn) {
await transBtn.click(); this._logger.info('Transcription already running (found "Stop transcription") — not clicking');
this._logger.info(`Clicked "Start transcription": ${transSel}`); alreadyRunning = true;
await this._page.waitForTimeout(2000); break;
return; // language dialog handled by _handleLanguageDialog()
} }
} catch { } catch {
// Continue // Continue
} }
} }
this._logger.warn('"Record and transcribe" opened but "Start transcription" not found'); if (!alreadyRunning) {
await this._page.keyboard.press('Escape'); // Click "Start transcription" (only explicit "Start" selectors)
break; const startSelectors = [
'[data-tid="call-transcript-button"]:has-text("Start")',
'[role="menuitem"]:has-text("Start transcription")',
'[role="menuitem"]:has-text("Transkription starten")',
'button:has-text("Start transcription")',
'button:has-text("Transkription starten")',
];
let started = false;
for (const startSel of startSelectors) {
try {
const startBtn = await this._page.$(startSel);
if (startBtn) {
await startBtn.click();
this._logger.info(`Clicked "Start transcription": ${startSel}`);
await this._page.waitForTimeout(2000);
started = true;
break;
}
} catch {
// Continue
}
}
if (!started) {
this._logger.warn('"Record and transcribe" opened but "Start transcription" not found');
}
}
// Click "Show transcript" to open the transcript panel for scraping
const showTranscriptSelectors = [
'[data-tid="transcript-panel-button"]',
'[role="menuitem"]:has-text("Show transcript")',
'[role="menuitem"]:has-text("Transkript anzeigen")',
'[role="menuitem"]:has-text("Transkript")',
];
for (const showSel of showTranscriptSelectors) {
try {
const showBtn = await this._page.$(showSel);
if (showBtn) {
await showBtn.click();
this._logger.info(`Clicked "Show transcript": ${showSel}`);
await this._page.waitForTimeout(2000);
break;
}
} catch {
// Continue
}
}
return;
} }
} catch { } catch {
// Continue // Continue
@ -493,15 +540,14 @@ export class CaptionsProcedure {
'div[data-tid="closed-caption-renderer-wrapper"]', 'div[data-tid="closed-caption-renderer-wrapper"]',
'div[data-tid="live-captions-renderer"]', 'div[data-tid="live-captions-renderer"]',
'[data-tid="caption-area"]', '[data-tid="caption-area"]',
// Transcript panel (authenticated Teams "Record and transcribe" flow)
'[data-tid="transcript-pane"]', '[data-tid="transcript-pane"]',
'[data-tid="transcript-view"]', '[data-tid="transcript-view"]',
'[data-tid*="transcript"]', '[data-tid="transcript-content"]',
]; ];
for (const selector of containerSelectors) { for (const selector of containerSelectors) {
try { try {
await this._page.waitForSelector(selector, { timeout: 10000 }); await this._page.waitForSelector(selector, { timeout: 8000 });
this._logger.info(`Found captions/transcript container: ${selector}`); this._logger.info(`Found captions/transcript container: ${selector}`);
return; return;
} catch { } catch {
@ -509,17 +555,22 @@ export class CaptionsProcedure {
} }
} }
// Log visible data-tid elements for debugging // Log ALL transcript/caption related data-tid elements for debugging
const tids = await this._page.evaluate(() => { const tids = await this._page.evaluate(() => {
const els = document.querySelectorAll('[data-tid]'); const els = document.querySelectorAll('[data-tid]');
return Array.from(els) return Array.from(els)
.map(e => e.getAttribute('data-tid') || '') .map(e => ({
.filter(t => t.includes('caption') || t.includes('transcript') || t.includes('subtitle')) tid: e.getAttribute('data-tid') || '',
.slice(0, 10); tag: e.tagName,
h: (e as HTMLElement).offsetHeight,
w: (e as HTMLElement).offsetWidth,
}))
.filter(t =>
t.tid.includes('caption') || t.tid.includes('transcript') || t.tid.includes('subtitle'),
)
.slice(0, 15);
}); });
if (tids.length > 0) { this._logger.info(`Transcript/caption data-tid elements: ${JSON.stringify(tids)}`);
this._logger.info(`Related data-tid elements found: ${JSON.stringify(tids)}`);
}
this._logger.warn('Could not find captions/transcript container with known selectors'); this._logger.warn('Could not find captions/transcript container with known selectors');
} }
@ -940,12 +991,14 @@ export class CaptionsProcedure {
'div[data-tid="closed-caption-renderer-wrapper"]', 'div[data-tid="closed-caption-renderer-wrapper"]',
'div[data-tid="live-captions-renderer"]', 'div[data-tid="live-captions-renderer"]',
'[data-tid="caption-area"]', '[data-tid="caption-area"]',
'[data-tid*="transcript"]', '[data-tid="transcript-pane"]',
'[data-tid="transcript-view"]',
'[data-tid="transcript-content"]',
]; ];
let containerFound = false; let containerFound = false;
for (const sel of waitSelectors) { for (const sel of waitSelectors) {
try { try {
await this._page.waitForSelector(sel, { timeout: 10000 }); await this._page.waitForSelector(sel, { timeout: 8000 });
containerFound = true; containerFound = true;
this._logger.info(`Captions/transcript container found: ${sel}`); this._logger.info(`Captions/transcript container found: ${sel}`);
break; break;
@ -953,7 +1006,27 @@ export class CaptionsProcedure {
// Try next // Try next
} }
} }
if (!containerFound) { if (!containerFound) {
// Log all transcript/caption related elements for debugging
const transcriptTids = await this._page.evaluate(() => {
const els = document.querySelectorAll('[data-tid]');
return Array.from(els)
.map(e => ({
tid: e.getAttribute('data-tid') || '',
tag: e.tagName,
h: (e as HTMLElement).offsetHeight,
w: (e as HTMLElement).offsetWidth,
children: e.children?.length || 0,
}))
.filter(t =>
t.tid.includes('caption') || t.tid.includes('transcript') || t.tid.includes('subtitle'),
)
.slice(0, 20);
});
this._logger.info(
`No exact container match. Transcript/caption elements: ${JSON.stringify(transcriptTids)}`,
);
this._logger.warn('Captions/transcript container not found, subscribing with body fallback'); this._logger.warn('Captions/transcript container not found, subscribing with body fallback');
} }
@ -1087,12 +1160,22 @@ export class CaptionsProcedure {
} }
} }
// Also try wildcard match (transcript container) // Also try wildcard match (transcript container — exclude buttons/controls)
if (!targetNode) { if (!targetNode) {
const transcriptEl = document.querySelector('[data-tid*="transcript"]'); const candidates = document.querySelectorAll('[data-tid*="transcript"]');
if (transcriptEl) { for (const c of Array.from(candidates)) {
targetNode = transcriptEl; const tid = c.getAttribute('data-tid') || '';
targetSelector = `[data-tid="${transcriptEl.getAttribute('data-tid')}"]`; const tag = c.tagName;
const height = (c as HTMLElement).offsetHeight || 0;
// Skip buttons, small elements, and control-related elements
if (
tag === 'BUTTON' || tag === 'SPAN' || tag === 'SVG' ||
tid.includes('button') || tid.includes('cancel') || tid.includes('stop') ||
height < 100
) continue;
targetNode = c;
targetSelector = `[data-tid="${tid}"]`;
break;
} }
} }
@ -1111,7 +1194,15 @@ export class CaptionsProcedure {
} }
// ── Fallback: observe document.body ── // ── Fallback: observe document.body ──
const allSelectors = [...containerSelectors, '[data-tid*="transcript"]']; const allSelectors = [...containerSelectors];
function _isTranscriptContainer(el: Element): boolean {
const tid = el.getAttribute('data-tid') || '';
if (!tid.includes('transcript')) return false;
if (el.tagName === 'BUTTON' || el.tagName === 'SPAN' || el.tagName === 'SVG') return false;
if (tid.includes('button') || tid.includes('cancel') || tid.includes('stop')) return false;
if ((el as HTMLElement).offsetHeight < 100) return false;
return true;
}
const bodyObserver = new MutationObserver((mutationsList) => { const bodyObserver = new MutationObserver((mutationsList) => {
for (const mutation of mutationsList) { for (const mutation of mutationsList) {
if (mutation.type !== 'childList') continue; if (mutation.type !== 'childList') continue;
@ -1119,7 +1210,7 @@ export class CaptionsProcedure {
if (node.nodeType !== Node.ELEMENT_NODE) return; if (node.nodeType !== Node.ELEMENT_NODE) return;
const el = node as HTMLElement; const el = node as HTMLElement;
// Check if a container just appeared // Check if a known container just appeared
for (const sel of allSelectors) { for (const sel of allSelectors) {
const container = el.matches?.(sel) ? el : el.querySelector?.(sel); const container = el.matches?.(sel) ? el : el.querySelector?.(sel);
if (container) { if (container) {
@ -1138,6 +1229,39 @@ export class CaptionsProcedure {
} }
} }
// Check if a transcript container appeared dynamically
if (_isTranscriptContainer(el)) {
bodyObserver.disconnect();
const tid = el.getAttribute('data-tid') || '';
const targeted = new MutationObserver((muts) => {
for (const m of muts) {
if (m.type === 'childList') {
m.addedNodes.forEach(_handleAddedNode);
}
}
});
targeted.observe(el, { childList: true, subtree: true });
(window as any).__captionsObserver = targeted;
return;
}
// Also check inside the added node for transcript containers
const transcriptChild = el.querySelector?.('[data-tid*="transcript"]');
if (transcriptChild && _isTranscriptContainer(transcriptChild)) {
bodyObserver.disconnect();
const tid = transcriptChild.getAttribute('data-tid') || '';
const targeted = new MutationObserver((muts) => {
for (const m of muts) {
if (m.type === 'childList') {
m.addedNodes.forEach(_handleAddedNode);
}
}
});
targeted.observe(transcriptChild, { childList: true, subtree: true });
(window as any).__captionsObserver = targeted;
return;
}
_handleAddedNode(node); _handleAddedNode(node);
}); });
} }