Files
google-meet-captions/print_captions_word_by_word.js

234 lines
7.4 KiB
JavaScript

class CaptionCapture {
constructor() {
this.isCapturing = false; // Flag to control capturing
this.intervalId = null; // Store interval reference
this.lastSpans = []; // Store last seen spans
this.captionsLog = []; // Stores captions with timestamps
this.mergedCpation = [];
this.subtitleText= null;
this.checkSubtitleInterval=100;
}
// Function to detect and get the caption spans as an array
detectCaptionAsArray() {
let spans = document.querySelectorAll('div[jsname="tgaKEf"]');
return Array.from(spans).map(span => span.innerText.trim()) || "";
}
// Function to get absolute time in HH:MM:SS,mmm format (Real-Time)
getCurrentTimestamp() {
let now = new Date();
let hours = String(now.getHours()).padStart(2, '0');
let minutes = String(now.getMinutes()).padStart(2, '0');
let seconds = String(now.getSeconds()).padStart(2, '0');
let millis = String(now.getMilliseconds()).padStart(3, '0');
return `${hours}:${minutes}:${seconds},${millis}`; // Absolute time format
}
// Function to save new words with timestamp
saveCaptionByWords(timestamp, newText) {
this.captionsLog.push({ time: timestamp, text: newText });
console.log(`${timestamp} --> ${newText}`); // Print in subtitle format
}
formatTimestamp(ms) {
const h = String(Math.floor(ms / 3600000)).padStart(2, "0");
ms %= 3600000;
const m = String(Math.floor(ms / 60000)).padStart(2, "0");
ms %= 60000;
const s = String(Math.floor(ms / 1000)).padStart(2, "0");
const msStr = String(ms % 1000).padStart(3, "0");
return `${h}:${m}:${s},${msStr}`;
}
mergeCaptions(captions, gap = 1000) {
if (!captions || captions.length === 0) return [];
const parseTime = (timestamp) => {
const [h, m, sMs] = timestamp.split(":");
const [s, ms] = sMs.split(",");
return (
parseInt(h) * 3600000 +
parseInt(m) * 60000 +
parseInt(s) * 1000 +
parseInt(ms)
);
};
const merged = [];
let current = {
start: parseTime(captions[0].time),
end: parseTime(captions[0].time),
texts: [captions[0].text]
};
for (let i = 1; i < captions.length; i++) {
const entry = captions[i];
const time = parseTime(entry.time);
const timeDiff = time - current.end;
if (timeDiff > gap) {
merged.push({
start: this.formatTimestamp(current.start),
end: this.formatTimestamp(current.end),
text: current.texts.join(" ")
});
current = {
start: time,
end: time,
texts: [entry.text]
};
} else {
current.end = time;
current.texts.push(entry.text);
}
}
// Push the last group
merged.push({
start: this.formatTimestamp(current.start),
end: this.formatTimestamp(current.end),
text: current.texts.join(" ")
});
this.mergedCpation=merged;
return merged;
}
printMergedSubtitle() {
const merged = this.mergeCaptions(this.getCapturedCaptions(), 1000);
if (merged.length === 0) {
console.log("No subtitles to print.");
return;
}
const parseTime = (timestamp) => {
const [h, m, sMs] = timestamp.split(":");
const [s, ms] = sMs.split(",");
return (
parseInt(h) * 3600000 +
parseInt(m) * 60000 +
parseInt(s) * 1000 +
parseInt(ms)
);
};
const baseTime = parseTime(merged[0].start); // time of first word
const toRelative = (absTime) => {
const ms = parseTime(absTime) - baseTime;
return this.formatTimestamp(ms);
};
this.subtitleText = merged.map((item, index) =>
`${index + 1}\n${toRelative(item.start)} --> ${toRelative(item.end)}\n${item.text}\n`
).join("\n");
console.log(this.subtitleText);
return this.subtitleText;
}
generateSrtFilename() {
const now = new Date();
const pad = (n) => String(n).padStart(2, '0');
return `caption_${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}.srt`;
}
downloadSrtFile() {
if (!this.subtitleText) {
console.warn("No subtitle content found. Please call printMergedSubtitle() first.");
return;
}
const filename = this.generateSrtFilename();
const blob = new Blob([this.subtitleText], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
// Function to start capturing captions
startCapturing() {
if (this.isCapturing) {
console.log("Caption capturing is already running.");
return;
}
this.isCapturing = true;
console.log("Caption capturing started.");
this.intervalId = setInterval(() => {
let currentSpans = this.detectCaptionAsArray();
let lastOldSpan = this.lastSpans[this.lastSpans.length - 1] || "";
let lastNewSpan = currentSpans[currentSpans.length - 1] || "";
let isLastCaptionChanged = lastNewSpan !== lastOldSpan;
if (isLastCaptionChanged) {
this.lastSpans = currentSpans; // Update stored spans
let timestamp = this.getCurrentTimestamp();
// Get only the new part of the last span
let newText = lastNewSpan.replace(lastOldSpan, "").trim();
if (newText) {
console.log(newText)
const wordCount = newText.trim().split(/\s+/).length;
const BurstTextCount=80;
if (wordCount <= (this.checkSubtitleInterval/100)*BurstTextCount) {
this.saveCaptionByWords(timestamp, newText);
} else {
console.log(`Skipped burst text with ${wordCount} words at ${timestamp}`);
}
}
}
}, this.checkSubtitleInterval); // Runs every 100ms for better precision
}
// Function to stop capturing captions
stopCapturing() {
if (!this.isCapturing) {
console.log("Caption capturing is not running.");
return;
}
this.isCapturing = false;
clearInterval(this.intervalId);
console.log("Caption capturing stopped.");
}
// Function to get the saved captions log
getCapturedCaptions() {
return this.captionsLog;
}
}
// Usage:
const captionCapture = new CaptionCapture();
// Start capturing:
captionCapture.startCapturing();
// Stop capturing:
captionCapture.stopCapturing();
// Retrieve stored captions:
console.log(captionCapture.getCapturedCaptions());
captionCapture.printMergedSubtitle();
captionCapture.downloadSrtFile();