Skip to content

Instantly share code, notes, and snippets.

@trojblue
Last active June 23, 2025 22:17
Show Gist options
  • Save trojblue/4a1df5023e6edf91dcda294ce665a390 to your computer and use it in GitHub Desktop.
Save trojblue/4a1df5023e6edf91dcda294ce665a390 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Export Full Meeting Transcripts (Lark)
// @namespace https://example.com
// @version 1.1
// @description Export all visible and virtualized transcript data
// @match https://*.larksuite.com/minutes/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
const PARAGRAPH_SELECTOR = '.paragraph-editor-wrapper';
const SPEAKER_SELECTOR = '.p-user-name';
const TIME_SELECTOR = '.p-time';
const TEXT_LINE_SELECTOR = '.ace-line';
const exportButton = document.createElement('button');
exportButton.textContent = 'Export Full Transcripts';
Object.assign(exportButton.style, {
position: 'fixed',
top: '10px',
right: '10px',
zIndex: 99999,
padding: '6px 12px',
backgroundColor: '#007bff',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
});
document.body.appendChild(exportButton);
const seenParagraphs = new Set();
function extractParagraphData(paragraph) {
const key = paragraph.getAttribute('data-zone-id');
if (key && seenParagraphs.has(key)) return null;
if (key) seenParagraphs.add(key);
const speakerEl = paragraph.querySelector(SPEAKER_SELECTOR);
const timeEl = paragraph.querySelector(TIME_SELECTOR);
const textEls = paragraph.querySelectorAll(TEXT_LINE_SELECTOR);
const speaker = speakerEl?.getAttribute('user-name-content') || speakerEl?.textContent.trim() || '';
const time = timeEl?.getAttribute('time-content') || timeEl?.textContent.trim() || '';
let text = '';
textEls.forEach(line => {
text += line.innerText.trim() + ' ';
});
return {
speaker,
time,
text: text.trim()
};
}
async function scrollAndCollectTranscripts() {
const container = document.querySelector('.rc-virtual-list-holder');
if (!container) {
alert("Transcript container not found.");
return;
}
const results = [];
let previousScrollTop = -1;
while (true) {
const paragraphs = document.querySelectorAll(PARAGRAPH_SELECTOR);
for (const p of paragraphs) {
const data = extractParagraphData(p);
if (data) results.push(data);
}
container.scrollTop += 500;
await new Promise(r => setTimeout(r, 300)); // wait for content to load
// Check if we're at the bottom (no new scroll movement)
if (container.scrollTop === previousScrollTop) break;
previousScrollTop = container.scrollTop;
}
return results;
}
function downloadFile(filename, content) {
const blob = new Blob([content], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
}
exportButton.onclick = async () => {
exportButton.textContent = 'Exporting...';
const results = await scrollAndCollectTranscripts();
downloadFile('full_transcript.json', JSON.stringify(results, null, 2));
exportButton.textContent = 'Export Full Transcripts';
};
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment