281 lines
11 KiB
JavaScript
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);
|
|
}
|
|
})();
|
|
|