Auto-deploy: 2026-06-02 18:39:04
This commit is contained in:
@@ -4,25 +4,7 @@
|
|||||||
const GEMINI_MODEL = 'gemini-flash-lite-latest';
|
const GEMINI_MODEL = 'gemini-flash-lite-latest';
|
||||||
const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent`;
|
const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent`;
|
||||||
|
|
||||||
// ─── Offscreen Document Management ───────────────────────────────────────────
|
// (Offscreen document logic removed in favor of iframe approach)
|
||||||
|
|
||||||
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',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
if (message.type === 'GEMINI_REQUEST') {
|
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') });
|
chrome.tabs.create({ url: chrome.runtime.getURL('permission.html') });
|
||||||
return true;
|
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 ───────────────────────────────────────────────────────────
|
// ─── Core API call ───────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -8,8 +8,7 @@
|
|||||||
"activeTab",
|
"activeTab",
|
||||||
"scripting",
|
"scripting",
|
||||||
"clipboardWrite",
|
"clipboardWrite",
|
||||||
"microphone",
|
"microphone"
|
||||||
"offscreen"
|
|
||||||
],
|
],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"https://www.linkedin.com/*",
|
"https://www.linkedin.com/*",
|
||||||
@@ -54,6 +53,15 @@
|
|||||||
"128": "icons/icon128.png"
|
"128": "icons/icon128.png"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"web_accessible_resources": [
|
||||||
|
{
|
||||||
|
"resources": [
|
||||||
|
"speech.html",
|
||||||
|
"speech.js"
|
||||||
|
],
|
||||||
|
"matches": ["<all_urls>"]
|
||||||
|
}
|
||||||
|
],
|
||||||
"background": {
|
"background": {
|
||||||
"service_worker": "background.js"
|
"service_worker": "background.js"
|
||||||
},
|
},
|
||||||
|
|||||||
41
popup.js
41
popup.js
@@ -318,6 +318,16 @@ function initDictation() {
|
|||||||
if (isRecording) {
|
if (isRecording) {
|
||||||
stopRecording(true);
|
stopRecording(true);
|
||||||
} else {
|
} else {
|
||||||
|
// 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;
|
isRecording = true;
|
||||||
micBtn.style.background = 'linear-gradient(135deg, #ff4d6d, #c9184a)';
|
micBtn.style.background = 'linear-gradient(135deg, #ff4d6d, #c9184a)';
|
||||||
micBtn.innerHTML = '🔴';
|
micBtn.innerHTML = '🔴';
|
||||||
@@ -327,15 +337,36 @@ function initDictation() {
|
|||||||
resultArea.value = '';
|
resultArea.value = '';
|
||||||
copyBtn.style.display = 'none';
|
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({
|
chrome.runtime.sendMessage({
|
||||||
type: 'START_RECORDING_FROM_POPUP',
|
type: 'START_RECORDING_FROM_POPUP',
|
||||||
payload: { language: 'ar-SA' }
|
payload: { language: 'ar-SA' }
|
||||||
}, (response) => {
|
});
|
||||||
if (!response || !response.success) {
|
}, 500);
|
||||||
console.error('Failed to start recording', response);
|
}).catch(err => {
|
||||||
statusEl.textContent = '❌ فشل بدء التسجيل';
|
console.error('Failed to inject iframe:', err);
|
||||||
|
statusEl.textContent = '❌ فشل الاتصال بالصفحة الحالية';
|
||||||
stopRecording(false);
|
stopRecording(false);
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
10
speech.html
Normal file
10
speech.html
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Speech Recognition Frame</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="speech.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
117
speech.js
Normal file
117
speech.js
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user