← whisper-to-text

initial commit

d5d4f85765174c6c49220689a25b95e1292c53ac · ovidiucs · Feb 12, 2026 01:12

+2541 · 20 files changed

  • README.md +20 −0
  • src/META-INF/cose.manifest +72 −0
  • src/META-INF/cose.sig binary
  • src/META-INF/manifest.mf +82 −0
  • src/META-INF/mozilla.rsa binary
  • src/META-INF/mozilla.sf +4 −0
  • src/background.js +155 −0
  • src/content.js +207 −0
  • src/icons/create-icons.svg +11 −0
  • src/icons/icon-128.png binary
  • src/icons/icon-16.png binary
  • src/icons/icon-48.png binary
  • src/manifest.json +61 −0
  • src/popup/popup.css +286 −0
  • src/popup/popup.html +53 −0
  • src/popup/popup.js +419 −0
  • src/settings/settings.css +349 −0
  • src/settings/settings.html +142 −0
  • src/settings/settings.js +461 −0
  • src/utils/audio-processor.js +219 −0
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b9d90be
--- /dev/null
+++ b/README.md
@@ -0,0 +1,20 @@
+Whisper Voice to Text
+
+This extension records your voice, transcribes it with OpenAI Whisper or compatible APIs, and inserts the text into the active input on the current page and into the clipboard.
+
+NB: You will need your own API key.
+
+Set up (only once):
+* Open the settings page, grant access to the microphone
+* Choose a Whisper provider and set the API key
+
+Usage:
+* Place the cursor into a text field
+* Press CTRL+ALT+S to start recording and speak into your microphone
+* Press Space or Enter to stop
+
+The transcribed text should be placed into the text field. If for some reason the text didn't appear (maybe you accidentally clicked away from the field), a backup copy of the text should be in the clipboard.
+
+Original author:
+Whisper Voice to Text by Vlad Vaviloff
+MIT License
diff --git a/src/META-INF/cose.manifest b/src/META-INF/cose.manifest
new file mode 100644
index 0000000..ae35fc1
--- /dev/null
+++ b/src/META-INF/cose.manifest
@@ -0,0 +1,72 @@
+Manifest-Version: 1.0
+
+Name: manifest.json
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: 8n/pS4mvVpS0piluR0gQg3BeP5I=
+SHA256-Digest: 4zZweEzXU5T68ccUXaK5mgwSn50X3G6IceHCk0cM7xs=
+
+Name: content.js
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: PxgVrJMdwrTvkNPx7eAPcVQttcw=
+SHA256-Digest: xmAa7wOYfAakG59cXGjQuO4swrFneKLcg19r4yCJpsQ=
+
+Name: background.js
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: o9EbEni3zADSq513gLq4Sr8wWws=
+SHA256-Digest: oD8tVvT2n5CahQVQqA4+SW5u46Ql9hqf9NMzukzjLkU=
+
+Name: utils/audio-processor.js
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: hb0VuYo8ojTPYW2fhhq5MQTJqVs=
+SHA256-Digest: pg23jor6QeAGv9gufD7CJeOgUicAjSluF7jr1hKD4sk=
+
+Name: popup/popup.css
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: 8i6Y2uGJmk3HUa1ZP+6PPMtboIo=
+SHA256-Digest: /OfUe87FbRJhDfxIcXXwwcEay7w0ugyl7FpcqhLaXAQ=
+
+Name: popup/popup.html
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: Hhdr0lBZwIgWBRIwqPr90JOus+4=
+SHA256-Digest: 5nenmux2MbheZ8sWLU6dCy/oof0LdRoshG/qlDUdRyk=
+
+Name: popup/popup.js
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: Kw+Z9qhxqbwKobMR5ojPvPLz0wY=
+SHA256-Digest: HQK1L0mu/kV7tX+QxndSHzsPMgi+ThjuwaL0uXS5ts8=
+
+Name: settings/settings.css
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: HTfaNRx7E3jj+K1fzINrsDm7BZ4=
+SHA256-Digest: pYaKW9E1N7fgVv9lNvn0mxOCh5CGW/o6oIpybbMvDHE=
+
+Name: settings/settings.js
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: wk7BMWvf0kgXleFA+4jN5vxbI6s=
+SHA256-Digest: 3tLr63H0MtmVTju38j0ykjxpsvIwLI6NwwMiwNPojuw=
+
+Name: settings/settings.html
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: GTo2MgfYuyHcmzpZOqKkgGgYOgI=
+SHA256-Digest: RG7Giix7x3aUP5Oxw6FaPhDc0CTTlKr0x9TAZkbouCY=
+
+Name: icons/create-icons.svg
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: VR661JMYB357bFkeFSdLxAl6njw=
+SHA256-Digest: BzyvmDH8PmBOhfoN5hHBYZoz+CU0SPeHyki0ugfXfFY=
+
+Name: icons/icon-16.png
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: qNb5pyexlAXwr0KljZnXlvYgtK8=
+SHA256-Digest: W8Mqg0vaYnVRnODmgxnSvenVEy+FL6jug+o3aXT5im8=
+
+Name: icons/icon-128.png
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: JSAtazdkRoU4hFGLEQ34eI9/1gU=
+SHA256-Digest: Zk3pH3NHLtccjKnBWMIvJcobUXhUlaSIBe7Aeb7z5bc=
+
+Name: icons/icon-48.png
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: anKRjo+3SW8vv1kbhTkijR0+m94=
+SHA256-Digest: scZzPu1Z+GafUQ/hRKr8qYeCTnd0ctapyfDDH/327Cg=
+
diff --git a/src/META-INF/cose.sig b/src/META-INF/cose.sig
new file mode 100644
index 0000000..185d95d
Binary files /dev/null and b/src/META-INF/cose.sig differ
diff --git a/src/META-INF/manifest.mf b/src/META-INF/manifest.mf
new file mode 100644
index 0000000..d605276
--- /dev/null
+++ b/src/META-INF/manifest.mf
@@ -0,0 +1,82 @@
+Manifest-Version: 1.0
+
+Name: manifest.json
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: 8n/pS4mvVpS0piluR0gQg3BeP5I=
+SHA256-Digest: 4zZweEzXU5T68ccUXaK5mgwSn50X3G6IceHCk0cM7xs=
+
+Name: content.js
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: PxgVrJMdwrTvkNPx7eAPcVQttcw=
+SHA256-Digest: xmAa7wOYfAakG59cXGjQuO4swrFneKLcg19r4yCJpsQ=
+
+Name: background.js
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: o9EbEni3zADSq513gLq4Sr8wWws=
+SHA256-Digest: oD8tVvT2n5CahQVQqA4+SW5u46Ql9hqf9NMzukzjLkU=
+
+Name: utils/audio-processor.js
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: hb0VuYo8ojTPYW2fhhq5MQTJqVs=
+SHA256-Digest: pg23jor6QeAGv9gufD7CJeOgUicAjSluF7jr1hKD4sk=
+
+Name: popup/popup.css
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: 8i6Y2uGJmk3HUa1ZP+6PPMtboIo=
+SHA256-Digest: /OfUe87FbRJhDfxIcXXwwcEay7w0ugyl7FpcqhLaXAQ=
+
+Name: popup/popup.html
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: Hhdr0lBZwIgWBRIwqPr90JOus+4=
+SHA256-Digest: 5nenmux2MbheZ8sWLU6dCy/oof0LdRoshG/qlDUdRyk=
+
+Name: popup/popup.js
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: Kw+Z9qhxqbwKobMR5ojPvPLz0wY=
+SHA256-Digest: HQK1L0mu/kV7tX+QxndSHzsPMgi+ThjuwaL0uXS5ts8=
+
+Name: settings/settings.css
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: HTfaNRx7E3jj+K1fzINrsDm7BZ4=
+SHA256-Digest: pYaKW9E1N7fgVv9lNvn0mxOCh5CGW/o6oIpybbMvDHE=
+
+Name: settings/settings.js
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: wk7BMWvf0kgXleFA+4jN5vxbI6s=
+SHA256-Digest: 3tLr63H0MtmVTju38j0ykjxpsvIwLI6NwwMiwNPojuw=
+
+Name: settings/settings.html
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: GTo2MgfYuyHcmzpZOqKkgGgYOgI=
+SHA256-Digest: RG7Giix7x3aUP5Oxw6FaPhDc0CTTlKr0x9TAZkbouCY=
+
+Name: icons/create-icons.svg
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: VR661JMYB357bFkeFSdLxAl6njw=
+SHA256-Digest: BzyvmDH8PmBOhfoN5hHBYZoz+CU0SPeHyki0ugfXfFY=
+
+Name: icons/icon-16.png
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: qNb5pyexlAXwr0KljZnXlvYgtK8=
+SHA256-Digest: W8Mqg0vaYnVRnODmgxnSvenVEy+FL6jug+o3aXT5im8=
+
+Name: icons/icon-128.png
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: JSAtazdkRoU4hFGLEQ34eI9/1gU=
+SHA256-Digest: Zk3pH3NHLtccjKnBWMIvJcobUXhUlaSIBe7Aeb7z5bc=
+
+Name: icons/icon-48.png
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: anKRjo+3SW8vv1kbhTkijR0+m94=
+SHA256-Digest: scZzPu1Z+GafUQ/hRKr8qYeCTnd0ctapyfDDH/327Cg=
+
+Name: META-INF/cose.manifest
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: FWmfUPS12z1R0/EAg3zaHnw1QhY=
+SHA256-Digest: 1vOWOkFX7wT9AK6LAfFoI4xTCfvHbKNjWj3ujT7dWnM=
+
+Name: META-INF/cose.sig
+Digest-Algorithms: SHA1 SHA256
+SHA1-Digest: KcRS/R+GTv82GsfQfO5zTx4sKjg=
+SHA256-Digest: VAC5R8B22ofTYJ0plFt+FrXcojYBLbjDjqg2Laa/zKo=
+
diff --git a/src/META-INF/mozilla.rsa b/src/META-INF/mozilla.rsa
new file mode 100644
index 0000000..fc7d0aa
Binary files /dev/null and b/src/META-INF/mozilla.rsa differ
diff --git a/src/META-INF/mozilla.sf b/src/META-INF/mozilla.sf
new file mode 100644
index 0000000..1a6e062
--- /dev/null
+++ b/src/META-INF/mozilla.sf
@@ -0,0 +1,4 @@
+Signature-Version: 1.0
+SHA1-Digest-Manifest: FGqbW4oYV4DyVj4LUof7iba0CaE=
+SHA256-Digest-Manifest: jIRETtUG5vXK3ZuMAVvlIBdlQ8pIuGFo7UAjzucUJI8=
+
diff --git a/src/background.js b/src/background.js
new file mode 100644
index 0000000..9b2dd51
--- /dev/null
+++ b/src/background.js
@@ -0,0 +1,155 @@
+/**
+ * Background script for Voice to Text extension
+ * Handles API communication with Whisper endpoints
+ */
+
+// Listen for messages from popup or content scripts
+browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
+  if (message.action === 'transcribe') {
+    // Handle async transcription
+    transcribeAudio(message.audioBlob)
+      .then(result => {
+        sendResponse(result);
+      })
+      .catch(error => {
+        console.error('Transcription error:', error);
+        sendResponse({
+          success: false,
+          error: error.message
+        });
+      });
+
+    // Return true to indicate we'll send response asynchronously
+    return true;
+  }
+
+  if (message.action === 'getSettings') {
+    // Handle async settings retrieval
+    getSettings()
+      .then(settings => {
+        sendResponse(settings);
+      })
+      .catch(error => {
+        sendResponse({
+          success: false,
+          error: error.message
+        });
+      });
+
+    // Return true to indicate we'll send response asynchronously
+    return true;
+  }
+});
+
+/**
+ * Get settings from browser storage
+ */
+async function getSettings() {
+  const defaults = {
+    apiEndpoint: 'https://api.openai.com/v1/audio/transcriptions',
+    apiToken: '',
+    model: 'whisper-1'
+  };
+
+  try {
+    const stored = await browser.storage.sync.get(['apiEndpoint', 'apiToken', 'model']);
+    return {
+      success: true,
+      settings: {
+        apiEndpoint: stored.apiEndpoint || defaults.apiEndpoint,
+        apiToken: stored.apiToken || defaults.apiToken,
+        model: stored.model || defaults.model
+      }
+    };
+  } catch (error) {
+    return {
+      success: false,
+      error: error.message,
+      settings: defaults
+    };
+  }
+}
+
+/**
+ * Transcribe audio using configured API
+ */
+async function transcribeAudio(audioBlob) {
+  const settingsResult = await getSettings();
+
+  if (!settingsResult.success) {
+    throw new Error('Failed to load settings: ' + settingsResult.error);
+  }
+
+  const settings = settingsResult.settings;
+
+  if (!settings.apiToken) {
+    throw new Error('API token not configured. Please set it in the extension settings.');
+  }
+
+  // Convert base64 to blob if needed
+  let audioFile;
+  if (typeof audioBlob === 'string') {
+    const response = await fetch(audioBlob);
+    audioFile = await response.blob();
+  } else {
+    audioFile = audioBlob;
+  }
+
+  // Create form data for API request
+  const formData = new FormData();
+  formData.append('file', audioFile, 'audio.webm');
+  formData.append('model', settings.model);
+
+  try {
+    const response = await fetch(settings.apiEndpoint, {
+      method: 'POST',
+      headers: {
+        'Authorization': `Bearer ${settings.apiToken}`
+      },
+      body: formData
+    });
+
+    if (!response.ok) {
+      const errorText = await response.text();
+      let errorMessage = `API request failed: ${response.status} ${response.statusText}`;
+
+      try {
+        const errorJson = JSON.parse(errorText);
+        if (errorJson.error && errorJson.error.message) {
+          errorMessage = errorJson.error.message;
+        }
+      } catch (e) {
+        // If not JSON, use the raw text
+        if (errorText) {
+          errorMessage += ` - ${errorText}`;
+        }
+      }
+
+      throw new Error(errorMessage);
+    }
+
+    const result = await response.json();
+
+    // Handle different response formats
+    let transcription = '';
+    if (result.text) {
+      transcription = result.text;
+    } else if (result.transcription) {
+      transcription = result.transcription;
+    } else {
+      throw new Error('Unexpected API response format');
+    }
+
+    return {
+      success: true,
+      text: transcription.trim()
+    };
+
+  } catch (error) {
+    console.error('API request error:', error);
+    throw error;
+  }
+}
+
+// Log when background script is loaded
+console.log('Voice to Text extension background script loaded');
diff --git a/src/content.js b/src/content.js
new file mode 100644
index 0000000..2421c38
--- /dev/null
+++ b/src/content.js
@@ -0,0 +1,207 @@
+/**
+ * Content script for Voice to Text extension
+ * Handles text insertion into active input fields
+ */
+
+// Store the last focused element
+let lastFocusedElement = null;
+
+// Track focus on all input elements
+document.addEventListener('focusin', (event) => {
+  const target = event.target;
+  if (isEditableElement(target)) {
+    lastFocusedElement = target;
+  }
+}, true);
+
+// Also track click events for contenteditable elements
+document.addEventListener('click', (event) => {
+  const target = event.target;
+  if (isEditableElement(target)) {
+    lastFocusedElement = target;
+  }
+}, true);
+
+/**
+ * Check if element is editable
+ */
+function isEditableElement(element) {
+  if (!element) return false;
+
+  const tagName = element.tagName ? element.tagName.toLowerCase() : '';
+
+  // Check for input fields
+  if (tagName === 'input') {
+    const type = element.type ? element.type.toLowerCase() : 'text';
+    const editableTypes = ['text', 'search', 'email', 'url', 'tel', 'password'];
+    return editableTypes.includes(type) && !element.disabled && !element.readOnly;
+  }
+
+  // Check for textarea
+  if (tagName === 'textarea') {
+    return !element.disabled && !element.readOnly;
+  }
+
+  // Check for contenteditable
+  if (element.isContentEditable) {
+    return true;
+  }
+
+  return false;
+}
+
+/**
+ * Get the currently active editable element
+ */
+function getActiveElement() {
+  // First try document.activeElement
+  let active = document.activeElement;
+
+  if (active && isEditableElement(active)) {
+    return active;
+  }
+
+  // If no active element, try the last focused one
+  if (lastFocusedElement && isEditableElement(lastFocusedElement)) {
+    return lastFocusedElement;
+  }
+
+  // If still nothing, try to find any focused input
+  const focused = document.querySelector('input:focus, textarea:focus, [contenteditable="true"]:focus');
+  if (focused) {
+    return focused;
+  }
+
+  return null;
+}
+
+/**
+ * Insert text into an element
+ */
+function insertText(element, text) {
+  if (!element || !text) return false;
+
+  try {
+    // Focus the element
+    element.focus();
+
+    const tagName = element.tagName ? element.tagName.toLowerCase() : '';
+
+    // Handle contenteditable elements
+    if (element.isContentEditable) {
+      const selection = window.getSelection();
+      const range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
+
+      if (range) {
+        // Check if we need to add a space before the text
+        let textToInsert = text;
+        const beforeCursor = range.startContainer.textContent?.substring(0, range.startOffset) || '';
+
+        // Add space if there's text before cursor and it doesn't end with whitespace
+        if (beforeCursor.length > 0 && !beforeCursor.match(/\s$/)) {
+          textToInsert = ' ' + text;
+        }
+
+        range.deleteContents();
+        const textNode = document.createTextNode(textToInsert);
+        range.insertNode(textNode);
+
+        // Move cursor to end of inserted text
+        range.setStartAfter(textNode);
+        range.setEndAfter(textNode);
+        selection.removeAllRanges();
+        selection.addRange(range);
+      } else {
+        // Fallback: append to end with space if needed
+        const currentText = element.textContent || '';
+        if (currentText.length > 0 && !currentText.match(/\s$/)) {
+          element.textContent += ' ' + text;
+        } else {
+          element.textContent += text;
+        }
+      }
+
+      // Trigger input event
+      element.dispatchEvent(new Event('input', { bubbles: true }));
+      element.dispatchEvent(new Event('change', { bubbles: true }));
+
+      return true;
+    }
+
+    // Handle input and textarea elements
+    if (tagName === 'input' || tagName === 'textarea') {
+      const start = element.selectionStart || 0;
+      const end = element.selectionEnd || 0;
+      const currentValue = element.value;
+
+      // Check if we need to add a space before the text
+      let textToInsert = text;
+      const beforeCursor = currentValue.substring(0, start);
+
+      // Add space if there's text before cursor and it doesn't end with whitespace
+      if (beforeCursor.length > 0 && !beforeCursor.match(/\s$/)) {
+        textToInsert = ' ' + text;
+      }
+
+      // Insert text at cursor position
+      const newValue = currentValue.substring(0, start) + textToInsert + currentValue.substring(end);
+      element.value = newValue;
+
+      // Set cursor position after inserted text
+      const newPosition = start + textToInsert.length;
+      element.setSelectionRange(newPosition, newPosition);
+
+      // Trigger events to ensure frameworks detect the change
+      element.dispatchEvent(new Event('input', { bubbles: true }));
+      element.dispatchEvent(new Event('change', { bubbles: true }));
+
+      return true;
+    }
+
+    return false;
+  } catch (error) {
+    console.error('Error inserting text:', error);
+    return false;
+  }
+}
+
+/**
+ * Listen for messages from popup
+ */
+browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
+  if (message.action === 'insertText') {
+    const element = getActiveElement();
+
+    if (!element) {
+      sendResponse({
+        success: false,
+        error: 'No active input field found. Please click on a text field first.'
+      });
+      return;
+    }
+
+    const success = insertText(element, message.text);
+
+    if (success) {
+      sendResponse({
+        success: true,
+        message: 'Text inserted successfully'
+      });
+    } else {
+      sendResponse({
+        success: false,
+        error: 'Failed to insert text into the field'
+      });
+    }
+  }
+
+  if (message.action === 'checkActiveElement') {
+    const element = getActiveElement();
+    sendResponse({
+      hasActiveElement: element !== null,
+      elementType: element ? element.tagName.toLowerCase() : null
+    });
+  }
+});
+
+console.log('Voice to Text content script loaded');
diff --git a/src/icons/create-icons.svg b/src/icons/create-icons.svg
new file mode 100644
index 0000000..72c5ebc
--- /dev/null
+++ b/src/icons/create-icons.svg
@@ -0,0 +1,11 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
+  <defs>
+    <linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
+      <stop offset="0%" style="stop-color:#667eea;stop-opacity:1" />
+      <stop offset="100%" style="stop-color:#764ba2;stop-opacity:1" />
+    </linearGradient>
+  </defs>
+  <circle cx="64" cy="64" r="60" fill="url(#grad)"/>
+  <path d="M64 44c-6.6 0-12 5.4-12 12v16c0 6.6 5.4 12 12 12s12-5.4 12-12V56c0-6.6-5.4-12-12-12z" fill="white"/>
+  <path d="M80 68c0 8.8-7.2 16-16 16s-16-7.2-16-16H40c0 12.4 9.2 22.6 21.3 24.3V104h5.4V92.3C78.8 90.6 88 80.4 88 68h-8z" fill="white"/>
+</svg>
diff --git a/src/icons/icon-128.png b/src/icons/icon-128.png
new file mode 100644
index 0000000..356b383
Binary files /dev/null and b/src/icons/icon-128.png differ
diff --git a/src/icons/icon-16.png b/src/icons/icon-16.png
new file mode 100644
index 0000000..34ee208
Binary files /dev/null and b/src/icons/icon-16.png differ
diff --git a/src/icons/icon-48.png b/src/icons/icon-48.png
new file mode 100644
index 0000000..16fc9fb
Binary files /dev/null and b/src/icons/icon-48.png differ
diff --git a/src/manifest.json b/src/manifest.json
new file mode 100644
index 0000000..176ed51
--- /dev/null
+++ b/src/manifest.json
@@ -0,0 +1,61 @@
+{
+  "manifest_version": 2,
+  "name": "Whisper Voice to Text",
+  "version": "1.0.0",
+  "description": "Record your voice and insert transcribed text into any input field using OpenAI Whisper or compatible APIs",
+  "permissions": [
+    "activeTab",
+    "storage",
+    "<all_urls>"
+  ],
+  "background": {
+    "scripts": [
+      "background.js"
+    ],
+    "persistent": false
+  },
+  "content_scripts": [
+    {
+      "matches": [
+        "<all_urls>"
+      ],
+      "js": [
+        "content.js"
+      ],
+      "run_at": "document_idle"
+    }
+  ],
+  "browser_action": {
+    "default_popup": "popup/popup.html",
+    "default_icon": {
+      "16": "icons/icon-16.png",
+      "48": "icons/icon-48.png",
+      "128": "icons/icon-128.png"
+    },
+    "default_title": "Whisper Voice to Text"
+  },
+  "commands": {
+    "_execute_browser_action": {
+      "suggested_key": {
+        "default": "Ctrl+Alt+S",
+        "mac": "Command+Alt+S"
+      },
+      "description": "Activate voice recording"
+    }
+  },
+  "options_ui": {
+    "page": "settings/settings.html",
+    "open_in_tab": true
+  },
+  "icons": {
+    "16": "icons/icon-16.png",
+    "48": "icons/icon-48.png",
+    "128": "icons/icon-128.png"
+  },
+  "browser_specific_settings": {
+    "gecko": {
+      "id": "voice-to-text@extension.local",
+      "strict_min_version": "58.0"
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/popup/popup.css b/src/popup/popup.css
new file mode 100644
index 0000000..573eaf5
--- /dev/null
+++ b/src/popup/popup.css
@@ -0,0 +1,286 @@
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+body {
+  width: 320px;
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
+  background: #f5f5f5;
+  color: #333;
+}
+
+.container {
+  display: flex;
+  flex-direction: column;
+  min-height: 400px;
+}
+
+header {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  padding: 16px;
+  text-align: center;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+header h1 {
+  font-size: 18px;
+  font-weight: 600;
+}
+
+main {
+  flex: 1;
+  padding: 20px;
+  background: white;
+}
+
+.status-message {
+  padding: 12px;
+  border-radius: 6px;
+  margin-bottom: 16px;
+  font-size: 14px;
+  text-align: center;
+  transition: all 0.3s ease;
+}
+
+.status-message.hidden {
+  display: none;
+}
+
+.status-message.error {
+  background: #fee;
+  color: #c33;
+  border: 1px solid #fcc;
+}
+
+.status-message.success {
+  background: #efe;
+  color: #3a3;
+  border: 1px solid #cfc;
+}
+
+.status-message.info {
+  background: #eef;
+  color: #33a;
+  border: 1px solid #ccf;
+}
+
+.recording-section {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 16px;
+  margin-bottom: 24px;
+}
+
+.record-btn {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 8px;
+  padding: 24px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  border: none;
+  border-radius: 12px;
+  cursor: pointer;
+  transition: all 0.3s ease;
+  font-size: 14px;
+  font-weight: 500;
+  width: 100%;
+  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
+}
+
+.record-btn:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 6px 16px rgba(102, 126, 234, 0.4);
+}
+
+.record-btn:active {
+  transform: translateY(0);
+}
+
+.record-btn.recording {
+  background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+  animation: pulse 2s infinite;
+}
+
+.record-btn.processing {
+  background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
+  cursor: wait;
+  pointer-events: none;
+}
+
+.mic-icon {
+  width: 48px;
+  height: 48px;
+  fill: currentColor;
+}
+
+.recording-indicator {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  color: #f5576c;
+  font-weight: 500;
+  font-size: 14px;
+}
+
+.recording-indicator.hidden {
+  display: none;
+}
+
+.pulse {
+  width: 12px;
+  height: 12px;
+  background: #f5576c;
+  border-radius: 50%;
+  animation: pulse-dot 1.5s infinite;
+}
+
+.timer {
+  font-size: 24px;
+  font-weight: 600;
+  color: #667eea;
+  font-variant-numeric: tabular-nums;
+}
+
+.timer.hidden {
+  display: none;
+}
+
+.info-section {
+  padding: 16px;
+  background: #f9f9f9;
+  border-radius: 8px;
+  border: 1px solid #e0e0e0;
+}
+
+.shortcut-hint {
+  font-size: 13px;
+  color: #666;
+  margin-bottom: 8px;
+  text-align: center;
+}
+
+kbd {
+  display: inline-block;
+  padding: 2px 6px;
+  background: white;
+  border: 1px solid #ccc;
+  border-radius: 3px;
+  font-size: 11px;
+  font-family: monospace;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+}
+
+.instruction {
+  font-size: 12px;
+  color: #888;
+  text-align: center;
+  line-height: 1.5;
+}
+
+footer {
+  padding: 12px 16px;
+  background: #f5f5f5;
+  border-top: 1px solid #e0e0e0;
+}
+
+.settings-btn {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 8px;
+  width: 100%;
+  padding: 10px;
+  background: white;
+  border: 1px solid #d0d0d0;
+  border-radius: 6px;
+  cursor: pointer;
+  font-size: 14px;
+  color: #555;
+  transition: all 0.2s ease;
+}
+
+.settings-btn:hover {
+  background: #fafafa;
+  border-color: #667eea;
+  color: #667eea;
+}
+
+.settings-btn svg {
+  width: 16px;
+  height: 16px;
+  fill: currentColor;
+}
+
+.transcribed-text {
+  margin: 16px 0;
+  padding: 16px;
+  background: #f9f9f9;
+  border: 2px solid #667eea;
+  border-radius: 8px;
+  position: relative;
+}
+
+.transcribed-text.hidden {
+  display: none;
+}
+
+.transcribed-text .text-content {
+  font-size: 14px;
+  line-height: 1.6;
+  color: #333;
+  word-wrap: break-word;
+  margin-bottom: 12px;
+  padding: 8px;
+  background: white;
+  border-radius: 4px;
+  max-height: 150px;
+  overflow-y: auto;
+}
+
+.transcribed-text .copy-btn {
+  padding: 8px 16px;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  border: none;
+  border-radius: 6px;
+  cursor: pointer;
+  font-size: 13px;
+  font-weight: 500;
+  transition: all 0.2s ease;
+  width: 100%;
+}
+
+.transcribed-text .copy-btn:hover {
+  transform: translateY(-1px);
+  box-shadow: 0 2px 8px rgba(102, 126, 234, 0.4);
+}
+
+.transcribed-text .copy-btn:active {
+  transform: translateY(0);
+}
+
+@keyframes pulse {
+  0%, 100% {
+    box-shadow: 0 4px 12px rgba(245, 87, 108, 0.3);
+  }
+  50% {
+    box-shadow: 0 4px 20px rgba(245, 87, 108, 0.6);
+  }
+}
+
+@keyframes pulse-dot {
+  0%, 100% {
+    opacity: 1;
+    transform: scale(1);
+  }
+  50% {
+    opacity: 0.5;
+    transform: scale(1.2);
+  }
+}
diff --git a/src/popup/popup.html b/src/popup/popup.html
new file mode 100644
index 0000000..03a2602
--- /dev/null
+++ b/src/popup/popup.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Voice to Text</title>
+  <link rel="stylesheet" href="popup.css">
+</head>
+<body>
+  <div class="container">
+    <header>
+      <h1>Voice to Text</h1>
+    </header>
+
+    <main>
+      <div id="status-message" class="status-message hidden"></div>
+
+      <div class="recording-section">
+        <button id="record-btn" class="record-btn" title="Click or press Ctrl+Shift+V to start recording">
+          <svg class="mic-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+            <path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z"/>
+            <path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/>
+          </svg>
+          <span id="button-text">Start Recording</span>
+        </button>
+
+        <div id="recording-indicator" class="recording-indicator hidden">
+          <div class="pulse"></div>
+          <span>Recording...</span>
+        </div>
+
+        <div id="timer" class="timer hidden">0:00</div>
+      </div>
+
+      <div class="info-section">
+        <p class="shortcut-hint">Press <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>S</kbd> to start recording</p>
+        <p class="instruction">Press <kbd>Space</kbd>, <kbd>Enter</kbd>, or <kbd>Esc</kbd> to stop and transcribe</p>
+      </div>
+    </main>
+
+    <footer>
+      <button id="settings-btn" class="settings-btn" title="Open settings">
+        <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+          <path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/>
+        </svg>
+        Settings
+      </button>
+    </footer>
+  </div>
+
+  <script src="popup.js"></script>
+</body>
+</html>
diff --git a/src/popup/popup.js b/src/popup/popup.js
new file mode 100644
index 0000000..1af3231
--- /dev/null
+++ b/src/popup/popup.js
@@ -0,0 +1,419 @@
+/**
+ * Popup script for Voice to Text extension
+ * Handles audio recording and UI interactions
+ */
+
+// State management
+let mediaRecorder = null;
+let audioChunks = [];
+let isRecording = false;
+let recordingStartTime = 0;
+let timerInterval = null;
+
+// DOM elements
+const recordBtn = document.getElementById('record-btn');
+const buttonText = document.getElementById('button-text');
+const statusMessage = document.getElementById('status-message');
+const recordingIndicator = document.getElementById('recording-indicator');
+const timer = document.getElementById('timer');
+const settingsBtn = document.getElementById('settings-btn');
+
+// Initialize
+document.addEventListener('DOMContentLoaded', async () => {
+  recordBtn.addEventListener('click', toggleRecording);
+  settingsBtn.addEventListener('click', openSettings);
+
+  // Check if API is configured
+  await checkConfiguration();
+
+  // Auto-start recording when popup opens
+  // (This happens when user presses the keyboard shortcut)
+  await startRecording();
+});
+
+/**
+ * Check if API is configured
+ */
+async function checkConfiguration() {
+  try {
+    const result = await browser.runtime.sendMessage({ action: 'getSettings' });
+    if (result.success && !result.settings.apiToken) {
+      showStatus('Please configure your API token in settings', 'info');
+    }
+  } catch (error) {
+    console.error('Error checking configuration:', error);
+  }
+}
+
+/**
+ * Toggle recording state
+ */
+async function toggleRecording() {
+  if (isRecording) {
+    stopRecording();
+  } else {
+    await startRecording();
+  }
+}
+
+/**
+ * Start recording audio
+ */
+async function startRecording() {
+  try {
+    showStatus('Requesting microphone access...', 'info');
+
+    // Request microphone permission
+    const stream = await navigator.mediaDevices.getUserMedia({
+      audio: {
+        echoCancellation: true,
+        noiseSuppression: true,
+        autoGainControl: true
+      }
+    });
+
+    // Create MediaRecorder
+    const mimeType = getSupportedMimeType();
+    mediaRecorder = new MediaRecorder(stream, { mimeType });
+
+    audioChunks = [];
+
+    mediaRecorder.addEventListener('dataavailable', (event) => {
+      if (event.data.size > 0) {
+        audioChunks.push(event.data);
+      }
+    });
+
+    mediaRecorder.addEventListener('stop', async () => {
+      // Stop all tracks
+      stream.getTracks().forEach(track => track.stop());
+
+      // Process the recording
+      await processRecording();
+    });
+
+    // Start recording
+    mediaRecorder.start();
+    isRecording = true;
+    recordingStartTime = Date.now();
+
+    // Update UI
+    updateUIForRecording();
+    hideStatus();
+    hideTranscribedText();
+    startTimer();
+
+  } catch (error) {
+    console.error('Error starting recording:', error);
+    let errorMessage = 'Failed to start recording. ';
+
+    if (error.name === 'NotAllowedError') {
+      errorMessage += 'Please allow microphone access.';
+    } else if (error.name === 'NotFoundError') {
+      errorMessage += 'No microphone found.';
+    } else {
+      errorMessage += error.message;
+    }
+
+    showStatus(errorMessage, 'error');
+    resetUI();
+  }
+}
+
+/**
+ * Stop recording audio
+ */
+function stopRecording() {
+  if (mediaRecorder && isRecording) {
+    mediaRecorder.stop();
+    isRecording = false;
+    stopTimer();
+
+    // Update UI to processing state
+    recordBtn.classList.remove('recording');
+    recordBtn.classList.add('processing');
+    buttonText.textContent = 'Processing...';
+    recordingIndicator.classList.add('hidden');
+
+    showStatus('Processing audio...', 'info');
+  }
+}
+
+/**
+ * Process recorded audio
+ */
+async function processRecording() {
+  try {
+    // Create blob from chunks
+    const mimeType = getSupportedMimeType();
+    const audioBlob = new Blob(audioChunks, { type: mimeType });
+
+    // Check if recording is too short
+    const duration = (Date.now() - recordingStartTime) / 1000;
+    if (duration < 0.5) {
+      showStatus('Recording too short. Please try again.', 'error');
+      resetUI();
+      return;
+    }
+
+    // Send to background script for transcription
+    showStatus('Transcribing audio...', 'info');
+
+    const result = await browser.runtime.sendMessage({
+      action: 'transcribe',
+      audioBlob: await blobToBase64(audioBlob)
+    });
+
+    if (result.success && result.text) {
+      // Always copy to clipboard as backup
+      try {
+        await navigator.clipboard.writeText(result.text);
+        console.log('Text copied to clipboard as backup');
+      } catch (clipboardError) {
+        console.warn('Could not copy to clipboard:', clipboardError);
+      }
+
+      // Try to insert text into active field
+      try {
+        const tabs = await browser.tabs.query({ active: true, currentWindow: true });
+
+        if (tabs.length === 0) {
+          // No active tab, just show the text
+          showTranscribedText(result.text);
+          return;
+        }
+
+        // Try to send text to content script
+        const response = await browser.tabs.sendMessage(tabs[0].id, {
+          action: 'insertText',
+          text: result.text
+        });
+
+        if (response && response.success) {
+          showStatus('Text inserted successfully! (also copied to clipboard)', 'success');
+          // Auto-close popup after successful insertion
+          setTimeout(() => {
+            window.close();
+          }, 800); // Brief delay to show success message
+        } else {
+          // Insertion failed, show the text instead
+          const errorMsg = response?.error || 'No input field focused';
+          showTranscribedText(result.text, errorMsg);
+        }
+      } catch (error) {
+        // Content script not responding or other error, show the text
+        console.log('Could not insert text, showing in popup:', error);
+        showTranscribedText(result.text, 'Could not insert text into page');
+      }
+
+    } else {
+      const errorMsg = result.error || 'Transcription failed';
+      showStatus(errorMsg, 'error');
+    }
+
+  } catch (error) {
+    console.error('Error processing recording:', error);
+    showStatus('Error: ' + error.message, 'error');
+  } finally {
+    resetUI();
+  }
+}
+
+/**
+ * Get supported MIME type for recording
+ */
+function getSupportedMimeType() {
+  const types = [
+    'audio/webm',
+    'audio/webm;codecs=opus',
+    'audio/ogg;codecs=opus',
+    'audio/mp4'
+  ];
+
+  for (const type of types) {
+    if (MediaRecorder.isTypeSupported(type)) {
+      return type;
+    }
+  }
+
+  return 'audio/webm'; // Fallback
+}
+
+/**
+ * Convert blob to base64
+ */
+function blobToBase64(blob) {
+  return new Promise((resolve, reject) => {
+    const reader = new FileReader();
+    reader.onloadend = () => resolve(reader.result);
+    reader.onerror = reject;
+    reader.readAsDataURL(blob);
+  });
+}
+
+/**
+ * Update UI for recording state
+ */
+function updateUIForRecording() {
+  recordBtn.classList.add('recording');
+  buttonText.textContent = 'Stop Recording';
+  recordingIndicator.classList.remove('hidden');
+  timer.classList.remove('hidden');
+}
+
+/**
+ * Reset UI to initial state
+ */
+function resetUI() {
+  recordBtn.classList.remove('recording', 'processing');
+  buttonText.textContent = 'Start Recording';
+  recordingIndicator.classList.add('hidden');
+  timer.classList.add('hidden');
+  timer.textContent = '0:00';
+}
+
+/**
+ * Start recording timer
+ */
+function startTimer() {
+  timerInterval = setInterval(() => {
+    const elapsed = Math.floor((Date.now() - recordingStartTime) / 1000);
+    const minutes = Math.floor(elapsed / 60);
+    const seconds = elapsed % 60;
+    timer.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
+  }, 1000);
+}
+
+/**
+ * Stop recording timer
+ */
+function stopTimer() {
+  if (timerInterval) {
+    clearInterval(timerInterval);
+    timerInterval = null;
+  }
+}
+
+/**
+ * Show status message
+ */
+function showStatus(message, type = 'info') {
+  statusMessage.textContent = message;
+  statusMessage.className = `status-message ${type}`;
+  statusMessage.classList.remove('hidden');
+}
+
+/**
+ * Hide status message
+ */
+function hideStatus() {
+  statusMessage.classList.add('hidden');
+}
+
+/**
+ * Show transcribed text in the popup when insertion fails
+ */
+function showTranscribedText(text, errorMsg = null) {
+  // Create or get text display element
+  let textDisplay = document.getElementById('transcribed-text-display');
+
+  if (!textDisplay) {
+    textDisplay = document.createElement('div');
+    textDisplay.id = 'transcribed-text-display';
+    textDisplay.className = 'transcribed-text';
+
+    // Add copy button
+    const copyBtn = document.createElement('button');
+    copyBtn.className = 'copy-btn';
+    copyBtn.textContent = 'Copy';
+    copyBtn.addEventListener('click', () => {
+      navigator.clipboard.writeText(text).then(() => {
+        copyBtn.textContent = 'Copied!';
+        setTimeout(() => {
+          copyBtn.textContent = 'Copy';
+        }, 2000);
+      });
+    });
+
+    const textContent = document.createElement('div');
+    textContent.className = 'text-content';
+    textContent.textContent = text;
+
+    textDisplay.appendChild(textContent);
+    textDisplay.appendChild(copyBtn);
+
+    // Insert after status message
+    const container = document.querySelector('.container main');
+    const recordingSection = document.querySelector('.recording-section');
+    container.insertBefore(textDisplay, recordingSection.nextSibling);
+  } else {
+    // Update existing display
+    const textContent = textDisplay.querySelector('.text-content');
+    textContent.textContent = text;
+
+    // Update copy button to copy new text
+    const copyBtn = textDisplay.querySelector('.copy-btn');
+    copyBtn.textContent = 'Copy';
+    copyBtn.onclick = () => {
+      navigator.clipboard.writeText(text).then(() => {
+        copyBtn.textContent = 'Copied!';
+        setTimeout(() => {
+          copyBtn.textContent = 'Copy';
+        }, 2000);
+      });
+    };
+
+    textDisplay.classList.remove('hidden');
+  }
+
+  // Show appropriate status message
+  if (errorMsg) {
+    showStatus(errorMsg + '. Text shown below (click to copy)', 'info');
+  } else {
+    showStatus('Transcription complete. Text shown below (click to copy)', 'info');
+  }
+}
+
+/**
+ * Hide transcribed text display
+ */
+function hideTranscribedText() {
+  const textDisplay = document.getElementById('transcribed-text-display');
+  if (textDisplay) {
+    textDisplay.classList.add('hidden');
+  }
+}
+
+/**
+ * Open settings page
+ */
+function openSettings() {
+  browser.runtime.openOptionsPage();
+}
+
+// Handle keyboard shortcuts within the popup
+document.addEventListener('keydown', (event) => {
+  // Space or Enter to stop recording
+  if ((event.key === ' ' || event.key === 'Enter') && isRecording) {
+    event.preventDefault();
+    stopRecording();
+    return;
+  }
+
+  // Escape to stop recording (and popup will close)
+  if (event.key === 'Escape' && isRecording) {
+    event.preventDefault();
+    stopRecording();
+    // Don't close popup immediately, let transcription happen
+    // User can press Esc again to close if needed
+    return;
+  }
+});
+
+// Listen for window focus to handle popup reopening
+window.addEventListener('focus', () => {
+  // If popup regains focus and not recording, don't auto-start again
+  // This prevents re-recording when popup is clicked
+});
+
+console.log('Voice to Text popup loaded');
diff --git a/src/settings/settings.css b/src/settings/settings.css
new file mode 100644
index 0000000..23edcc7
--- /dev/null
+++ b/src/settings/settings.css
@@ -0,0 +1,349 @@
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+body {
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
+  background: #f5f5f5;
+  color: #333;
+  line-height: 1.6;
+}
+
+.container {
+  max-width: 800px;
+  margin: 0 auto;
+  background: white;
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+}
+
+header {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  padding: 32px 24px;
+  text-align: center;
+}
+
+header h1 {
+  font-size: 28px;
+  font-weight: 600;
+  margin-bottom: 8px;
+}
+
+header p {
+  font-size: 16px;
+  opacity: 0.9;
+}
+
+main {
+  flex: 1;
+  padding: 32px 24px;
+}
+
+.status-message {
+  padding: 16px;
+  border-radius: 8px;
+  margin-bottom: 24px;
+  font-size: 14px;
+  text-align: center;
+  transition: all 0.3s ease;
+}
+
+.status-message.hidden {
+  display: none;
+}
+
+.status-message.error {
+  background: #fee;
+  color: #c33;
+  border: 1px solid #fcc;
+}
+
+.status-message.success {
+  background: #efe;
+  color: #3a3;
+  border: 1px solid #cfc;
+}
+
+.status-message.info {
+  background: #eef;
+  color: #33a;
+  border: 1px solid #ccf;
+}
+
+.form-section {
+  margin-bottom: 32px;
+  padding: 24px;
+  background: #fafafa;
+  border-radius: 8px;
+  border: 1px solid #e0e0e0;
+}
+
+.form-section h2 {
+  font-size: 20px;
+  font-weight: 600;
+  margin-bottom: 8px;
+  color: #333;
+}
+
+.form-section h3 {
+  font-size: 16px;
+  font-weight: 600;
+  margin-bottom: 12px;
+  color: #555;
+}
+
+.section-description {
+  font-size: 14px;
+  color: #666;
+  margin-bottom: 16px;
+}
+
+.form-group {
+  margin-bottom: 20px;
+  position: relative;
+}
+
+.form-group:last-child {
+  margin-bottom: 0;
+}
+
+label {
+  display: block;
+  font-weight: 500;
+  margin-bottom: 8px;
+  color: #444;
+  font-size: 14px;
+}
+
+.help-text {
+  display: block;
+  font-weight: 400;
+  font-size: 12px;
+  color: #888;
+  margin-top: 4px;
+}
+
+input[type="url"],
+input[type="password"],
+input[type="text"] {
+  width: 100%;
+  padding: 12px;
+  border: 1px solid #d0d0d0;
+  border-radius: 6px;
+  font-size: 14px;
+  font-family: inherit;
+  transition: all 0.2s ease;
+  background: white;
+}
+
+input:focus {
+  outline: none;
+  border-color: #667eea;
+  box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
+}
+
+.toggle-btn {
+  position: absolute;
+  right: 8px;
+  bottom: 8px;
+  padding: 4px 12px;
+  background: #f0f0f0;
+  border: 1px solid #d0d0d0;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 12px;
+  color: #666;
+  transition: all 0.2s ease;
+}
+
+.toggle-btn:hover {
+  background: #e8e8e8;
+}
+
+.preset-buttons {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+  gap: 12px;
+}
+
+.preset-btn {
+  padding: 16px;
+  background: white;
+  border: 2px solid #d0d0d0;
+  border-radius: 8px;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  text-align: left;
+}
+
+.preset-btn:hover {
+  border-color: #667eea;
+  transform: translateY(-2px);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.preset-btn.active {
+  border-color: #667eea;
+  background: #f0f4ff;
+}
+
+.preset-name {
+  font-weight: 600;
+  font-size: 16px;
+  color: #333;
+  margin-bottom: 4px;
+}
+
+.preset-desc {
+  font-size: 13px;
+  color: #666;
+}
+
+.info-section ul {
+  list-style: none;
+  padding: 0;
+}
+
+.info-section li {
+  padding: 8px 0;
+  font-size: 14px;
+  color: #555;
+}
+
+.info-section a {
+  color: #667eea;
+  text-decoration: none;
+}
+
+.info-section a:hover {
+  text-decoration: underline;
+}
+
+.form-actions {
+  display: flex;
+  gap: 12px;
+  margin-top: 24px;
+  flex-wrap: wrap;
+}
+
+.btn {
+  padding: 12px 24px;
+  border: none;
+  border-radius: 6px;
+  font-size: 14px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  font-family: inherit;
+}
+
+.btn-primary {
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: white;
+  flex: 1;
+  min-width: 150px;
+}
+
+.btn-primary:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
+}
+
+.btn-primary:active {
+  transform: translateY(0);
+}
+
+.btn-primary:disabled {
+  opacity: 0.6;
+  cursor: not-allowed;
+  transform: none;
+}
+
+.btn-secondary {
+  background: white;
+  color: #667eea;
+  border: 1px solid #667eea;
+}
+
+.btn-secondary:hover {
+  background: #f0f4ff;
+}
+
+kbd {
+  display: inline-block;
+  padding: 3px 8px;
+  background: white;
+  border: 1px solid #ccc;
+  border-radius: 4px;
+  font-size: 13px;
+  font-family: monospace;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+}
+
+code {
+  background: white;
+  padding: 2px 6px;
+  border-radius: 3px;
+  font-size: 13px;
+  color: #667eea;
+  border: 1px solid #e0e0e0;
+}
+
+.mic-status {
+  margin-top: 12px;
+  padding: 12px;
+  border-radius: 6px;
+  font-size: 14px;
+  font-weight: 500;
+}
+
+.mic-status.hidden {
+  display: none;
+}
+
+.mic-status.granted {
+  background: #efe;
+  color: #3a3;
+  border: 1px solid #cfc;
+}
+
+.mic-status.denied {
+  background: #fee;
+  color: #c33;
+  border: 1px solid #fcc;
+}
+
+footer {
+  padding: 20px;
+  text-align: center;
+  background: #fafafa;
+  border-top: 1px solid #e0e0e0;
+  font-size: 14px;
+  color: #888;
+}
+
+@media (max-width: 600px) {
+  .container {
+    margin: 0;
+  }
+
+  main {
+    padding: 20px 16px;
+  }
+
+  .form-actions {
+    flex-direction: column;
+  }
+
+  .btn {
+    width: 100%;
+  }
+
+  .preset-buttons {
+    grid-template-columns: 1fr;
+  }
+}
diff --git a/src/settings/settings.html b/src/settings/settings.html
new file mode 100644
index 0000000..f5fb6f6
--- /dev/null
+++ b/src/settings/settings.html
@@ -0,0 +1,142 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Voice to Text - Settings</title>
+  <link rel="stylesheet" href="settings.css">
+</head>
+<body>
+  <div class="container">
+    <header>
+      <h1>Voice to Text Settings</h1>
+      <p>Configure your API settings for speech transcription</p>
+    </header>
+
+    <main>
+      <div id="status-message" class="status-message hidden"></div>
+
+      <form id="settings-form">
+        <div class="form-section">
+          <h2>Microphone Permission</h2>
+          <p class="section-description">
+            Before using the extension, you need to grant microphone access.
+          </p>
+          <button type="button" id="request-mic-btn" class="btn btn-primary">
+            Grant Microphone Access
+          </button>
+          <p id="mic-status" class="mic-status hidden"></p>
+        </div>
+
+        <div class="form-section">
+          <h2>API Configuration</h2>
+
+          <div class="form-group">
+            <label for="api-endpoint">
+              API Endpoint
+              <span class="help-text">The URL of the Whisper API endpoint</span>
+            </label>
+            <input
+              type="url"
+              id="api-endpoint"
+              name="apiEndpoint"
+              placeholder="https://api.openai.com/v1/audio/transcriptions"
+              required
+            />
+          </div>
+
+          <div class="form-group">
+            <label for="api-token">
+              API Token
+              <span class="help-text">Your API authentication token (stored securely in browser)</span>
+            </label>
+            <input
+              type="password"
+              id="api-token"
+              name="apiToken"
+              placeholder="sk-..."
+              required
+            />
+            <button type="button" id="toggle-token" class="toggle-btn">Show</button>
+          </div>
+
+          <div class="form-group">
+            <label for="model">
+              Model
+              <span class="help-text">The model name to use for transcription</span>
+            </label>
+            <input
+              type="text"
+              id="model"
+              name="model"
+              placeholder="whisper-1"
+              required
+            />
+          </div>
+        </div>
+
+        <div class="form-section presets">
+          <h2>Quick Presets</h2>
+          <p class="section-description">Load configuration for popular providers</p>
+
+          <div class="preset-buttons">
+            <button type="button" class="preset-btn" data-provider="openai">
+              <div class="preset-name">OpenAI</div>
+              <div class="preset-desc">Official OpenAI API</div>
+            </button>
+
+            <button type="button" class="preset-btn" data-provider="groq">
+              <div class="preset-name">Groq</div>
+              <div class="preset-desc">Fast Whisper inference</div>
+            </button>
+
+            <button type="button" class="preset-btn" data-provider="custom">
+              <div class="preset-name">Custom</div>
+              <div class="preset-desc">Self-hosted or other provider</div>
+            </button>
+          </div>
+        </div>
+
+        <div class="form-section info-section">
+          <h3>Getting Started</h3>
+          <ul>
+            <li><strong>OpenAI:</strong> Get your API key from <a href="https://platform.openai.com/api-keys" target="_blank">platform.openai.com</a></li>
+            <li><strong>Groq:</strong> Sign up at <a href="https://console.groq.com" target="_blank">console.groq.com</a> for free API access</li>
+            <li><strong>Custom:</strong> Use any Whisper-compatible API endpoint</li>
+          </ul>
+        </div>
+
+        <div class="form-actions">
+          <button type="submit" id="save-btn" class="btn btn-primary">
+            Save Settings
+          </button>
+          <button type="button" id="test-btn" class="btn btn-secondary">
+            Test Connection
+          </button>
+          <button type="button" id="reset-btn" class="btn btn-secondary">
+            Reset to Defaults
+          </button>
+        </div>
+      </form>
+
+      <div class="form-section">
+        <h2>Keyboard Shortcut</h2>
+        <p class="section-description">
+          The default keyboard shortcut is <kbd>Ctrl</kbd>+<kbd>Alt</kbd>+<kbd>S</kbd>
+          (<kbd>Cmd</kbd>+<kbd>Alt</kbd>+<kbd>S</kbd> on Mac).
+        </p>
+        <p class="section-description">
+          To customize the shortcut, visit <code>about:addons</code> in Firefox,
+          click the gear icon, select "Manage Extension Shortcuts", and find "Voice to Text".
+        </p>
+      </div>
+    </main>
+
+    <footer>
+      <p>&copy; 2024 Voice to Text Extension</p>
+    </footer>
+  </div>
+
+  <script src="settings.js"></script>
+</body>
+</html>
diff --git a/src/settings/settings.js b/src/settings/settings.js
new file mode 100644
index 0000000..20a47e1
--- /dev/null
+++ b/src/settings/settings.js
@@ -0,0 +1,461 @@
+/**
+ * Settings script for Voice to Text extension
+ * Handles configuration management
+ */
+
+// Preset configurations
+const PRESETS = {
+  openai: {
+    apiEndpoint: 'https://api.openai.com/v1/audio/transcriptions',
+    model: 'whisper-1'
+  },
+  groq: {
+    apiEndpoint: 'https://api.groq.com/openai/v1/audio/transcriptions',
+    model: 'whisper-large-v3'
+  },
+  custom: {
+    apiEndpoint: '',
+    model: 'whisper-1'
+  }
+};
+
+// DOM elements
+const form = document.getElementById('settings-form');
+const apiEndpointInput = document.getElementById('api-endpoint');
+const apiTokenInput = document.getElementById('api-token');
+const modelInput = document.getElementById('model');
+const toggleTokenBtn = document.getElementById('toggle-token');
+const saveBtn = document.getElementById('save-btn');
+const testBtn = document.getElementById('test-btn');
+const resetBtn = document.getElementById('reset-btn');
+const statusMessage = document.getElementById('status-message');
+const presetButtons = document.querySelectorAll('.preset-btn');
+const requestMicBtn = document.getElementById('request-mic-btn');
+const micStatus = document.getElementById('mic-status');
+
+// Initialize
+document.addEventListener('DOMContentLoaded', () => {
+  loadSettings();
+  attachEventListeners();
+  checkMicrophonePermission();
+});
+
+/**
+ * Attach event listeners
+ */
+function attachEventListeners() {
+  form.addEventListener('submit', handleSubmit);
+  toggleTokenBtn.addEventListener('click', toggleTokenVisibility);
+  testBtn.addEventListener('click', testConnection);
+  resetBtn.addEventListener('click', resetToDefaults);
+  requestMicBtn.addEventListener('click', requestMicrophonePermission);
+
+  presetButtons.forEach(btn => {
+    btn.addEventListener('click', () => {
+      const provider = btn.dataset.provider;
+      applyPreset(provider);
+    });
+  });
+}
+
+/**
+ * Check if microphone permission is already granted
+ */
+async function checkMicrophonePermission() {
+  try {
+    // Try to query permission state using Permissions API
+    if (navigator.permissions && navigator.permissions.query) {
+      try {
+        const result = await navigator.permissions.query({ name: 'microphone' });
+        updateMicrophoneStatus(result.state);
+
+        // Listen for permission changes
+        result.addEventListener('change', () => {
+          updateMicrophoneStatus(result.state);
+        });
+
+        // If not granted, show a reminder
+        if (result.state !== 'granted') {
+          showStatus('Please grant microphone access to use the extension', 'info');
+        }
+      } catch (queryError) {
+        // Permissions API query failed, try direct access check
+        console.log('Permissions query failed, trying direct check:', queryError);
+        await checkMicrophoneAccessDirectly();
+      }
+    } else {
+      // Permissions API not supported, try direct check
+      await checkMicrophoneAccessDirectly();
+    }
+  } catch (error) {
+    console.log('Permission check not supported, will request on button click');
+  }
+}
+
+/**
+ * Check microphone access directly by attempting to access it
+ */
+async function checkMicrophoneAccessDirectly() {
+  try {
+    // Try to get media devices list to check if we have access
+    const devices = await navigator.mediaDevices.enumerateDevices();
+    const hasAudioInput = devices.some(device => device.kind === 'audioinput');
+
+    if (hasAudioInput) {
+      // We can see audio devices, check if we have permission
+      const audioDevicesWithLabel = devices.filter(
+        device => device.kind === 'audioinput' && device.label !== ''
+      );
+
+      if (audioDevicesWithLabel.length > 0) {
+        // If we can see device labels, we have permission
+        updateMicrophoneStatus('granted');
+      } else {
+        // No labels means no permission yet
+        updateMicrophoneStatus('prompt');
+        showStatus('Please grant microphone access to use the extension', 'info');
+      }
+    } else {
+      // No audio input devices found
+      updateMicrophoneStatus('prompt');
+    }
+  } catch (error) {
+    console.error('Error checking microphone access:', error);
+    updateMicrophoneStatus('prompt');
+  }
+}
+
+/**
+ * Request microphone permission
+ */
+async function requestMicrophonePermission() {
+  requestMicBtn.disabled = true;
+  requestMicBtn.textContent = 'Requesting...';
+
+  try {
+    // Request microphone access
+    const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+
+    // Permission granted - stop the stream immediately
+    stream.getTracks().forEach(track => track.stop());
+
+    updateMicrophoneStatus('granted');
+    showStatus('Microphone permission granted! You can now use the extension.', 'success');
+
+  } catch (error) {
+    console.error('Microphone permission error:', error);
+
+    if (error.name === 'NotAllowedError') {
+      updateMicrophoneStatus('denied');
+      showStatus('Microphone permission denied. Please allow access in your browser settings.', 'error');
+    } else if (error.name === 'NotFoundError') {
+      updateMicrophoneStatus('denied');
+      showStatus('No microphone found. Please connect a microphone and try again.', 'error');
+    } else {
+      updateMicrophoneStatus('denied');
+      showStatus('Error accessing microphone: ' + error.message, 'error');
+    }
+  } finally {
+    requestMicBtn.disabled = false;
+    requestMicBtn.textContent = 'Grant Microphone Access';
+  }
+}
+
+/**
+ * Update microphone permission status UI
+ */
+function updateMicrophoneStatus(state) {
+  micStatus.classList.remove('hidden', 'granted', 'denied');
+
+  if (state === 'granted') {
+    micStatus.classList.add('granted');
+    micStatus.textContent = '✓ Microphone access granted';
+    requestMicBtn.textContent = 'Microphone Access Granted';
+    requestMicBtn.disabled = true;
+  } else if (state === 'denied') {
+    micStatus.classList.add('denied');
+    micStatus.textContent = '✗ Microphone access denied';
+    requestMicBtn.textContent = 'Grant Microphone Access';
+    requestMicBtn.disabled = false;
+  } else if (state === 'prompt') {
+    micStatus.classList.add('hidden');
+    requestMicBtn.textContent = 'Grant Microphone Access';
+    requestMicBtn.disabled = false;
+  }
+}
+
+/**
+ * Load settings from storage
+ */
+async function loadSettings() {
+  try {
+    const result = await browser.storage.sync.get(['apiEndpoint', 'apiToken', 'model']);
+
+    if (result.apiEndpoint) {
+      apiEndpointInput.value = result.apiEndpoint;
+    } else {
+      apiEndpointInput.value = PRESETS.openai.apiEndpoint;
+    }
+
+    if (result.apiToken) {
+      apiTokenInput.value = result.apiToken;
+    }
+
+    if (result.model) {
+      modelInput.value = result.model;
+    } else {
+      modelInput.value = PRESETS.openai.model;
+    }
+
+    // Highlight active preset
+    updateActivePreset();
+
+  } catch (error) {
+    console.error('Error loading settings:', error);
+    showStatus('Error loading settings: ' + error.message, 'error');
+  }
+}
+
+/**
+ * Handle form submission
+ */
+async function handleSubmit(event) {
+  event.preventDefault();
+
+  const settings = {
+    apiEndpoint: apiEndpointInput.value.trim(),
+    apiToken: apiTokenInput.value.trim(),
+    model: modelInput.value.trim()
+  };
+
+  // Validate settings
+  if (!settings.apiEndpoint) {
+    showStatus('Please enter an API endpoint', 'error');
+    return;
+  }
+
+  if (!settings.apiToken) {
+    showStatus('Please enter an API token', 'error');
+    return;
+  }
+
+  if (!settings.model) {
+    showStatus('Please enter a model name', 'error');
+    return;
+  }
+
+  // Validate URL
+  try {
+    new URL(settings.apiEndpoint);
+  } catch (e) {
+    showStatus('Please enter a valid URL for the API endpoint', 'error');
+    return;
+  }
+
+  try {
+    saveBtn.disabled = true;
+    saveBtn.textContent = 'Saving...';
+
+    await browser.storage.sync.set(settings);
+
+    showStatus('Settings saved successfully!', 'success');
+    updateActivePreset();
+
+    // Reset button text after a delay
+    setTimeout(() => {
+      saveBtn.disabled = false;
+      saveBtn.textContent = 'Save Settings';
+    }, 2000);
+
+  } catch (error) {
+    console.error('Error saving settings:', error);
+    showStatus('Error saving settings: ' + error.message, 'error');
+    saveBtn.disabled = false;
+    saveBtn.textContent = 'Save Settings';
+  }
+}
+
+/**
+ * Toggle API token visibility
+ */
+function toggleTokenVisibility() {
+  if (apiTokenInput.type === 'password') {
+    apiTokenInput.type = 'text';
+    toggleTokenBtn.textContent = 'Hide';
+  } else {
+    apiTokenInput.type = 'password';
+    toggleTokenBtn.textContent = 'Show';
+  }
+}
+
+/**
+ * Test API connection
+ */
+async function testConnection() {
+  const settings = {
+    apiEndpoint: apiEndpointInput.value.trim(),
+    apiToken: apiTokenInput.value.trim(),
+    model: modelInput.value.trim()
+  };
+
+  if (!settings.apiEndpoint || !settings.apiToken) {
+    showStatus('Please fill in API endpoint and token first', 'error');
+    return;
+  }
+
+  try {
+    testBtn.disabled = true;
+    testBtn.textContent = 'Testing...';
+
+    showStatus('Testing connection to API...', 'info');
+
+    // Create a minimal audio file for testing (1 second of silence)
+    const sampleRate = 16000;
+    const duration = 1;
+    const numSamples = sampleRate * duration;
+    const buffer = new ArrayBuffer(44 + numSamples * 2);
+    const view = new DataView(buffer);
+
+    // WAV header
+    const writeString = (offset, string) => {
+      for (let i = 0; i < string.length; i++) {
+        view.setUint8(offset + i, string.charCodeAt(i));
+      }
+    };
+
+    writeString(0, 'RIFF');
+    view.setUint32(4, 36 + numSamples * 2, true);
+    writeString(8, 'WAVE');
+    writeString(12, 'fmt ');
+    view.setUint32(16, 16, true);
+    view.setUint16(20, 1, true);
+    view.setUint16(22, 1, true);
+    view.setUint32(24, sampleRate, true);
+    view.setUint32(28, sampleRate * 2, true);
+    view.setUint16(32, 2, true);
+    view.setUint16(34, 16, true);
+    writeString(36, 'data');
+    view.setUint32(40, numSamples * 2, true);
+
+    const blob = new Blob([buffer], { type: 'audio/wav' });
+
+    // Test API request
+    const formData = new FormData();
+    formData.append('file', blob, 'test.wav');
+    formData.append('model', settings.model);
+
+    const response = await fetch(settings.apiEndpoint, {
+      method: 'POST',
+      headers: {
+        'Authorization': `Bearer ${settings.apiToken}`
+      },
+      body: formData
+    });
+
+    if (response.ok) {
+      showStatus('Connection successful! API is working correctly.', 'success');
+    } else {
+      const errorText = await response.text();
+      let errorMessage = `Connection failed: ${response.status} ${response.statusText}`;
+
+      try {
+        const errorJson = JSON.parse(errorText);
+        if (errorJson.error && errorJson.error.message) {
+          errorMessage = errorJson.error.message;
+        }
+      } catch (e) {
+        if (errorText) {
+          errorMessage += ` - ${errorText}`;
+        }
+      }
+
+      showStatus(errorMessage, 'error');
+    }
+
+  } catch (error) {
+    console.error('Connection test error:', error);
+    showStatus('Connection test failed: ' + error.message, 'error');
+  } finally {
+    testBtn.disabled = false;
+    testBtn.textContent = 'Test Connection';
+  }
+}
+
+/**
+ * Reset settings to defaults
+ */
+async function resetToDefaults() {
+  if (!confirm('Are you sure you want to reset all settings to defaults?')) {
+    return;
+  }
+
+  try {
+    await browser.storage.sync.clear();
+    await loadSettings();
+    showStatus('Settings reset to defaults', 'success');
+  } catch (error) {
+    console.error('Error resetting settings:', error);
+    showStatus('Error resetting settings: ' + error.message, 'error');
+  }
+}
+
+/**
+ * Apply preset configuration
+ */
+function applyPreset(provider) {
+  const preset = PRESETS[provider];
+
+  if (!preset) {
+    return;
+  }
+
+  if (preset.apiEndpoint) {
+    apiEndpointInput.value = preset.apiEndpoint;
+  }
+
+  if (preset.model) {
+    modelInput.value = preset.model;
+  }
+
+  // Don't clear the API token when switching presets
+  // Just highlight the selected preset
+  updateActivePreset();
+
+  showStatus(`${provider.toUpperCase()} preset applied. Don't forget to enter your API token!`, 'info');
+}
+
+/**
+ * Update active preset button
+ */
+function updateActivePreset() {
+  const currentEndpoint = apiEndpointInput.value;
+
+  presetButtons.forEach(btn => {
+    const provider = btn.dataset.provider;
+    const preset = PRESETS[provider];
+
+    if (preset.apiEndpoint === currentEndpoint) {
+      btn.classList.add('active');
+    } else {
+      btn.classList.remove('active');
+    }
+  });
+}
+
+/**
+ * Show status message
+ */
+function showStatus(message, type = 'info') {
+  statusMessage.textContent = message;
+  statusMessage.className = `status-message ${type}`;
+  statusMessage.classList.remove('hidden');
+
+  // Auto-hide after 5 seconds for success messages
+  if (type === 'success') {
+    setTimeout(() => {
+      statusMessage.classList.add('hidden');
+    }, 5000);
+  }
+}
+
+console.log('Voice to Text settings page loaded');
diff --git a/src/utils/audio-processor.js b/src/utils/audio-processor.js
new file mode 100644
index 0000000..8b38ed3
--- /dev/null
+++ b/src/utils/audio-processor.js
@@ -0,0 +1,219 @@
+/**
+ * Audio processor utility for Voice to Text extension
+ * Handles audio format conversion and processing
+ */
+
+/**
+ * Convert audio blob to different format if needed
+ * @param {Blob} audioBlob - The input audio blob
+ * @param {string} targetFormat - Target format (e.g., 'wav', 'mp3')
+ * @returns {Promise<Blob>} - Converted audio blob
+ */
+async function convertAudioFormat(audioBlob, targetFormat = 'wav') {
+  // For now, we'll return the original blob as most APIs accept webm
+  // This function can be extended with AudioContext for actual conversion
+  return audioBlob;
+}
+
+/**
+ * Get audio duration from blob
+ * @param {Blob} audioBlob - The audio blob
+ * @returns {Promise<number>} - Duration in seconds
+ */
+function getAudioDuration(audioBlob) {
+  return new Promise((resolve, reject) => {
+    const audio = new Audio();
+    audio.addEventListener('loadedmetadata', () => {
+      resolve(audio.duration);
+    });
+    audio.addEventListener('error', reject);
+    audio.src = URL.createObjectURL(audioBlob);
+  });
+}
+
+/**
+ * Resample audio to target sample rate using Web Audio API
+ * @param {Blob} audioBlob - The input audio blob
+ * @param {number} targetSampleRate - Target sample rate (e.g., 16000)
+ * @returns {Promise<Blob>} - Resampled audio blob
+ */
+async function resampleAudio(audioBlob, targetSampleRate = 16000) {
+  try {
+    const audioContext = new (window.AudioContext || window.webkitAudioContext)();
+    const arrayBuffer = await audioBlob.arrayBuffer();
+    const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
+
+    // If already at target sample rate, return original
+    if (audioBuffer.sampleRate === targetSampleRate) {
+      return audioBlob;
+    }
+
+    // Create offline context with target sample rate
+    const offlineContext = new OfflineAudioContext(
+      audioBuffer.numberOfChannels,
+      audioBuffer.duration * targetSampleRate,
+      targetSampleRate
+    );
+
+    // Create buffer source
+    const source = offlineContext.createBufferSource();
+    source.buffer = audioBuffer;
+    source.connect(offlineContext.destination);
+    source.start(0);
+
+    // Render audio
+    const resampledBuffer = await offlineContext.startRendering();
+
+    // Convert to WAV
+    const wavBlob = audioBufferToWav(resampledBuffer);
+
+    return wavBlob;
+
+  } catch (error) {
+    console.error('Error resampling audio:', error);
+    // Return original on error
+    return audioBlob;
+  }
+}
+
+/**
+ * Convert AudioBuffer to WAV blob
+ * @param {AudioBuffer} audioBuffer - The audio buffer
+ * @returns {Blob} - WAV blob
+ */
+function audioBufferToWav(audioBuffer) {
+  const numberOfChannels = audioBuffer.numberOfChannels;
+  const sampleRate = audioBuffer.sampleRate;
+  const format = 1; // PCM
+  const bitDepth = 16;
+
+  const bytesPerSample = bitDepth / 8;
+  const blockAlign = numberOfChannels * bytesPerSample;
+
+  const data = [];
+  for (let i = 0; i < numberOfChannels; i++) {
+    data.push(audioBuffer.getChannelData(i));
+  }
+
+  const interleaved = interleaveChannels(data);
+  const dataLength = interleaved.length * bytesPerSample;
+  const buffer = new ArrayBuffer(44 + dataLength);
+  const view = new DataView(buffer);
+
+  // Write WAV header
+  writeString(view, 0, 'RIFF');
+  view.setUint32(4, 36 + dataLength, true);
+  writeString(view, 8, 'WAVE');
+  writeString(view, 12, 'fmt ');
+  view.setUint32(16, 16, true);
+  view.setUint16(20, format, true);
+  view.setUint16(22, numberOfChannels, true);
+  view.setUint32(24, sampleRate, true);
+  view.setUint32(28, sampleRate * blockAlign, true);
+  view.setUint16(32, blockAlign, true);
+  view.setUint16(34, bitDepth, true);
+  writeString(view, 36, 'data');
+  view.setUint32(40, dataLength, true);
+
+  // Write audio data
+  floatTo16BitPCM(view, 44, interleaved);
+
+  return new Blob([buffer], { type: 'audio/wav' });
+}
+
+/**
+ * Interleave multiple audio channels
+ * @param {Array<Float32Array>} channels - Array of channel data
+ * @returns {Float32Array} - Interleaved data
+ */
+function interleaveChannels(channels) {
+  const length = channels[0].length;
+  const numberOfChannels = channels.length;
+  const result = new Float32Array(length * numberOfChannels);
+
+  let offset = 0;
+  for (let i = 0; i < length; i++) {
+    for (let channel = 0; channel < numberOfChannels; channel++) {
+      result[offset++] = channels[channel][i];
+    }
+  }
+
+  return result;
+}
+
+/**
+ * Convert float samples to 16-bit PCM
+ * @param {DataView} view - DataView to write to
+ * @param {number} offset - Offset in the view
+ * @param {Float32Array} input - Input float samples
+ */
+function floatTo16BitPCM(view, offset, input) {
+  for (let i = 0; i < input.length; i++, offset += 2) {
+    const s = Math.max(-1, Math.min(1, input[i]));
+    view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
+  }
+}
+
+/**
+ * Write string to DataView
+ * @param {DataView} view - DataView to write to
+ * @param {number} offset - Offset in the view
+ * @param {string} string - String to write
+ */
+function writeString(view, offset, string) {
+  for (let i = 0; i < string.length; i++) {
+    view.setUint8(offset + i, string.charCodeAt(i));
+  }
+}
+
+/**
+ * Normalize audio volume
+ * @param {Blob} audioBlob - The input audio blob
+ * @returns {Promise<Blob>} - Normalized audio blob
+ */
+async function normalizeAudio(audioBlob) {
+  try {
+    const audioContext = new (window.AudioContext || window.webkitAudioContext)();
+    const arrayBuffer = await audioBlob.arrayBuffer();
+    const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
+
+    // Find peak value
+    let peak = 0;
+    for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
+      const data = audioBuffer.getChannelData(channel);
+      for (let i = 0; i < data.length; i++) {
+        const abs = Math.abs(data[i]);
+        if (abs > peak) peak = abs;
+      }
+    }
+
+    // Normalize if needed
+    if (peak > 0 && peak < 0.95) {
+      const gain = 0.95 / peak;
+      for (let channel = 0; channel < audioBuffer.numberOfChannels; channel++) {
+        const data = audioBuffer.getChannelData(channel);
+        for (let i = 0; i < data.length; i++) {
+          data[i] *= gain;
+        }
+      }
+    }
+
+    // Convert to WAV
+    return audioBufferToWav(audioBuffer);
+
+  } catch (error) {
+    console.error('Error normalizing audio:', error);
+    return audioBlob;
+  }
+}
+
+// Export functions if using modules
+if (typeof module !== 'undefined' && module.exports) {
+  module.exports = {
+    convertAudioFormat,
+    getAudioDuration,
+    resampleAudio,
+    audioBufferToWav,
+    normalizeAudio
+  };
+}