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_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 ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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": ["<all_urls>"]
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
|
||||
65
popup.js
65
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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