From 979a5bbdae80c28d38a8963a4f478928dd161236 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Tue, 2 Jun 2026 18:39:04 +0300 Subject: [PATCH] Auto-deploy: 2026-06-02 18:39:04 --- background.js | 45 +------------------ manifest.json | 12 +++++- popup.js | 65 ++++++++++++++++++++-------- speech.html | 10 +++++ speech.js | 117 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 186 insertions(+), 63 deletions(-) create mode 100644 speech.html create mode 100644 speech.js diff --git a/background.js b/background.js index 526aeec..af0603f 100644 --- a/background.js +++ b/background.js @@ -4,25 +4,7 @@ const GEMINI_MODEL = 'gemini-flash-lite-latest'; const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent`; -// ─── Offscreen Document Management ─────────────────────────────────────────── - -async function setupOffscreenDocument(path) { - const offscreenUrl = chrome.runtime.getURL(path); - const existingContexts = await chrome.runtime.getContexts({ - contextTypes: ['OFFSCREEN_DOCUMENT'], - documentUrls: [offscreenUrl] - }); - - if (existingContexts.length > 0) { - return; - } - - await chrome.offscreen.createDocument({ - url: path, - reasons: ['USER_MEDIA'], - justification: 'Recording microphone input for speech recognition', - }); -} +// (Offscreen document logic removed in favor of iframe approach) chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.type === 'GEMINI_REQUEST') { @@ -43,31 +25,6 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { chrome.tabs.create({ url: chrome.runtime.getURL('permission.html') }); return true; } - - // --- Offscreen Document Relays for Popup --- - if (message.type === 'START_RECORDING_FROM_POPUP') { - setupOffscreenDocument('claude-arabic-voice/offscreen.html') - .then(() => { - chrome.runtime.sendMessage({ - type: 'START_RECORDING', - payload: message.payload - }, (response) => { - sendResponse(response || { success: true }); - }); - }) - .catch(err => { - console.error('Failed to setup offscreen doc', err); - sendResponse({ success: false, error: err.message }); - }); - return true; - } - - if (message.type === 'STOP_RECORDING_FROM_POPUP') { - chrome.runtime.sendMessage({ type: 'STOP_RECORDING' }, (response) => { - sendResponse(response || { success: true }); - }); - return true; - } }); // ─── Core API call ─────────────────────────────────────────────────────────── diff --git a/manifest.json b/manifest.json index e9e7d66..31c78f9 100644 --- a/manifest.json +++ b/manifest.json @@ -8,8 +8,7 @@ "activeTab", "scripting", "clipboardWrite", - "microphone", - "offscreen" + "microphone" ], "host_permissions": [ "https://www.linkedin.com/*", @@ -54,6 +53,15 @@ "128": "icons/icon128.png" } }, + "web_accessible_resources": [ + { + "resources": [ + "speech.html", + "speech.js" + ], + "matches": [""] + } + ], "background": { "service_worker": "background.js" }, diff --git a/popup.js b/popup.js index ce828fd..44ed4cf 100644 --- a/popup.js +++ b/popup.js @@ -318,24 +318,55 @@ function initDictation() { if (isRecording) { stopRecording(true); } else { - isRecording = true; - micBtn.style.background = 'linear-gradient(135deg, #ff4d6d, #c9184a)'; - micBtn.innerHTML = '🔴'; - statusEl.textContent = 'جارٍ الاستماع... اضغط للإيقاف'; - finalTranscript = ''; - interimTranscript = ''; - resultArea.value = ''; - copyBtn.style.display = 'none'; - - chrome.runtime.sendMessage({ - type: 'START_RECORDING_FROM_POPUP', - payload: { language: 'ar-SA' } - }, (response) => { - if (!response || !response.success) { - console.error('Failed to start recording', response); - statusEl.textContent = '❌ فشل بدء التسجيل'; - stopRecording(false); + // First, get the active tab + chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { + const activeTab = tabs[0]; + + // Cannot inject into chrome:// or chrome-extension:// pages + if (!activeTab || !activeTab.url || activeTab.url.startsWith('chrome://') || activeTab.url.startsWith('chrome-extension://') || activeTab.url.startsWith('edge://')) { + statusEl.textContent = '❌ يرجى فتح موقع عادي أولاً (مثل جوجل أو لينكدإن)'; + return; } + + isRecording = true; + micBtn.style.background = 'linear-gradient(135deg, #ff4d6d, #c9184a)'; + micBtn.innerHTML = '🔴'; + statusEl.textContent = 'جارٍ الاستماع... اضغط للإيقاف'; + finalTranscript = ''; + interimTranscript = ''; + resultArea.value = ''; + copyBtn.style.display = 'none'; + + const extensionUrl = chrome.runtime.getURL('speech.html'); + + chrome.scripting.executeScript({ + target: { tabId: activeTab.id }, + func: (url) => { + let iframe = document.getElementById('lja-dictation-frame'); + if (!iframe) { + iframe = document.createElement('iframe'); + iframe.id = 'lja-dictation-frame'; + iframe.src = url; + iframe.style.display = 'none'; + // Allow microphone explicitly if needed + iframe.setAttribute('allow', 'microphone'); + document.body.appendChild(iframe); + } + }, + args: [extensionUrl] + }).then(() => { + // Small delay to ensure iframe has loaded and registered listener + setTimeout(() => { + chrome.runtime.sendMessage({ + type: 'START_RECORDING_FROM_POPUP', + payload: { language: 'ar-SA' } + }); + }, 500); + }).catch(err => { + console.error('Failed to inject iframe:', err); + statusEl.textContent = '❌ فشل الاتصال بالصفحة الحالية'; + stopRecording(false); + }); }); } }); diff --git a/speech.html b/speech.html new file mode 100644 index 0000000..a8f4d1e --- /dev/null +++ b/speech.html @@ -0,0 +1,10 @@ + + + + + Speech Recognition Frame + + + + + diff --git a/speech.js b/speech.js new file mode 100644 index 0000000..ad098ac --- /dev/null +++ b/speech.js @@ -0,0 +1,117 @@ +// speech.js - Handles webkitSpeechRecognition inside the injected iframe + +let recognition = null; +let isRecording = false; +let localStream = null; + +function stopMediaTracks() { + if (localStream) { + localStream.getTracks().forEach(track => track.stop()); + localStream = null; + } +} + +function initRecognition(language) { + if (recognition) return recognition; + + const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; + if (!SpeechRecognition) { + throw new Error('Speech recognition not supported'); + } + + recognition = new SpeechRecognition(); + recognition.continuous = true; + recognition.interimResults = true; + recognition.lang = language || 'ar-SA'; + recognition.maxAlternatives = 1; + + recognition.onresult = (event) => { + let interimText = ''; + let finalText = ''; + + for (let i = event.resultIndex; i < event.results.length; i++) { + const result = event.results[i]; + if (result.isFinal) { + finalText += result[0].transcript + ' '; + } else { + interimText += result[0].transcript; + } + } + + chrome.runtime.sendMessage({ + type: 'OFFSCREEN_RECORDING_RESULT', + payload: { interimText, finalText } + }); + }; + + recognition.onerror = (event) => { + console.error('[Speech Iframe] Recognition error:', event.error); + + if (event.error !== 'no-speech') { + isRecording = false; + stopMediaTracks(); + } + + chrome.runtime.sendMessage({ + type: 'OFFSCREEN_RECORDING_ERROR', + payload: { error: event.error } + }); + }; + + recognition.onend = () => { + if (isRecording) { + try { + recognition.start(); + } catch (e) { + console.warn('[Speech Iframe] Restart failed:', e); + } + } else { + stopMediaTracks(); + chrome.runtime.sendMessage({ type: 'OFFSCREEN_RECORDING_END' }); + } + }; + + return recognition; +} + +// Listen for messages broadcasted across the extension +chrome.runtime.onMessage.addListener((message) => { + if (message.type === 'START_RECORDING_FROM_POPUP') { + const lang = message.payload?.language || 'ar-SA'; + + navigator.mediaDevices.getUserMedia({ audio: true }) + .then((stream) => { + localStream = stream; + try { + if (!recognition || recognition.lang !== lang) { + recognition = initRecognition(lang); + } + if (!isRecording) { + recognition.start(); + isRecording = true; + // Tell popup it started successfully + chrome.runtime.sendMessage({ type: 'OFFSCREEN_RECORDING_START_SUCCESS' }); + } + } catch (e) { + console.error('[Speech Iframe] Failed to start:', e); + chrome.runtime.sendMessage({ type: 'OFFSCREEN_RECORDING_ERROR', payload: { error: e.message } }); + } + }) + .catch((err) => { + console.error('[Speech Iframe] getUserMedia failed:', err); + chrome.runtime.sendMessage({ type: 'OFFSCREEN_RECORDING_ERROR', payload: { error: 'not-allowed' } }); + }); + } + + if (message.type === 'STOP_RECORDING_FROM_POPUP') { + if (isRecording && recognition) { + isRecording = false; + try { + recognition.stop(); + } catch (e) { + console.warn('[Speech Iframe] Stop failed:', e); + } + } + stopMediaTracks(); + } +});