diff --git a/background.js b/background.js index 5a8b0d0..b895db0 100644 --- a/background.js +++ b/background.js @@ -4,27 +4,7 @@ const GEMINI_MODEL = 'gemini-flash-lite-latest'; const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent`; -// ─── Offscreen Document Management ─────────────────────────────────────────── - -let recordingTabId = null; - -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') { @@ -41,42 +21,10 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { return true; // Keep message channel open for async } - // --- Offscreen Document Relays --- - if (message.type === 'START_RECORDING_FROM_CONTENT') { - recordingTabId = sender.tab ? sender.tab.id : null; - 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_CONTENT') { - chrome.runtime.sendMessage({ type: 'STOP_RECORDING' }, (response) => { - sendResponse(response || { success: true }); - }); - return true; - } - if (message.type === 'OPEN_PERMISSION_PAGE') { chrome.tabs.create({ url: chrome.runtime.getURL('claude-arabic-voice/permission.html') }); return true; } - - if (message.type.startsWith('OFFSCREEN_RECORDING_')) { - if (recordingTabId) { - chrome.tabs.sendMessage(recordingTabId, message).catch(() => {}); - } - } }); // ─── Core API call ─────────────────────────────────────────────────────────── diff --git a/claude-arabic-voice/content.js b/claude-arabic-voice/content.js index 2f7b12d..74a620c 100644 --- a/claude-arabic-voice/content.js +++ b/claude-arabic-voice/content.js @@ -49,41 +49,80 @@ }); } - // ─── Offscreen Document Integration ────────────────────────────────────── - chrome.runtime.onMessage.addListener((message) => { - if (message.type === 'OFFSCREEN_RECORDING_RESULT') { - lastSpeechTime = Date.now(); - interimText = message.payload.interimText || ''; - finalText = message.payload.finalText || ''; - - updateInputField(); + // ─── Iframe Integration ────────────────────────────────────────────────── + let speechIframe = null; - clearTimeout(silenceTimer); - silenceTimer = setTimeout(() => { - if (isListening && Date.now() - lastSpeechTime >= SILENCE_TIMEOUT) { - stopListening(); + function initSpeechIframe() { + if (speechIframe) return; + + speechIframe = document.createElement('iframe'); + speechIframe.id = 'claude-voice-speech-iframe'; + speechIframe.style.cssText = ` + position: absolute; + width: 1px; + height: 1px; + left: -9999px; + top: -9999px; + opacity: 0; + pointer-events: none; + border: none; + `; + speechIframe.src = chrome.runtime.getURL('claude-arabic-voice/speech.html'); + speechIframe.allow = 'microphone'; + document.body.appendChild(speechIframe); + + window.addEventListener('message', (event) => { + if (event.source !== speechIframe.contentWindow) return; + + const message = event.data; + if (message.type === 'SPEECH_START_SUCCESS') { + isListening = true; + lastSpeechTime = Date.now(); + interimText = ''; + finalText = ''; + updateMicButton(true); + showStatus('listening', '🎤 جارٍ الاستماع...'); + + clearTimeout(autoSendTimer); + autoSendTimer = setTimeout(() => { + if (isListening) stopListening(); + }, MAX_RECORDING_TIME); + + clearTimeout(silenceTimer); + silenceTimer = setTimeout(checkSilence, SILENCE_TIMEOUT); + } else if (message.type === 'SPEECH_RESULT') { + lastSpeechTime = Date.now(); + interimText = message.payload.interimText || ''; + finalText = message.payload.finalText || ''; + + updateInputField(); + + clearTimeout(silenceTimer); + silenceTimer = setTimeout(checkSilence, SILENCE_TIMEOUT); + } else if (message.type === 'SPEECH_ERROR') { + console.error('[ClaudeVoice] Recognition error:', message.payload.error); + if (message.payload.error === 'no-speech') { + return; } - }, SILENCE_TIMEOUT); - } else if (message.type === 'OFFSCREEN_RECORDING_ERROR') { - console.error('[ClaudeVoice] Recognition error:', message.payload.error); - if (message.payload.error === 'no-speech') { - return; // offscreen script restarts it automatically - } - stopListening(); - - if (message.payload.error === 'not-allowed') { - showStatus('error', '❌ الرجاء منح الصلاحية (تم فتح صفحة جديدة)'); - chrome.runtime.sendMessage({ type: 'OPEN_PERMISSION_PAGE' }); - } else { - showStatus('error', '❌ خطأ: ' + getArabicError(message.payload.error)); - } - } else if (message.type === 'OFFSCREEN_RECORDING_END') { - if (isListening) { - // Unexpected end from offscreen while we still consider ourselves listening stopListening(); + + if (message.payload.error === 'not-allowed') { + showStatus('error', '❌ الرجاء منح الصلاحية (تم فتح صفحة جديدة)'); + chrome.runtime.sendMessage({ type: 'OPEN_PERMISSION_PAGE' }); + } else { + showStatus('error', '❌ خطأ: ' + getArabicError(message.payload.error)); + } + } else if (message.type === 'SPEECH_END') { + if (isListening) stopListening(); } + }); + } + + function checkSilence() { + if (isListening && Date.now() - lastSpeechTime >= SILENCE_TIMEOUT) { + stopListening(); } - }); + } function getArabicError(error) { const errors = { @@ -101,36 +140,13 @@ // ─── Start / Stop Listening ───────────────────────────────────────────── async function startListening() { await loadSettings(); + if (!speechIframe) initSpeechIframe(); try { - chrome.runtime.sendMessage({ - type: 'START_RECORDING_FROM_CONTENT', + speechIframe.contentWindow.postMessage({ + type: 'START_RECORDING', payload: { language: settings.language } - }, (response) => { - if (response && response.success) { - isListening = true; - lastSpeechTime = Date.now(); - interimText = ''; - finalText = ''; - updateMicButton(true); - showStatus('listening', '🎤 جارٍ الاستماع...'); - - clearTimeout(autoSendTimer); - autoSendTimer = setTimeout(() => { - if (isListening) stopListening(); - }, MAX_RECORDING_TIME); - - clearTimeout(silenceTimer); - silenceTimer = setTimeout(() => { - if (isListening && Date.now() - lastSpeechTime >= SILENCE_TIMEOUT) { - stopListening(); - } - }, SILENCE_TIMEOUT); - } else { - console.error('[ClaudeVoice] Start failed:', response?.error); - showStatus('error', '❌ فشل بدء التسجيل'); - } - }); + }, '*'); } catch (e) { console.error('[ClaudeVoice] Start message failed:', e); showStatus('error', '❌ فشل الاتصال بالإضافة'); @@ -144,7 +160,9 @@ clearTimeout(silenceTimer); clearTimeout(autoSendTimer); - chrome.runtime.sendMessage({ type: 'STOP_RECORDING_FROM_CONTENT' }); + if (speechIframe) { + speechIframe.contentWindow.postMessage({ type: 'STOP_RECORDING' }, '*'); + } updateMicButton(false); const text = finalText.trim() || interimText.trim(); @@ -594,6 +612,7 @@ CORRECTED TEXT:`; async function init() { await loadSettings(); injectStyles(); + initSpeechIframe(); // Inject iframe immediately createMicButton(); // Re-position on scroll and resize diff --git a/claude-arabic-voice/speech.html b/claude-arabic-voice/speech.html new file mode 100644 index 0000000..a8f4d1e --- /dev/null +++ b/claude-arabic-voice/speech.html @@ -0,0 +1,10 @@ + + +
+ +