Merge remote-tracking branch 'upstream/main' into add_default_server
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
# Audio Transcription
|
||||
|
||||
Audio Transcription is a Chrome extension that allows users to capture any audio playing on the current tab and transcribe it using OpenAI-whisper in real time. Users will have the option to do voice activity detection as well to not send audio to server when there is no speech.
|
||||
|
||||
We use OpenAI-whisper model to process the audio continuously and send the transcription back to the client. We apply a few optimizations on top of OpenAI's implementation to improve performance and run it faster in a real-time manner. To this end, we used [faster-whisper](https://github.com/guillaumekln/faster-whisper) which is 4x faster than OpenAI's implementation.
|
||||
|
||||
## Loading the Extension
|
||||
- Open the Google Chrome browser.
|
||||
- Type chrome://extensions in the address bar and press Enter.
|
||||
- Enable the Developer mode toggle switch located in the top right corner.
|
||||
- Clone this repository
|
||||
- Click the Load unpacked button.
|
||||
- Browse to the location where you cloned the repository files and select the ```Audio Transcription``` folder.
|
||||
- The extension should now be loaded and visible on the extensions page.
|
||||
|
||||
|
||||
## Real time transcription with OpenAI-whisper
|
||||
This Chrome extension allows you to send audio from your browser to a server for transcribing the audio in real time. It can also incorporate voice activity detection on the client side to detect when speech is present, and it continuously receives transcriptions of the spoken content from the server. You can select from the options menu if you want to run the speech recognition.
|
||||
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Capturing Audio
|
||||
To capture the audio in the current tab, we used the chrome `tabCapture` API to obtain a `MediaStream` object of the current tab.
|
||||
|
||||
### Getting Started
|
||||
- Make sure the transcription server is running properly. To know more about how to start the server, see the [documentation here](https://github.com/collabora/whisper-live).
|
||||
- Just click on the Chrome Extension which should show 2 options
|
||||
- **Start Capture** : Starts capturing the audio in the current tab and sends the captured audio to the server for transcription. This also creates an element to show the transcriptions recieved from the server on the current tab.
|
||||
- **Stop Capture** - Stops capturing the audio.
|
||||
|
||||
|
||||
## Limitations
|
||||
This extension requires an internet connection to stream audio and receive transcriptions. The accuracy of the transcriptions may vary depending on the audio quality and the performance of the server-side transcription service. The extension may consume additional system resources while running, especially when streaming audio.
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* Removes a tab with the specified tab ID in Google Chrome.
|
||||
* @param {number} tabId - The ID of the tab to be removed.
|
||||
* @returns {Promise<void>} A promise that resolves when the tab is successfully removed or fails to remove.
|
||||
*/
|
||||
function removeChromeTab(tabId) {
|
||||
return new Promise((resolve) => {
|
||||
chrome.tabs.remove(tabId)
|
||||
.then(resolve)
|
||||
.catch(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Executes a script file in a specific tab in Google Chrome.
|
||||
* @param {number} tabId - The ID of the tab where the script should be executed.
|
||||
* @param {string} file - The file path or URL of the script to be executed.
|
||||
* @returns {Promise<void>} A promise that resolves when the script is successfully executed or fails to execute.
|
||||
*/
|
||||
function executeScriptInTab(tabId, file) {
|
||||
return new Promise((resolve) => {
|
||||
chrome.scripting.executeScript(
|
||||
{
|
||||
target: { tabId },
|
||||
files: [file],
|
||||
}, () => {
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Opens the options page of the Chrome extension in a new pinned tab.
|
||||
* @returns {Promise<chrome.tabs.Tab>} A promise that resolves with the created tab object.
|
||||
*/
|
||||
function openExtensionOptions() {
|
||||
return new Promise((resolve) => {
|
||||
chrome.tabs.create(
|
||||
{
|
||||
pinned: true,
|
||||
active: false,
|
||||
url: `chrome-extension://${chrome.runtime.id}/options.html`,
|
||||
},
|
||||
(tab) => {
|
||||
resolve(tab);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves the value associated with the specified key from the local storage in Google Chrome.
|
||||
* @param {string} key - The key of the value to retrieve from the local storage.
|
||||
* @returns {Promise<any>} A promise that resolves with the retrieved value from the local storage.
|
||||
*/
|
||||
function getLocalStorageValue(key) {
|
||||
return new Promise((resolve) => {
|
||||
chrome.storage.local.get([key], (result) => {
|
||||
resolve(result[key]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sends a message to a specific tab in Google Chrome.
|
||||
* @param {number} tabId - The ID of the tab to send the message to.
|
||||
* @param {any} data - The data to be sent as the message.
|
||||
* @returns {Promise<any>} A promise that resolves with the response from the tab.
|
||||
*/
|
||||
function sendMessageToTab(tabId, data) {
|
||||
return new Promise((resolve) => {
|
||||
chrome.tabs.sendMessage(tabId, data, (response) => {
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delays the execution for a specified duration.
|
||||
* @param {number} ms - The duration to sleep in milliseconds (default: 0).
|
||||
* @returns {Promise<void>} A promise that resolves after the specified duration.
|
||||
*/
|
||||
function delayExecution(ms = 0) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets a value associated with the specified key in the local storage of Google Chrome.
|
||||
* @param {string} key - The key to set in the local storage.
|
||||
* @param {any} value - The value to associate with the key in the local storage.
|
||||
* @returns {Promise<any>} A promise that resolves with the value that was set in the local storage.
|
||||
*/
|
||||
function setLocalStorageValue(key, value) {
|
||||
return new Promise((resolve) => {
|
||||
chrome.storage.local.set(
|
||||
{
|
||||
[key]: value,
|
||||
}, () => {
|
||||
resolve(value);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Retrieves the tab object with the specified tabId.
|
||||
* @param {number} tabId - The ID of the tab to retrieve.
|
||||
* @returns {Promise<object>} - A Promise that resolves to the tab object.
|
||||
*/
|
||||
async function getTab(tabId) {
|
||||
return new Promise((resolve) => {
|
||||
chrome.tabs.get(tabId, (tab) => {
|
||||
resolve(tab);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Starts the capture process for the specified tab.
|
||||
* @param {number} tabId - The ID of the tab to start capturing.
|
||||
* @returns {Promise<void>} - A Promise that resolves when the capture process is started successfully.
|
||||
*/
|
||||
async function startCapture(options) {
|
||||
const { tabId } = options;
|
||||
const optionTabId = await getLocalStorageValue("optionTabId");
|
||||
if (optionTabId) {
|
||||
await removeChromeTab(optionTabId);
|
||||
}
|
||||
|
||||
try {
|
||||
const currentTab = await getTab(tabId);
|
||||
if (currentTab.audible) {
|
||||
await setLocalStorageValue("currentTabId", currentTab.id);
|
||||
await executeScriptInTab(currentTab.id, "content.js");
|
||||
await delayExecution(500);
|
||||
|
||||
const optionTab = await openExtensionOptions();
|
||||
|
||||
await setLocalStorageValue("optionTabId", optionTab.id);
|
||||
await delayExecution(500);
|
||||
|
||||
await sendMessageToTab(optionTab.id, {
|
||||
type: "start_capture",
|
||||
data: { currentTabId: currentTab.id, host: options.host, port: options.port },
|
||||
});
|
||||
} else {
|
||||
console.log("No Audio");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error occurred while starting capture:", error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Stops the capture process and performs cleanup.
|
||||
* @returns {Promise<void>} - A Promise that resolves when the capture process is stopped successfully.
|
||||
*/
|
||||
async function stopCapture() {
|
||||
const optionTabId = await getLocalStorageValue("optionTabId");
|
||||
const currentTabId = await getLocalStorageValue("currentTabId");
|
||||
|
||||
if (optionTabId) {
|
||||
res = await sendMessageToTab(currentTabId, {
|
||||
type: "STOP",
|
||||
data: { currentTabId: currentTabId },
|
||||
});
|
||||
await removeChromeTab(optionTabId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Listens for messages from the runtime and performs corresponding actions.
|
||||
* @param {Object} message - The message received from the runtime.
|
||||
*/
|
||||
chrome.runtime.onMessage.addListener((message) => {
|
||||
if (message.action === "startCapture") {
|
||||
startCapture(message);
|
||||
} else if (message.action === "stopCapture") {
|
||||
stopCapture();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Listens for if the tab is reloaded.
|
||||
* @param {Object} message - The message received from the runtime.
|
||||
*/
|
||||
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
|
||||
if (changeInfo.status === 'complete') {
|
||||
await executeScriptInTab(tabId, "content.js");
|
||||
await delayExecution(500);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
|
||||
|
||||
var elem_container = null;
|
||||
var elem_text = null;
|
||||
|
||||
var segments = [];
|
||||
var text_segments = [];
|
||||
|
||||
function init_element() {
|
||||
if (document.getElementById('transcription')) {
|
||||
return;
|
||||
}
|
||||
|
||||
elem_container = document.createElement('div');
|
||||
elem_container.id = "transcription";
|
||||
elem_container.style.cssText = 'padding-top:16px;font-size:18px;line-height:18px;top:0px;position:absolute;width:500px;height:90px;opacity:0.9;z-index:100;background:black;border-radius:10px;color:white;';
|
||||
|
||||
for (var i = 0; i < 4; i++) {
|
||||
elem_text = document.createElement('span');
|
||||
elem_text.style.cssText = 'position: absolute;padding-left:16px;padding-right:16px;';
|
||||
elem_text.id = "t" + i;
|
||||
elem_container.appendChild(elem_text);
|
||||
|
||||
if (i == 3) {
|
||||
elem_text.style.top = "-1000px"
|
||||
}
|
||||
}
|
||||
|
||||
document.body.appendChild(elem_container);
|
||||
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
|
||||
// Query the element
|
||||
const ele = elem_container;
|
||||
|
||||
// Handle the mousedown event
|
||||
// that's triggered when user drags the element
|
||||
const mouseDownHandler = function (e) {
|
||||
// Get the current mouse position
|
||||
x = e.clientX;
|
||||
y = e.clientY;
|
||||
|
||||
// Attach the listeners to `document`
|
||||
document.addEventListener('mousemove', mouseMoveHandler);
|
||||
document.addEventListener('mouseup', mouseUpHandler);
|
||||
};
|
||||
|
||||
const mouseMoveHandler = function (e) {
|
||||
// How far the mouse has been moved
|
||||
const dx = e.clientX - x;
|
||||
const dy = e.clientY - y;
|
||||
|
||||
// Set the position of element
|
||||
ele.style.top = `${ele.offsetTop + dy}px`;
|
||||
ele.style.left = `${ele.offsetLeft + dx}px`;
|
||||
|
||||
// Reassign the position of mouse
|
||||
x = e.clientX;
|
||||
y = e.clientY;
|
||||
};
|
||||
|
||||
const mouseUpHandler = function () {
|
||||
// Remove the handlers of `mousemove` and `mouseup`
|
||||
document.removeEventListener('mousemove', mouseMoveHandler);
|
||||
document.removeEventListener('mouseup', mouseUpHandler);
|
||||
};
|
||||
|
||||
ele.addEventListener('mousedown', mouseDownHandler);
|
||||
}
|
||||
|
||||
function getStyle(el,styleProp)
|
||||
{
|
||||
var x = document.getElementById(el);
|
||||
if (x.currentStyle)
|
||||
var y = x.currentStyle[styleProp];
|
||||
else if (window.getComputedStyle)
|
||||
var y = document.defaultView.getComputedStyle(x,null).getPropertyValue(styleProp);
|
||||
return y;
|
||||
}
|
||||
|
||||
function get_lines(elem, line_height) {
|
||||
var divHeight = elem.offsetHeight;
|
||||
var lines = divHeight / line_height;
|
||||
|
||||
var original_text = elem.innerHTML;
|
||||
|
||||
var words = original_text.split(' ');
|
||||
var segments = [];
|
||||
var current_lines = 1;
|
||||
var segment = '';
|
||||
var segment_len = 0;
|
||||
for (var i = 0; i < words.length; i++)
|
||||
{
|
||||
segment += words[i] + ' ';
|
||||
elem.innerHTML = segment;
|
||||
divHeight = elem.offsetHeight;
|
||||
|
||||
if ((divHeight / line_height) > current_lines) {
|
||||
var line_segment = segment.substring(segment_len, segment.length - 1 - words[i].length - 1);
|
||||
segments.push(line_segment);
|
||||
segment_len += line_segment.length + 1;
|
||||
current_lines++;
|
||||
}
|
||||
}
|
||||
|
||||
var line_segment = segment.substring(segment_len, segment.length - 1)
|
||||
segments.push(line_segment);
|
||||
|
||||
elem.innerHTML = original_text;
|
||||
|
||||
return segments;
|
||||
|
||||
}
|
||||
|
||||
function remove_element() {
|
||||
var elem = document.getElementById('transcription')
|
||||
for (var i = 0; i < 4; i++) {
|
||||
document.getElementById("t" + i).remove();
|
||||
}
|
||||
elem.remove()
|
||||
}
|
||||
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
const { type, data } = request;
|
||||
|
||||
if (type === "STOP") {
|
||||
remove_element();
|
||||
sendResponse({data: "STOPPED"});
|
||||
return;
|
||||
}
|
||||
|
||||
init_element();
|
||||
|
||||
message = JSON.parse(data);
|
||||
|
||||
var text = '';
|
||||
for (var i = 0; i < message.length; i++) {
|
||||
text += message[i].text + ' ';
|
||||
}
|
||||
text = text.replace(/(\r\n|\n|\r)/gm, "");
|
||||
|
||||
var elem = document.getElementById('t3');
|
||||
elem.innerHTML = 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;
|
||||
var lines = divHeight / line_height;
|
||||
|
||||
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 {
|
||||
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);
|
||||
elem.style.top = parent_elem.offsetHeight + parent_elem.offsetTop + 'px';
|
||||
}
|
||||
|
||||
sendResponse({});
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
|
||||
"name": "Audio Transcription",
|
||||
"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.",
|
||||
|
||||
"options_page": "options.html",
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"permissions": [
|
||||
"storage",
|
||||
"activeTab",
|
||||
"tabCapture",
|
||||
"scripting"
|
||||
],
|
||||
"icons": {
|
||||
"128":"icon128.png"
|
||||
},
|
||||
"action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_icon": "icon128.png"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Audio Transcription Options</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="style.css" type="text/css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script src="ort.min.js"></script>
|
||||
<script src="options.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* Captures audio from the active tab in Google Chrome.
|
||||
* @returns {Promise<MediaStream>} A promise that resolves with the captured audio stream.
|
||||
*/
|
||||
function captureTabAudio() {
|
||||
return new Promise((resolve) => {
|
||||
chrome.tabCapture.capture(
|
||||
{
|
||||
audio: true,
|
||||
video: false,
|
||||
},
|
||||
(stream) => {
|
||||
resolve(stream);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sends a message to a specific tab in Google Chrome.
|
||||
* @param {number} tabId - The ID of the tab to send the message to.
|
||||
* @param {any} data - The data to be sent as the message.
|
||||
* @returns {Promise<any>} A promise that resolves with the response from the tab.
|
||||
*/
|
||||
function sendMessageToTab(tabId, data) {
|
||||
return new Promise((resolve) => {
|
||||
chrome.tabs.sendMessage(tabId, data, (response) => {
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Starts recording audio from the captured tab.
|
||||
* @param {Object} option - The options object containing the currentTabId.
|
||||
*/
|
||||
async function startRecord(option) {
|
||||
const stream = await captureTabAudio();
|
||||
|
||||
if (stream) {
|
||||
// call when the stream inactive
|
||||
stream.oninactive = () => {
|
||||
window.close();
|
||||
};
|
||||
|
||||
const socket = new WebSocket(`ws://${option.host}:${option.port}/`);
|
||||
let isServerReady = false;
|
||||
socket.onopen = function(e) {
|
||||
socket.send("handshake");
|
||||
};
|
||||
|
||||
socket.onmessage = async (event) => {
|
||||
console.log(event.data);
|
||||
if (isServerReady === false){
|
||||
isServerReady = true;
|
||||
return;
|
||||
}
|
||||
|
||||
res = await sendMessageToTab(option.currentTabId, {
|
||||
type: "transcript",
|
||||
data: event.data,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const audioDataCache = [];
|
||||
const context = new AudioContext();
|
||||
const mediaStream = context.createMediaStreamSource(stream);
|
||||
const recorder = context.createScriptProcessor(4096, 1, 1);
|
||||
|
||||
recorder.onaudioprocess = async (event) => {
|
||||
if (!context || !isServerReady) return;
|
||||
|
||||
const inputData = event.inputBuffer.getChannelData(0);
|
||||
const audioData16kHz = resampleTo16kHZ(inputData, context.sampleRate);
|
||||
|
||||
audioDataCache.push(inputData);
|
||||
|
||||
// feed inputs and run
|
||||
socket.send(audioData16kHz);
|
||||
};
|
||||
|
||||
// Prevent page mute
|
||||
mediaStream.connect(recorder);
|
||||
recorder.connect(context.destination);
|
||||
mediaStream.connect(context.destination);
|
||||
// }
|
||||
} else {
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener for incoming messages from the extension's background script.
|
||||
* @param {Object} request - The message request object.
|
||||
* @param {Object} sender - The sender object containing information about the message sender.
|
||||
* @param {Function} sendResponse - The function to send a response back to the message sender.
|
||||
*/
|
||||
chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
|
||||
const { type, data } = request;
|
||||
|
||||
switch (type) {
|
||||
case "start_capture":
|
||||
await startRecord(data);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
sendResponse({});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Audio Capture</title>
|
||||
<script src="popup.js"></script>
|
||||
<link rel="stylesheet" href="style.css" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="header"><img src="./icon128.png"/> <h1>Audio Transcription</h1></div>
|
||||
<div class="button-container">
|
||||
<div class="button" id="startCapture">Start Capture</div>
|
||||
<div class="button" id="stopCapture" disabled>Stop Capture</div>
|
||||
</div>
|
||||
<div class="checkbox-container">
|
||||
<input type="checkbox" id="useServerCheckbox">
|
||||
<label for="useServerCheckbox">Use Collabora Whisper-Live Server</label>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,99 @@
|
||||
// Wait for the DOM content to be fully loaded
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
const startButton = document.getElementById("startCapture");
|
||||
const stopButton = document.getElementById("stopCapture");
|
||||
|
||||
const useServerCheckbox = document.getElementById("useServerCheckbox");
|
||||
|
||||
// Add click event listeners to the buttons
|
||||
startButton.addEventListener("click", startCapture);
|
||||
stopButton.addEventListener("click", stopCapture);
|
||||
|
||||
// Retrieve capturing state from storage on popup open
|
||||
chrome.storage.local.get("capturingState", ({ capturingState }) => {
|
||||
if (capturingState && capturingState.isCapturing) {
|
||||
toggleCaptureButtons(true);
|
||||
} else {
|
||||
toggleCaptureButtons(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Retrieve checkbox state from storage on popup open
|
||||
chrome.storage.local.get("useServerState", ({ useServerState }) => {
|
||||
if (useServerState !== undefined) {
|
||||
useServerCheckbox.checked = useServerState;
|
||||
}
|
||||
});
|
||||
|
||||
// Function to handle the start capture button click event
|
||||
async function startCapture() {
|
||||
// Ignore click if the button is disabled
|
||||
if (startButton.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the current active tab
|
||||
const currentTab = await getCurrentTab();
|
||||
|
||||
// Send a message to the background script to start capturing
|
||||
let host = "localhost";
|
||||
let port = "9090";
|
||||
const useCollaboraServer = useServerCheckbox.checked;
|
||||
if (useCollaboraServer){
|
||||
host = "transcription.kurg.org"
|
||||
port = "7090"
|
||||
}
|
||||
|
||||
chrome.runtime.sendMessage(
|
||||
{
|
||||
action: "startCapture",
|
||||
tabId: currentTab.id,
|
||||
host: host,
|
||||
port: port }, () => {
|
||||
// Update capturing state in storage and toggle the buttons
|
||||
chrome.storage.local.set({ capturingState: { isCapturing: true } }, () => {
|
||||
toggleCaptureButtons(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Function to handle the stop capture button click event
|
||||
function stopCapture() {
|
||||
// Ignore click if the button is disabled
|
||||
if (stopButton.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Send a message to the background script to stop capturing
|
||||
chrome.runtime.sendMessage({ action: "stopCapture" }, () => {
|
||||
// Update capturing state in storage and toggle the buttons
|
||||
chrome.storage.local.set({ capturingState: { isCapturing: false } }, () => {
|
||||
toggleCaptureButtons(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Function to get the current active tab
|
||||
async function getCurrentTab() {
|
||||
return new Promise((resolve) => {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
|
||||
resolve(tabs[0]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Function to toggle the capture buttons based on the capturing state
|
||||
function toggleCaptureButtons(isCapturing) {
|
||||
startButton.disabled = isCapturing;
|
||||
stopButton.disabled = !isCapturing;
|
||||
useServerCheckbox.disabled = isCapturing;
|
||||
startButton.classList.toggle("disabled", isCapturing);
|
||||
stopButton.classList.toggle("disabled", !isCapturing);
|
||||
}
|
||||
|
||||
// Save the checkbox state when it's toggled
|
||||
useServerCheckbox.addEventListener("change", () => {
|
||||
const useServerState = useServerCheckbox.checked;
|
||||
chrome.storage.local.set({ useServerState });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-bottom: 15px;
|
||||
padding-left: 20px;
|
||||
border-bottom: 2px solid darkred;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 64px;
|
||||
margin: 0 20px 0 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.inner {
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.options-list {
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.options-list li {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.limit {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.radioChoice {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 10px;
|
||||
border: 2px solid darkred;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
width: 150px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.button:hover:not(:disabled) {
|
||||
color: red;
|
||||
background-color: darkred;
|
||||
}
|
||||
|
||||
#save {
|
||||
font-size: 16px;
|
||||
margin-left: 50px;
|
||||
}
|
||||
|
||||
#status {
|
||||
color: red;
|
||||
margin-top: 8px;
|
||||
margin-left: 50px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#qualityLi {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#maxTime {
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.checkbox-container {
|
||||
padding: 10px;
|
||||
}
|
||||
Reference in New Issue
Block a user