Merge pull request #385 from makaveli10/add_srt_download_opt_browser_ext
Add srt download opt browser ext
This commit is contained in:
@@ -27,6 +27,7 @@ To capture the audio in the current tab, we used the chrome `tabCapture` API to
|
|||||||
When using the Audio Transcription extension, you have the following options:
|
When using the Audio Transcription extension, you have the following options:
|
||||||
- **Use Collabora Server**: We provide a demo server which runs the whisper small model.
|
- **Use Collabora Server**: We provide a demo server which runs the whisper small model.
|
||||||
- **Language**: Select the target language for transcription or translation. You can choose from a variety of languages supported by OpenAI-whisper.
|
- **Language**: Select the target language for transcription or translation. You can choose from a variety of languages supported by OpenAI-whisper.
|
||||||
|
- **Download SRT file at Stop Capture**: Select if you want to download the srt file for the session at stop capture.
|
||||||
- **Task:** Choose the specific task to perform on the audio. You can select either "transcribe" for transcription or "translate" to translate the audio to English.
|
- **Task:** Choose the specific task to perform on the audio. You can select either "transcribe" for transcription or "translate" to translate the audio to English.
|
||||||
- **Model Size**: Select the whisper model size to run the server with.
|
- **Model Size**: Select the whisper model size to run the server with.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
class AudioPreProcessor extends AudioWorkletProcessor {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.sampleRate = sampleRate || 48000;
|
||||||
|
this.targetSampleRate = 16000;
|
||||||
|
this.inputSamplesNeeded = this.sampleRate * 0.5; // 0.5s
|
||||||
|
this.inputBuffer = new Float32Array(this.inputSamplesNeeded);
|
||||||
|
this.inputWriteOffset = 0;
|
||||||
|
this.processCount = 0;
|
||||||
|
this.audioDetectedCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
process(inputs, outputs) {
|
||||||
|
this.processCount++;
|
||||||
|
|
||||||
|
const input = inputs[0];
|
||||||
|
const output = outputs[0];
|
||||||
|
|
||||||
|
if (!input || input.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let channel = 0; channel < Math.min(input.length, output.length); channel++) {
|
||||||
|
if (input[channel] && output[channel]) {
|
||||||
|
output[channel].set(input[channel]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let monoInput;
|
||||||
|
if (input.length === 1) {
|
||||||
|
monoInput = input[0];
|
||||||
|
} else if (input.length >= 2) {
|
||||||
|
monoInput = new Float32Array(input[0].length);
|
||||||
|
for (let i = 0; i < input[0].length; i++) {
|
||||||
|
monoInput[i] = (input[0][i] + (input[1] ? input[1][i] : 0)) * 0.5;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!monoInput || monoInput.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let inputOffset = 0;
|
||||||
|
while (inputOffset < monoInput.length) {
|
||||||
|
const remainingBuffer = this.inputSamplesNeeded - this.inputWriteOffset;
|
||||||
|
const toCopy = Math.min(remainingBuffer, monoInput.length - inputOffset);
|
||||||
|
this.inputBuffer.set(monoInput.subarray(inputOffset, inputOffset + toCopy), this.inputWriteOffset);
|
||||||
|
|
||||||
|
this.inputWriteOffset += toCopy;
|
||||||
|
inputOffset += toCopy;
|
||||||
|
|
||||||
|
if (this.inputWriteOffset === this.inputSamplesNeeded) {
|
||||||
|
const downsampled = this.downsampleTo16kHz(this.inputBuffer);
|
||||||
|
this.port.postMessage(downsampled);
|
||||||
|
|
||||||
|
this.inputWriteOffset = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
downsampleTo16kHz(inputBuffer) {
|
||||||
|
const ratio = this.sampleRate / this.targetSampleRate;
|
||||||
|
const length = Math.floor(inputBuffer.length / ratio);
|
||||||
|
const result = new Float32Array(length);
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
const idx = Math.floor(i * ratio);
|
||||||
|
result[i] = inputBuffer[idx];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerProcessor('audiopreprocessor', AudioPreProcessor);
|
||||||
@@ -159,6 +159,7 @@ async function startCapture(options) {
|
|||||||
task: options.task,
|
task: options.task,
|
||||||
modelSize: options.modelSize,
|
modelSize: options.modelSize,
|
||||||
useVad: options.useVad,
|
useVad: options.useVad,
|
||||||
|
saveCaptions: options.saveCaptions,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -174,14 +175,14 @@ async function startCapture(options) {
|
|||||||
* Stops the capture process and performs cleanup.
|
* Stops the capture process and performs cleanup.
|
||||||
* @returns {Promise<void>} - A Promise that resolves when the capture process is stopped successfully.
|
* @returns {Promise<void>} - A Promise that resolves when the capture process is stopped successfully.
|
||||||
*/
|
*/
|
||||||
async function stopCapture() {
|
async function stopCapture(options) {
|
||||||
const optionTabId = await getLocalStorageValue("optionTabId");
|
const optionTabId = await getLocalStorageValue("optionTabId");
|
||||||
const currentTabId = await getLocalStorageValue("currentTabId");
|
const currentTabId = await getLocalStorageValue("currentTabId");
|
||||||
|
|
||||||
if (optionTabId) {
|
if (optionTabId) {
|
||||||
res = await sendMessageToTab(currentTabId, {
|
res = await sendMessageToTab(currentTabId, {
|
||||||
type: "STOP",
|
type: "STOP",
|
||||||
data: { currentTabId: currentTabId },
|
data: { currentTabId: currentTabId, saveCaptions: options.saveCaptions },
|
||||||
});
|
});
|
||||||
await removeChromeTab(optionTabId);
|
await removeChromeTab(optionTabId);
|
||||||
}
|
}
|
||||||
@@ -196,7 +197,7 @@ chrome.runtime.onMessage.addListener(async (message) => {
|
|||||||
if (message.action === "startCapture") {
|
if (message.action === "startCapture") {
|
||||||
startCapture(message);
|
startCapture(message);
|
||||||
} else if (message.action === "stopCapture") {
|
} else if (message.action === "stopCapture") {
|
||||||
stopCapture();
|
stopCapture(message);
|
||||||
} else if (message.action === "updateSelectedLanguage") {
|
} else if (message.action === "updateSelectedLanguage") {
|
||||||
const detectedLanguage = message.detectedLanguage;
|
const detectedLanguage = message.detectedLanguage;
|
||||||
chrome.runtime.sendMessage({ action: "updateSelectedLanguage", detectedLanguage });
|
chrome.runtime.sendMessage({ action: "updateSelectedLanguage", detectedLanguage });
|
||||||
@@ -204,7 +205,7 @@ chrome.runtime.onMessage.addListener(async (message) => {
|
|||||||
} else if (message.action === "toggleCaptureButtons") {
|
} else if (message.action === "toggleCaptureButtons") {
|
||||||
chrome.runtime.sendMessage({ action: "toggleCaptureButtons", data: false });
|
chrome.runtime.sendMessage({ action: "toggleCaptureButtons", data: false });
|
||||||
chrome.storage.local.set({ capturingState: { isCapturing: false } })
|
chrome.storage.local.set({ capturingState: { isCapturing: false } })
|
||||||
stopCapture();
|
stopCapture({saveCaptions: message.saveCaptions});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,45 @@
|
|||||||
|
|
||||||
|
|
||||||
var elem_container = null;
|
var elem_container = null;
|
||||||
var elem_text = null;
|
var elem_text = null;
|
||||||
|
|
||||||
var segments = [];
|
var segments = [];
|
||||||
var text_segments = [];
|
var text_segments = [];
|
||||||
|
var allSegments = [];
|
||||||
|
var lastIncompleteSegment = null;
|
||||||
|
|
||||||
|
function formatTime(seconds) {
|
||||||
|
const date = new Date(seconds * 1000);
|
||||||
|
const hh = String(date.getUTCHours()).padStart(2, '0');
|
||||||
|
const mm = String(date.getUTCMinutes()).padStart(2, '0');
|
||||||
|
const ss = String(date.getUTCSeconds()).padStart(2, '0');
|
||||||
|
const mmm = String(date.getUTCMilliseconds()).padStart(3, '0');
|
||||||
|
return `${hh}:${mm}:${ss},${mmm}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSRT() {
|
||||||
|
return allSegments
|
||||||
|
.map((seg, i) => {
|
||||||
|
const start = formatTime(seg.start);
|
||||||
|
const end = formatTime(seg.end);
|
||||||
|
const text = seg.text.trim().replace(/[\r\n]+/g, ' ');
|
||||||
|
return `${i + 1}\n${start} --> ${end}\n${text}`;
|
||||||
|
})
|
||||||
|
.join('\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadSRT() {
|
||||||
|
console.log("downloadSRT called");
|
||||||
|
console.log("Total segments for SRT:", allSegments.length);
|
||||||
|
const srtBlob = new Blob([generateSRT()], { type: 'text/srt;charset=utf-8' });
|
||||||
|
const url = URL.createObjectURL(srtBlob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'captions.srt';
|
||||||
|
a.style.display = 'none';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
document.body.removeChild(a);
|
||||||
|
}
|
||||||
|
|
||||||
function initPopupElement() {
|
function initPopupElement() {
|
||||||
if (document.getElementById('popupElement')) {
|
if (document.getElementById('popupElement')) {
|
||||||
@@ -32,7 +67,7 @@ function initPopupElement() {
|
|||||||
closePopupButton.style.cursor = 'pointer';
|
closePopupButton.style.cursor = 'pointer';
|
||||||
closePopupButton.addEventListener('click', async () => {
|
closePopupButton.addEventListener('click', async () => {
|
||||||
popupContainer.style.display = 'none';
|
popupContainer.style.display = 'none';
|
||||||
await browser.runtime.sendMessage({ action: 'toggleCaptureButtons', data: false });
|
await chrome.runtime.sendMessage({ action: 'toggleCaptureButtons', data: false });
|
||||||
});
|
});
|
||||||
buttonContainer.appendChild(closePopupButton);
|
buttonContainer.appendChild(closePopupButton);
|
||||||
popupContainer.appendChild(buttonContainer);
|
popupContainer.appendChild(buttonContainer);
|
||||||
@@ -169,8 +204,25 @@ function remove_element() {
|
|||||||
|
|
||||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||||
const { type, data } = request;
|
const { type, data } = request;
|
||||||
|
const saveCaptions = data.saveCaptions;
|
||||||
if (type === "STOP") {
|
|
||||||
|
if (type === "STOP") {
|
||||||
|
if (saveCaptions === true) {
|
||||||
|
// If there is a last incomplete segment, push it to allSegments
|
||||||
|
if (lastIncompleteSegment && lastIncompleteSegment.text && lastIncompleteSegment.text.trim() !== "") {
|
||||||
|
// Apply same Python logic: check if transcript is empty OR start >= last end
|
||||||
|
if (allSegments.length === 0 || parseFloat(lastIncompleteSegment.start) >= parseFloat(allSegments[allSegments.length - 1].end)) {
|
||||||
|
allSegments.push({
|
||||||
|
start: lastIncompleteSegment.start,
|
||||||
|
end: lastIncompleteSegment.end,
|
||||||
|
text: lastIncompleteSegment.text
|
||||||
|
});
|
||||||
|
console.log("Added final incomplete segment");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadSRT();
|
||||||
|
}
|
||||||
remove_element();
|
remove_element();
|
||||||
sendResponse({data: "STOPPED"});
|
sendResponse({data: "STOPPED"});
|
||||||
return true;
|
return true;
|
||||||
@@ -184,53 +236,77 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|||||||
|
|
||||||
init_element();
|
init_element();
|
||||||
|
|
||||||
message = JSON.parse(data);
|
try {
|
||||||
message = message["segments"];
|
const message = JSON.parse(data.data);
|
||||||
|
const segments = message["segments"];
|
||||||
var text = '';
|
|
||||||
for (var i = 0; i < message.length; i++) {
|
if (saveCaptions === true) {
|
||||||
text += message[i].text + ' ';
|
segments.forEach(seg => {
|
||||||
}
|
if (seg.completed === true &&
|
||||||
text = text.replace(/(\r\n|\n|\r)/gm, "");
|
(allSegments.length === 0 || parseFloat(seg.start) >= parseFloat(allSegments[allSegments.length - 1].end))) {
|
||||||
|
allSegments.push({
|
||||||
var elem = document.getElementById('t3');
|
start: seg.start,
|
||||||
elem.innerHTML = text;
|
end: seg.end,
|
||||||
|
text: seg.text
|
||||||
var line_height_style = getStyle('t3', 'line-height');
|
});
|
||||||
var line_height = parseInt(line_height_style.substring(0, line_height_style.length - 2));
|
|
||||||
var divHeight = elem.offsetHeight;
|
lastIncompleteSegment = null;
|
||||||
var lines = divHeight / line_height;
|
} else if (seg.completed !== true) {
|
||||||
|
lastIncompleteSegment = seg;
|
||||||
text_segments = [];
|
}
|
||||||
text_segments = get_lines(elem, line_height);
|
});
|
||||||
|
|
||||||
elem.innerHTML = '';
|
|
||||||
|
|
||||||
if (text_segments.length > 2) {
|
|
||||||
for (var i = 0; i < 3; i++) {
|
|
||||||
document.getElementById('t' + i).innerHTML = text_segments[text_segments.length - 3 + i];
|
|
||||||
}
|
}
|
||||||
} else {
|
var text = '';
|
||||||
for (var i = 0; i < 3; i++) {
|
for (var i = 0; i < segments.length; i++) {
|
||||||
document.getElementById('t' + i).innerHTML = '';
|
text += segments[i].text + ' ';
|
||||||
}
|
}
|
||||||
}
|
text = text.replace(/(\r\n|\n|\r)/gm, "");
|
||||||
|
|
||||||
|
var elem = document.getElementById('t3');
|
||||||
|
if (elem) {
|
||||||
|
elem.innerHTML = text;
|
||||||
|
|
||||||
if (text_segments.length <= 2) {
|
var line_height_style = getStyle('t3', 'line-height');
|
||||||
for (var i = 0; i < text_segments.length; i++) {
|
var line_height = parseInt(line_height_style.substring(0, line_height_style.length - 2));
|
||||||
document.getElementById('t' + i).innerHTML = text_segments[i];
|
var divHeight = elem.offsetHeight;
|
||||||
}
|
var lines = divHeight / line_height;
|
||||||
} else {
|
|
||||||
for (var i = 0; i < 3; i++) {
|
|
||||||
document.getElementById('t' + i).innerHTML = text_segments[text_segments.length - 3 + i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 1; i < 3; i++)
|
text_segments = [];
|
||||||
{
|
text_segments = get_lines(elem, line_height);
|
||||||
var parent_elem = document.getElementById('t' + (i - 1));
|
|
||||||
var elem = document.getElementById('t' + i);
|
elem.innerHTML = '';
|
||||||
elem.style.top = parent_elem.offsetHeight + parent_elem.offsetTop + 'px';
|
|
||||||
|
if (text_segments.length > 2) {
|
||||||
|
for (var i = 0; i < 3; i++) {
|
||||||
|
document.getElementById('t' + i).innerHTML = text_segments[text_segments.length - 3 + i];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (var i = 0; i < 3; i++) {
|
||||||
|
document.getElementById('t' + i).innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text_segments.length <= 2) {
|
||||||
|
for (var i = 0; i < text_segments.length; i++) {
|
||||||
|
document.getElementById('t' + i).innerHTML = text_segments[i];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (var i = 0; i < 3; i++) {
|
||||||
|
document.getElementById('t' + i).innerHTML = text_segments[text_segments.length - 3 + i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 1; i < 3; i++)
|
||||||
|
{
|
||||||
|
var parent_elem = document.getElementById('t' + (i - 1));
|
||||||
|
var elem = document.getElementById('t' + i);
|
||||||
|
if (parent_elem && elem) {
|
||||||
|
elem.style.top = parent_elem.offsetHeight + parent_elem.offsetTop + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error processing message:", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendResponse({});
|
sendResponse({});
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
|
|
||||||
"name": "Audio Transcription",
|
"name": "Audio Transcription",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "This extension captures the audio on the current tab, sends it to a server for transcription and shows the transcription in Real-time.",
|
"description": "This extension captures the audio on the current tab, sends it to a server for transcription and shows the transcription in Real-time.",
|
||||||
|
|
||||||
"options_page": "options.html",
|
"options_page": "options.html",
|
||||||
"background": {
|
"background": {
|
||||||
"service_worker": "background.js"
|
"service_worker": "background.js"
|
||||||
},
|
},
|
||||||
|
"web_accessible_resources": [
|
||||||
|
{
|
||||||
|
"resources": ["audiopreprocessor.js"],
|
||||||
|
"matches": ["<all_urls>"]
|
||||||
|
}
|
||||||
|
],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"storage",
|
"storage",
|
||||||
"activeTab",
|
"activeTab",
|
||||||
|
|||||||
@@ -31,41 +31,6 @@ function sendMessageToTab(tabId, data) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resamples the audio data to a target sample rate of 16kHz.
|
|
||||||
* @param {Array|ArrayBuffer|TypedArray} audioData - The input audio data.
|
|
||||||
* @param {number} [origSampleRate=44100] - The original sample rate of the audio data.
|
|
||||||
* @returns {Float32Array} The resampled audio data at 16kHz.
|
|
||||||
*/
|
|
||||||
function resampleTo16kHZ(audioData, origSampleRate = 44100) {
|
|
||||||
// Convert the audio data to a Float32Array
|
|
||||||
const data = new Float32Array(audioData);
|
|
||||||
|
|
||||||
// Calculate the desired length of the resampled data
|
|
||||||
const targetLength = Math.round(data.length * (16000 / origSampleRate));
|
|
||||||
|
|
||||||
// Create a new Float32Array for the resampled data
|
|
||||||
const resampledData = new Float32Array(targetLength);
|
|
||||||
|
|
||||||
// Calculate the spring factor and initialize the first and last values
|
|
||||||
const springFactor = (data.length - 1) / (targetLength - 1);
|
|
||||||
resampledData[0] = data[0];
|
|
||||||
resampledData[targetLength - 1] = data[data.length - 1];
|
|
||||||
|
|
||||||
// Resample the audio data
|
|
||||||
for (let i = 1; i < targetLength - 1; i++) {
|
|
||||||
const index = i * springFactor;
|
|
||||||
const leftIndex = Math.floor(index).toFixed();
|
|
||||||
const rightIndex = Math.ceil(index).toFixed();
|
|
||||||
const fraction = index - leftIndex;
|
|
||||||
resampledData[i] = data[leftIndex] + (data[rightIndex] - data[leftIndex]) * fraction;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the resampled data
|
|
||||||
return resampledData;
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateUUID() {
|
function generateUUID() {
|
||||||
let dt = new Date().getTime();
|
let dt = new Date().getTime();
|
||||||
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||||
@@ -76,24 +41,99 @@ function generateUUID() {
|
|||||||
return uuid;
|
return uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Global variables for audio processing
|
||||||
|
let audioContext = null;
|
||||||
|
let preNode = null;
|
||||||
|
let socket = null;
|
||||||
|
let isServerReady = false;
|
||||||
|
let currentStream = null;
|
||||||
|
let currentOptions = null;
|
||||||
|
|
||||||
|
// AudioWorklet URL - make sure this path matches your manifest.json
|
||||||
|
const WORKLET_URL = chrome.runtime.getURL('audiopreprocessor.js');
|
||||||
|
|
||||||
|
async function initAudioWorklet(stream) {
|
||||||
|
audioContext = new AudioContext();
|
||||||
|
if (audioContext.state === 'suspended') {
|
||||||
|
await audioContext.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await audioContext.audioWorklet.addModule(WORKLET_URL);
|
||||||
|
preNode = new AudioWorkletNode(audioContext, 'audiopreprocessor');
|
||||||
|
const mediaStream = audioContext.createMediaStreamSource(stream);
|
||||||
|
|
||||||
|
mediaStream.connect(preNode);
|
||||||
|
preNode.connect(audioContext.destination);
|
||||||
|
preNode.port.onmessage = (event) => {
|
||||||
|
const data = event.data;
|
||||||
|
|
||||||
|
|
||||||
|
const audio16k = data; // Float32Array @ 16 kHz
|
||||||
|
|
||||||
|
if (socket && socket.readyState === WebSocket.OPEN && isServerReady) {
|
||||||
|
socket.send(audio16k);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test if we can hear audio (this will help verify the audio path)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error initializing AudioWorklet:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupAudio() {
|
||||||
|
|
||||||
|
if (preNode) {
|
||||||
|
preNode.port.onmessage = null;
|
||||||
|
preNode.disconnect();
|
||||||
|
preNode = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (audioContext) {
|
||||||
|
audioContext.close();
|
||||||
|
audioContext = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentStream) {
|
||||||
|
currentStream.getTracks().forEach(track => {
|
||||||
|
track.stop();
|
||||||
|
console.log("Stopped track:", track.kind);
|
||||||
|
});
|
||||||
|
currentStream = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts recording audio from the captured tab.
|
* Starts recording audio from the captured tab.
|
||||||
* @param {Object} option - The options object containing the currentTabId.
|
* @param {Object} option - The options object containing the currentTabId.
|
||||||
*/
|
*/
|
||||||
async function startRecord(option) {
|
async function startRecord(option) {
|
||||||
|
currentOptions = option;
|
||||||
const stream = await captureTabAudio();
|
const stream = await captureTabAudio();
|
||||||
const uuid = generateUUID();
|
const uuid = generateUUID();
|
||||||
|
|
||||||
if (stream) {
|
if (stream) {
|
||||||
// call when the stream inactive
|
currentStream = stream;
|
||||||
stream.oninactive = () => {
|
stream.oninactive = () => {
|
||||||
|
cleanupAudio();
|
||||||
window.close();
|
window.close();
|
||||||
};
|
};
|
||||||
const socket = new WebSocket(`ws://${option.host}:${option.port}/`);
|
|
||||||
let isServerReady = false;
|
try {
|
||||||
|
await initAudioWorklet(stream);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to initialize AudioWorklet:", error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket = new WebSocket(`ws://${option.host}:${option.port}/`);
|
||||||
|
isServerReady = false;
|
||||||
let language = option.language;
|
let language = option.language;
|
||||||
socket.onopen = function(e) {
|
|
||||||
|
socket.onopen = function(e) {
|
||||||
socket.send(
|
socket.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
uid: uuid,
|
uid: uuid,
|
||||||
@@ -129,7 +169,6 @@ async function startRecord(option) {
|
|||||||
language = data["language"];
|
language = data["language"];
|
||||||
|
|
||||||
// send message to popup.js to update dropdown
|
// send message to popup.js to update dropdown
|
||||||
// console.log(language);
|
|
||||||
chrome.runtime.sendMessage({
|
chrome.runtime.sendMessage({
|
||||||
action: "updateSelectedLanguage",
|
action: "updateSelectedLanguage",
|
||||||
detectedLanguage: language,
|
detectedLanguage: language,
|
||||||
@@ -139,43 +178,33 @@ async function startRecord(option) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (data["message"] === "DISCONNECT"){
|
if (data["message"] === "DISCONNECT"){
|
||||||
chrome.runtime.sendMessage({ action: "toggleCaptureButtons", data: false })
|
chrome.runtime.sendMessage({ action: "toggleCaptureButtons", data: false, saveCaptions: option.saveCaptions });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res = await sendMessageToTab(option.currentTabId, {
|
const res = await sendMessageToTab(option.currentTabId, {
|
||||||
type: "transcript",
|
type: "transcript",
|
||||||
data: event.data,
|
data: {
|
||||||
|
data: event.data,
|
||||||
|
saveCaptions: option.saveCaptions,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
socket.onclose = () => {
|
||||||
const audioDataCache = [];
|
cleanupAudio();
|
||||||
const context = new AudioContext();
|
};
|
||||||
const mediaStream = context.createMediaStreamSource(stream);
|
|
||||||
const recorder = context.createScriptProcessor(4096, 1, 1);
|
socket.onerror = (error) => {
|
||||||
|
cleanupAudio();
|
||||||
recorder.onaudioprocess = async (event) => {
|
|
||||||
if (!context || !isServerReady) return;
|
|
||||||
|
|
||||||
const inputData = event.inputBuffer.getChannelData(0);
|
|
||||||
const audioData16kHz = resampleTo16kHZ(inputData, context.sampleRate);
|
|
||||||
|
|
||||||
audioDataCache.push(inputData);
|
|
||||||
|
|
||||||
socket.send(audioData16kHz);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Prevent page mute
|
|
||||||
mediaStream.connect(recorder);
|
|
||||||
recorder.connect(context.destination);
|
|
||||||
mediaStream.connect(context.destination);
|
|
||||||
// }
|
|
||||||
} else {
|
} else {
|
||||||
window.close();
|
window.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listener for incoming messages from the extension's background script.
|
* Listener for incoming messages from the extension's background script.
|
||||||
* @param {Object} request - The message request object.
|
* @param {Object} request - The message request object.
|
||||||
|
|||||||
@@ -19,6 +19,10 @@
|
|||||||
<input type="checkbox" id="useVadCheckbox">
|
<input type="checkbox" id="useVadCheckbox">
|
||||||
<label for="useVadCheckbox">Use Voice Activity Detection</label>
|
<label for="useVadCheckbox">Use Voice Activity Detection</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="checkbox-container">
|
||||||
|
<input type="checkbox" id="saveCaptionsCheckbox">
|
||||||
|
<label for="saveCaptions">Download SRT file at Stop Capture</label>
|
||||||
|
</div>
|
||||||
<div class="dropdown-container">
|
<div class="dropdown-container">
|
||||||
<label for="languageDropdown">Select Language:</label>
|
<label for="languageDropdown">Select Language:</label>
|
||||||
<select id="languageDropdown">
|
<select id="languageDropdown">
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
|
|
||||||
const useServerCheckbox = document.getElementById("useServerCheckbox");
|
const useServerCheckbox = document.getElementById("useServerCheckbox");
|
||||||
const useVadCheckbox = document.getElementById("useVadCheckbox");
|
const useVadCheckbox = document.getElementById("useVadCheckbox");
|
||||||
|
const saveCaptionsCheckbox = document.getElementById("saveCaptionsCheckbox");
|
||||||
const languageDropdown = document.getElementById('languageDropdown');
|
const languageDropdown = document.getElementById('languageDropdown');
|
||||||
const taskDropdown = document.getElementById('taskDropdown');
|
const taskDropdown = document.getElementById('taskDropdown');
|
||||||
const modelSizeDropdown = document.getElementById('modelSizeDropdown');
|
const modelSizeDropdown = document.getElementById('modelSizeDropdown');
|
||||||
@@ -38,6 +39,12 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
chrome.storage.local.get("saveCaptionsState", ({ saveCaptionsState }) => {
|
||||||
|
if (saveCaptionsState !== undefined) {
|
||||||
|
saveCaptionsCheckbox.checked = saveCaptionsState;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
chrome.storage.local.get("selectedLanguage", ({ selectedLanguage: storedLanguage }) => {
|
chrome.storage.local.get("selectedLanguage", ({ selectedLanguage: storedLanguage }) => {
|
||||||
if (storedLanguage !== undefined) {
|
if (storedLanguage !== undefined) {
|
||||||
languageDropdown.value = storedLanguage;
|
languageDropdown.value = storedLanguage;
|
||||||
@@ -88,6 +95,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
task: selectedTask,
|
task: selectedTask,
|
||||||
modelSize: selectedModelSize,
|
modelSize: selectedModelSize,
|
||||||
useVad: useVadCheckbox.checked,
|
useVad: useVadCheckbox.checked,
|
||||||
|
saveCaptions: saveCaptionsCheckbox.checked,
|
||||||
}, () => {
|
}, () => {
|
||||||
// Update capturing state in storage and toggle the buttons
|
// Update capturing state in storage and toggle the buttons
|
||||||
chrome.storage.local.set({ capturingState: { isCapturing: true } }, () => {
|
chrome.storage.local.set({ capturingState: { isCapturing: true } }, () => {
|
||||||
@@ -105,7 +113,11 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send a message to the background script to stop capturing
|
// Send a message to the background script to stop capturing
|
||||||
chrome.runtime.sendMessage({ action: "stopCapture" }, () => {
|
chrome.runtime.sendMessage(
|
||||||
|
{
|
||||||
|
action: "stopCapture",
|
||||||
|
saveCaptions: saveCaptionsCheckbox.checked,
|
||||||
|
}, () => {
|
||||||
// Update capturing state in storage and toggle the buttons
|
// Update capturing state in storage and toggle the buttons
|
||||||
chrome.storage.local.set({ capturingState: { isCapturing: false } }, () => {
|
chrome.storage.local.set({ capturingState: { isCapturing: false } }, () => {
|
||||||
toggleCaptureButtons(false);
|
toggleCaptureButtons(false);
|
||||||
@@ -128,6 +140,7 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
stopButton.disabled = !isCapturing;
|
stopButton.disabled = !isCapturing;
|
||||||
useServerCheckbox.disabled = isCapturing;
|
useServerCheckbox.disabled = isCapturing;
|
||||||
useVadCheckbox.disabled = isCapturing;
|
useVadCheckbox.disabled = isCapturing;
|
||||||
|
saveCaptionsCheckbox.disabled = isCapturing;
|
||||||
modelSizeDropdown.disabled = isCapturing;
|
modelSizeDropdown.disabled = isCapturing;
|
||||||
languageDropdown.disabled = isCapturing;
|
languageDropdown.disabled = isCapturing;
|
||||||
taskDropdown.disabled = isCapturing;
|
taskDropdown.disabled = isCapturing;
|
||||||
@@ -146,6 +159,11 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
chrome.storage.local.set({ useVadState });
|
chrome.storage.local.set({ useVadState });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
saveCaptionsCheckbox.addEventListener("change", () => {
|
||||||
|
const saveCaptionsState = saveCaptionsCheckbox.checked;
|
||||||
|
chrome.storage.local.set({ saveCaptionsState });
|
||||||
|
});
|
||||||
|
|
||||||
languageDropdown.addEventListener('change', function() {
|
languageDropdown.addEventListener('change', function() {
|
||||||
if (languageDropdown.value === "") {
|
if (languageDropdown.value === "") {
|
||||||
selectedLanguage = null;
|
selectedLanguage = null;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ To capture the audio in the current tab, we used the chrome `tabCapture` API to
|
|||||||
When using the Audio Transcription extension, you have the following options:
|
When using the Audio Transcription extension, you have the following options:
|
||||||
- **Use Collabora Server**: We provide a demo server which runs the whisper small model.
|
- **Use Collabora Server**: We provide a demo server which runs the whisper small model.
|
||||||
- **Language**: Select the target language for transcription or translation. You can choose from a variety of languages supported by OpenAI-whisper.
|
- **Language**: Select the target language for transcription or translation. You can choose from a variety of languages supported by OpenAI-whisper.
|
||||||
|
- **Download SRT file at Stop Capture**: Select if you want to download the srt file for the session at stop capture.
|
||||||
- **Task:** Choose the specific task to perform on the audio. You can select either "transcribe" for transcription or "translate" to translate the audio to English.
|
- **Task:** Choose the specific task to perform on the audio. You can select either "transcribe" for transcription or "translate" to translate the audio to English.
|
||||||
- **Model Size**: Select the whisper model size to run the server with.
|
- **Model Size**: Select the whisper model size to run the server with.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
class AudioPreProcessor extends AudioWorkletProcessor {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.sampleRate = sampleRate || 48000;
|
||||||
|
this.targetSampleRate = 16000;
|
||||||
|
this.inputSamplesNeeded = this.sampleRate * 0.5;
|
||||||
|
this.inputBuffer = new Float32Array(this.inputSamplesNeeded);
|
||||||
|
this.inputWriteOffset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
process(inputs, outputs) {
|
||||||
|
const input = inputs[0];
|
||||||
|
const output = outputs[0];
|
||||||
|
if (!input || input.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (let channel = 0; channel < Math.min(input.length, output.length); channel++) {
|
||||||
|
if (input[channel] && output[channel]) {
|
||||||
|
output[channel].set(input[channel]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let monoInput;
|
||||||
|
if (input.length === 1) {
|
||||||
|
monoInput = input[0];
|
||||||
|
} else if (input.length > 1) {
|
||||||
|
monoInput = new Float32Array(input[0].length);
|
||||||
|
for (let channel = 0; channel < input.length; channel++) {
|
||||||
|
monoInput.set(input[channel], 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!monoInput) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let inputOffset = 0;
|
||||||
|
while (inputOffset < monoInput.length) {
|
||||||
|
const remainingBuffer = this.inputSamplesNeeded - this.inputWriteOffset;
|
||||||
|
const toCopy = Math.min(remainingBuffer, monoInput.length - inputOffset);
|
||||||
|
this.inputBuffer.set(monoInput.subarray(inputOffset, inputOffset + toCopy), this.inputWriteOffset);
|
||||||
|
|
||||||
|
this.inputWriteOffset += toCopy;
|
||||||
|
inputOffset += toCopy;
|
||||||
|
|
||||||
|
if (this.inputWriteOffset === this.inputSamplesNeeded) {
|
||||||
|
const downsampled = this.downsampleTo16kHz(this.inputBuffer);
|
||||||
|
this.port.postMessage(downsampled);
|
||||||
|
|
||||||
|
this.inputWriteOffset = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
downsampleTo16kHz(inputBuffer) {
|
||||||
|
const ratio = this.sampleRate / this.targetSampleRate;
|
||||||
|
const length = Math.floor(inputBuffer.length / ratio);
|
||||||
|
const result = new Float32Array(length);
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
const idx = Math.floor(i * ratio);
|
||||||
|
result[i] = inputBuffer[idx];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerProcessor('audiopreprocessor', AudioPreProcessor);
|
||||||
|
|
||||||
@@ -1,144 +1,162 @@
|
|||||||
let socket = null;
|
let socket = null;
|
||||||
let isCapturing = false;
|
let isCapturing = false;
|
||||||
let mediaStream = null;
|
|
||||||
let audioContext = null;
|
let audioContext = null;
|
||||||
let scriptProcessor = null;
|
|
||||||
let language = null;
|
let language = null;
|
||||||
|
|
||||||
let isPaused = false;
|
let isPaused = false;
|
||||||
|
let preNode = null;
|
||||||
|
let allSegments = [];
|
||||||
|
let lastIncompleteSegment = null;
|
||||||
|
|
||||||
const mediaElements = document.querySelectorAll('video, audio');
|
function formatTime(seconds) {
|
||||||
mediaElements.forEach((mediaElement) => {
|
const date = new Date(seconds * 1000);
|
||||||
mediaElement.addEventListener('play', handlePlaybackStateChange);
|
const hh = String(date.getUTCHours()).padStart(2, '0');
|
||||||
mediaElement.addEventListener('pause', handlePlaybackStateChange);
|
const mm = String(date.getUTCMinutes()).padStart(2, '0');
|
||||||
});
|
const ss = String(date.getUTCSeconds()).padStart(2, '0');
|
||||||
|
const mmm = String(date.getUTCMilliseconds()).padStart(3, '0');
|
||||||
|
return `${hh}:${mm}:${ss},${mmm}`;
|
||||||
function handlePlaybackStateChange(event) {
|
|
||||||
isPaused = event.target.paused;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function generateSRT() {
|
||||||
|
return allSegments
|
||||||
|
.map((seg, i) => {
|
||||||
|
const start = formatTime(seg.start);
|
||||||
|
const end = formatTime(seg.end);
|
||||||
|
const text = seg.text.trim().replace(/[\r\n]+/g, ' ');
|
||||||
|
return `${i + 1}\n${start} --> ${end}\n${text}`;
|
||||||
|
})
|
||||||
|
.join('\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadSRT() {
|
||||||
|
const srtBlob = new Blob([generateSRT()], { type: 'text/srt;charset=utf-8' });
|
||||||
|
const url = URL.createObjectURL(srtBlob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'captions.srt';
|
||||||
|
a.style.display = 'none';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
document.body.removeChild(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function generateUUID() {
|
function generateUUID() {
|
||||||
let dt = new Date().getTime();
|
let dt = new Date().getTime();
|
||||||
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
||||||
const r = (dt + Math.random() * 16) % 16 | 0;
|
const r = (dt + Math.random() * 16) % 16 | 0;
|
||||||
dt = Math.floor(dt / 16);
|
dt = Math.floor(dt / 16);
|
||||||
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
||||||
});
|
});
|
||||||
return uuid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
document.querySelectorAll('video, audio').forEach(el => {
|
||||||
* Resamples the audio data to a target sample rate of 16kHz.
|
el.addEventListener('play', () => { isPaused = false; });
|
||||||
* @param {Array|ArrayBuffer|TypedArray} audioData - The input audio data.
|
el.addEventListener('pause', () => { isPaused = true; });
|
||||||
* @param {number} [origSampleRate=44100] - The original sample rate of the audio data.
|
});
|
||||||
* @returns {Float32Array} The resampled audio data at 16kHz.
|
|
||||||
*/
|
|
||||||
function resampleTo16kHZ(audioData, origSampleRate = 44100) {
|
|
||||||
// Convert the audio data to a Float32Array
|
|
||||||
const data = new Float32Array(audioData);
|
|
||||||
|
|
||||||
// Calculate the desired length of the resampled data
|
|
||||||
const targetLength = Math.round(data.length * (16000 / origSampleRate));
|
|
||||||
|
|
||||||
// Create a new Float32Array for the resampled data
|
function setupMessageHandler() {
|
||||||
const resampledData = new Float32Array(targetLength);
|
if (preNode) {
|
||||||
|
preNode.port.onmessage = e => {
|
||||||
|
const audio16k = e.data;
|
||||||
|
if (isCapturing && socket && socket.readyState === WebSocket.OPEN && !isPaused) {
|
||||||
|
socket.send(audio16k);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate the spring factor and initialize the first and last values
|
|
||||||
const springFactor = (data.length - 1) / (targetLength - 1);
|
|
||||||
resampledData[0] = data[0];
|
|
||||||
resampledData[targetLength - 1] = data[data.length - 1];
|
|
||||||
|
|
||||||
// Resample the audio data
|
const WORKLET_URL = browser.runtime.getURL('audiopreprocessor.js');
|
||||||
for (let i = 1; i < targetLength - 1; i++) {
|
|
||||||
const index = i * springFactor;
|
async function initAudioWorklet() {
|
||||||
const leftIndex = Math.floor(index).toFixed();
|
if (audioContext && preNode) {
|
||||||
const rightIndex = Math.ceil(index).toFixed();
|
setupMessageHandler();
|
||||||
const fraction = index - leftIndex;
|
return;
|
||||||
resampledData[i] = data[leftIndex] + (data[rightIndex] - data[leftIndex]) * fraction;
|
}
|
||||||
|
audioContext = new AudioContext();
|
||||||
|
await audioContext.audioWorklet.addModule(WORKLET_URL);
|
||||||
|
|
||||||
|
preNode = new AudioWorkletNode(audioContext, 'audiopreprocessor');
|
||||||
|
document.querySelectorAll('audio, video').forEach(el => {
|
||||||
|
let src;
|
||||||
|
try {
|
||||||
|
src = audioContext.createMediaElementSource(el);
|
||||||
|
} catch(e) {
|
||||||
|
console.warn('Could not create MediaElementSource for', el, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
src.connect(preNode);
|
||||||
|
src.connect(audioContext.destination);
|
||||||
|
});
|
||||||
|
|
||||||
|
preNode.connect(audioContext.destination);
|
||||||
|
|
||||||
|
setupMessageHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function startRecording(data) {
|
||||||
|
if (!audioContext) {
|
||||||
|
await initAudioWorklet();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the resampled data
|
const uid = generateUUID();
|
||||||
return resampledData;
|
socket = new WebSocket(`ws://${data.host}:${data.port}/`);
|
||||||
|
language = data.language;
|
||||||
|
|
||||||
|
socket.onopen = () => {
|
||||||
|
socket.send(JSON.stringify({
|
||||||
|
uid,
|
||||||
|
language: data.language,
|
||||||
|
task: data.task,
|
||||||
|
model: data.modelSize,
|
||||||
|
use_vad: data.useVad
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
let serverReady = false;
|
||||||
|
socket.onmessage = async event => {
|
||||||
|
const msg = JSON.parse(event.data);
|
||||||
|
if (msg.uid !== uid) return;
|
||||||
|
|
||||||
|
if (msg.status === 'WAIT') {
|
||||||
|
await browser.runtime.sendMessage({ action: 'showPopup', data: msg.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!serverReady && msg.message === 'SERVER_READY') {
|
||||||
|
serverReady = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!language && msg.language) {
|
||||||
|
language = msg.language;
|
||||||
|
await browser.runtime.sendMessage({ action: 'updateSelectedLanguage', data: language });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.message === 'DISCONNECT') {
|
||||||
|
await browser.runtime.sendMessage({ action: 'toggleCaptureButtons' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (msg.segments) {
|
||||||
|
await browser.runtime.sendMessage({ action: 'transcript', data: {data: event.data, saveCaption: data.saveCaption} });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
isCapturing = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function startRecording(data) {
|
function stopRecording() {
|
||||||
socket = new WebSocket(`ws://${data.host}:${data.port}/`);
|
isCapturing = false;
|
||||||
language = data.language;
|
if (socket) {
|
||||||
|
socket.close();
|
||||||
|
socket = null;
|
||||||
|
}
|
||||||
|
|
||||||
const uuid = generateUUID();
|
remove_element();
|
||||||
socket.onopen = function(e) {
|
|
||||||
socket.send(
|
|
||||||
JSON.stringify({
|
|
||||||
uid: uuid,
|
|
||||||
language: data.language,
|
|
||||||
task: data.task,
|
|
||||||
model: data.modelSize,
|
|
||||||
use_vad: data.useVad
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
let isServerReady = false;
|
|
||||||
socket.onmessage = async (event) => {
|
|
||||||
const data = JSON.parse(event.data);
|
|
||||||
if (data["uid"] !== uuid)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (data["status"] === "WAIT"){
|
|
||||||
await browser.runtime.sendMessage({ action: "showPopup", data: data["message"] })
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isServerReady && data["message"] === "SERVER_READY"){
|
|
||||||
isServerReady = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (language === null ){
|
|
||||||
language = data["language"];
|
|
||||||
await browser.runtime.sendMessage({ action: "updateSelectedLanguage", data: language })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data["message"] === "DISCONNECT"){
|
|
||||||
await browser.runtime.sendMessage({ action: "toggleCaptureButtons", data: false })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await browser.runtime.sendMessage({ action: "transcript", data: event.data })
|
|
||||||
.catch(function(error) {
|
|
||||||
console.error("Error sending message:", error);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Access the audio stream from the current tab
|
|
||||||
navigator.mediaDevices.getUserMedia({ audio: true })
|
|
||||||
.then(function(stream) {
|
|
||||||
// Create a new MediaRecorder instance
|
|
||||||
const audioDataCache = [];
|
|
||||||
audioContext = new AudioContext();
|
|
||||||
mediaStream = audioContext.createMediaStreamSource(stream);
|
|
||||||
recorder = audioContext.createScriptProcessor(4096, 1, 1);
|
|
||||||
|
|
||||||
recorder.onaudioprocess = async (event) => {
|
|
||||||
if (!audioContext || !isCapturing || !isServerReady || isPaused) return;
|
|
||||||
|
|
||||||
const inputData = event.inputBuffer.getChannelData(0);
|
|
||||||
const audioData16kHz = resampleTo16kHZ(inputData, audioContext.sampleRate);
|
|
||||||
|
|
||||||
audioDataCache.push(inputData);
|
|
||||||
|
|
||||||
socket.send(audioData16kHz);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Prevent page mute
|
|
||||||
mediaStream.connect(recorder);
|
|
||||||
recorder.connect(audioContext.destination);
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var elem_container = null;
|
var elem_container = null;
|
||||||
var elem_text = null;
|
var elem_text = null;
|
||||||
|
|
||||||
@@ -308,6 +326,8 @@ function remove_element() {
|
|||||||
|
|
||||||
browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||||
const { action, data } = request;
|
const { action, data } = request;
|
||||||
|
const saveCaption = data.saveCaption || false;
|
||||||
|
|
||||||
if (action === "startCapture") {
|
if (action === "startCapture") {
|
||||||
isCapturing = true;
|
isCapturing = true;
|
||||||
startRecording(data);
|
startRecording(data);
|
||||||
@@ -318,12 +338,20 @@ browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|||||||
socket.close();
|
socket.close();
|
||||||
socket = null;
|
socket = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audioContext) {
|
|
||||||
audioContext.close();
|
if (saveCaption === true) {
|
||||||
audioContext = null;
|
if (lastIncompleteSegment && lastIncompleteSegment.text && lastIncompleteSegment.text.trim() !== "") {
|
||||||
mediaStream = null;
|
if (allSegments.length === 0 || parseFloat(lastIncompleteSegment.start) >= parseFloat(allSegments[allSegments.length - 1].end)) {
|
||||||
recorder = null;
|
allSegments.push({
|
||||||
|
start: lastIncompleteSegment.start,
|
||||||
|
end: lastIncompleteSegment.end,
|
||||||
|
text: lastIncompleteSegment.text
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadSRT();
|
||||||
}
|
}
|
||||||
|
|
||||||
remove_element();
|
remove_element();
|
||||||
@@ -337,8 +365,25 @@ browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|||||||
} else if (action === "show_transcript"){
|
} else if (action === "show_transcript"){
|
||||||
if (!isCapturing) return;
|
if (!isCapturing) return;
|
||||||
init_element();
|
init_element();
|
||||||
message = JSON.parse(data);
|
message = JSON.parse(data.data);
|
||||||
message = message["segments"];
|
message = message["segments"];
|
||||||
|
|
||||||
|
if (saveCaption === true) {
|
||||||
|
message.forEach(seg => {
|
||||||
|
if (seg.completed === true &&
|
||||||
|
(allSegments.length === 0 || parseFloat(seg.start) >= parseFloat(allSegments[allSegments.length - 1].end))) {
|
||||||
|
allSegments.push({
|
||||||
|
start: seg.start,
|
||||||
|
end: seg.end,
|
||||||
|
text: seg.text
|
||||||
|
});
|
||||||
|
|
||||||
|
lastIncompleteSegment = null;
|
||||||
|
} else if (seg.completed !== true) {
|
||||||
|
lastIncompleteSegment = seg;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var text = '';
|
var text = '';
|
||||||
for (var i = 0; i < message.length; i++) {
|
for (var i = 0; i < message.length; i++) {
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
"activeTab",
|
"activeTab",
|
||||||
"<all_urls>"
|
"<all_urls>"
|
||||||
],
|
],
|
||||||
|
"web_accessible_resources": [
|
||||||
|
"audiopreprocessor.js"
|
||||||
|
],
|
||||||
"background": {
|
"background": {
|
||||||
"scripts": ["background.js"],
|
"scripts": ["background.js"],
|
||||||
"persistent": false
|
"persistent": false
|
||||||
|
|||||||
@@ -19,6 +19,10 @@
|
|||||||
<input type="checkbox" id="useVadCheckbox">
|
<input type="checkbox" id="useVadCheckbox">
|
||||||
<label for="useVadCheckbox">Use Voice Activity Detection</label>
|
<label for="useVadCheckbox">Use Voice Activity Detection</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="checkbox-container">
|
||||||
|
<input type="checkbox" id="saveCaptionCheckbox">
|
||||||
|
<label for="saveCaption">Download SRT file at Stop Capture</label>
|
||||||
|
</div>
|
||||||
<textarea id="waitTextBox" style="display: none;"></textarea>
|
<textarea id="waitTextBox" style="display: none;"></textarea>
|
||||||
<div class="dropdown-container">
|
<div class="dropdown-container">
|
||||||
<label for="languageDropdown">Select Language:</label>
|
<label for="languageDropdown">Select Language:</label>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ document.addEventListener("DOMContentLoaded", function() {
|
|||||||
|
|
||||||
const useServerCheckbox = document.getElementById("useServerCheckbox");
|
const useServerCheckbox = document.getElementById("useServerCheckbox");
|
||||||
const useVadCheckbox = document.getElementById("useVadCheckbox");
|
const useVadCheckbox = document.getElementById("useVadCheckbox");
|
||||||
|
const saveCaptionCheckbox = document.getElementById("saveCaptionCheckbox");
|
||||||
const languageDropdown = document.getElementById('languageDropdown');
|
const languageDropdown = document.getElementById('languageDropdown');
|
||||||
const taskDropdown = document.getElementById('taskDropdown');
|
const taskDropdown = document.getElementById('taskDropdown');
|
||||||
const modelSizeDropdown = document.getElementById('modelSizeDropdown');
|
const modelSizeDropdown = document.getElementById('modelSizeDropdown');
|
||||||
@@ -41,6 +42,12 @@ document.addEventListener("DOMContentLoaded", function() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
browser.storage.local.get("saveCaptionState", ({ saveCaptionState }) => {
|
||||||
|
if (saveCaptionState !== undefined) {
|
||||||
|
saveCaptionCheckbox.checked = saveCaptionState;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
browser.storage.local.get("selectedLanguage", ({ selectedLanguage: storedLanguage }) => {
|
browser.storage.local.get("selectedLanguage", ({ selectedLanguage: storedLanguage }) => {
|
||||||
if (storedLanguage !== undefined) {
|
if (storedLanguage !== undefined) {
|
||||||
languageDropdown.value = storedLanguage;
|
languageDropdown.value = storedLanguage;
|
||||||
@@ -85,6 +92,7 @@ document.addEventListener("DOMContentLoaded", function() {
|
|||||||
task: selectedTask,
|
task: selectedTask,
|
||||||
modelSize: selectedModelSize,
|
modelSize: selectedModelSize,
|
||||||
useVad: useVadCheckbox.checked,
|
useVad: useVadCheckbox.checked,
|
||||||
|
saveCaption: saveCaptionCheckbox.checked,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
toggleCaptureButtons(true);
|
toggleCaptureButtons(true);
|
||||||
@@ -101,7 +109,7 @@ document.addEventListener("DOMContentLoaded", function() {
|
|||||||
stopButton.addEventListener("click", function() {
|
stopButton.addEventListener("click", function() {
|
||||||
browser.tabs.query({ active: true, currentWindow: true })
|
browser.tabs.query({ active: true, currentWindow: true })
|
||||||
.then(function(tabs) {
|
.then(function(tabs) {
|
||||||
browser.tabs.sendMessage(tabs[0].id, { action: "stopCapture" })
|
browser.tabs.sendMessage(tabs[0].id, { action: "stopCapture", data: {saveCaption: saveCaptionCheckbox.checked, } })
|
||||||
.then(function(response) {
|
.then(function(response) {
|
||||||
toggleCaptureButtons(false);
|
toggleCaptureButtons(false);
|
||||||
browser.storage.local.set({ capturingState: { isCapturing: false } })
|
browser.storage.local.set({ capturingState: { isCapturing: false } })
|
||||||
@@ -124,6 +132,7 @@ document.addEventListener("DOMContentLoaded", function() {
|
|||||||
stopButton.disabled = !isCapturing;
|
stopButton.disabled = !isCapturing;
|
||||||
useServerCheckbox.disabled = isCapturing;
|
useServerCheckbox.disabled = isCapturing;
|
||||||
useVadCheckbox.disabled = isCapturing;
|
useVadCheckbox.disabled = isCapturing;
|
||||||
|
saveCaptionCheckbox.disabled = isCapturing;
|
||||||
modelSizeDropdown.disabled = isCapturing;
|
modelSizeDropdown.disabled = isCapturing;
|
||||||
languageDropdown.disabled = isCapturing;
|
languageDropdown.disabled = isCapturing;
|
||||||
taskDropdown.disabled = isCapturing;
|
taskDropdown.disabled = isCapturing;
|
||||||
@@ -142,6 +151,11 @@ document.addEventListener("DOMContentLoaded", function() {
|
|||||||
browser.storage.local.set({ useVadState });
|
browser.storage.local.set({ useVadState });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
saveCaptionCheckbox.addEventListener("change", () => {
|
||||||
|
const saveCaptionState = saveCaptionCheckbox.checked;
|
||||||
|
browser.storage.local.set({ saveCaptionState });
|
||||||
|
});
|
||||||
|
|
||||||
languageDropdown.addEventListener('change', function() {
|
languageDropdown.addEventListener('change', function() {
|
||||||
if (languageDropdown.value === "") {
|
if (languageDropdown.value === "") {
|
||||||
selectedLanguage = null;
|
selectedLanguage = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user