Files
google-meet-captions/caption-extension/content.js
T

281 lines
11 KiB
JavaScript

"use strict";
(async () => {
// This file cannot import config.js or logger.js directly because it runs in a different context (source:https://stackoverflow.com/a/53033388/4235602)
// So we need to load them dynamically. But our config.js and logger.js are in the same directory and can import each other
try {
//#region load logger
const loggerSrc = chrome.runtime.getURL("logger.js");
const loggerModule = await import(loggerSrc);
const logger = loggerModule.default;
logger.debug("Logger initialized successfully");
logger.debug("Content script starting...");
//#endregion
//#region load config
const configSrc = chrome.runtime.getURL("config.js");
const configModule = await import(configSrc);
const config = configModule.default;
logger.debug("Config loaded:", config);
//#endregion
class CaptionCapture {
constructor() {
this.isCapturing = false;
this.intervalId = null;
this.lastSpans = [];
this.captionsLog = [];
this.mergedCaption = [];
this.subtitleText = null;
this.checkSubtitleInterval = config.defaultCaptureInterval;
}
// Function to detect and get the caption spans as an array
detectCaptionAsArray() {
try {
let spans = document.querySelectorAll('div.ygicle.VbkSUe');
if (!spans || spans.length === 0) {
logger.debug("No caption spans found");
return [];
}
return Array.from(spans).map(span => span.innerText.trim()) || [];
} catch (error) {
logger.error("Error detecting captions:", error);
return [];
}
}
// 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 });
logger.debug(`${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.mergedCaption = merged;
return merged;
}
printMergedSubtitle() {
const merged = this.mergeCaptions(this.getCapturedCaptions(), 1000);
if (merged.length === 0) {
logger.debug("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");
logger.debug(this.subtitleText);
return this.subtitleText;
}
generateSrtFilename() {
const now = new Date();
const pad = (n) => String(n).padStart(2, '0');
return `meet_caption_${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}.srt`;
}
downloadSrtFile() {
if (!this.subtitleText) {
logger.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) {
logger.debug("Caption capturing is already running.");
return;
}
this.isCapturing = true;
logger.debug("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) {
logger.debug(newText)
const wordCount = newText.trim().split(/\s+/).length;
const BurstTextCount = 80;
if (wordCount <= (this.checkSubtitleInterval / 100) * BurstTextCount) {
this.saveCaptionByWords(timestamp, newText);
} else {
logger.debug(`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) {
logger.debug("Caption capturing is not running.");
return;
}
this.isCapturing = false;
clearInterval(this.intervalId);
logger.debug("Caption capturing stopped.");
}
// Function to get the saved captions log
getCapturedCaptions() {
return this.captionsLog;
}
}
// Initialize CaptionCapture and message listener
const captionCapture = new CaptionCapture();
logger.debug("CaptionCapture initialized");
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
logger.debug("start of listener in content.js");
if (message.action === "startCapture") {
logger.debug("startCapture content.js");
captionCapture.startCapturing();
sendResponse({ status: "started" });
} else if (message.action === "stopCapture") {
logger.debug("stopCapture content.js");
captionCapture.stopCapturing();
sendResponse({ status: "stopped" });
} else if (message.action === "downloadSrt") {
alert("Downloading SRT file...");
logger.debug("downloadSrt content.js");
captionCapture.printMergedSubtitle();
captionCapture.downloadSrtFile();
sendResponse({ status: "downloadStarted" });
}
});
logger.debug("Message listener set up");
} catch (error) {
logger.error("Error loading modules:", error);
}
})();