Import Google Meet captions project and extension backups
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
"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);
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user