Auto-deploy: 2026-06-02 18:02:41

This commit is contained in:
Hamza-Ayed
2026-06-02 18:02:41 +03:00
parent d5919fbf01
commit 36e4dd42e4
5 changed files with 212 additions and 97 deletions

View File

@@ -4,7 +4,27 @@
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`;
// ─── Message listener ──────────────────────────────────────────────────────── // ─── 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',
});
}
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'GEMINI_REQUEST') { if (message.type === 'GEMINI_REQUEST') {
@@ -20,6 +40,38 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
.catch(err => sendResponse({ success: false, error: err.message })); .catch(err => sendResponse({ success: false, error: err.message }));
return true; // Keep message channel open for async 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.startsWith('OFFSCREEN_RECORDING_')) {
if (recordingTabId) {
chrome.tabs.sendMessage(recordingTabId, message).catch(() => {});
}
}
}); });
// ─── Core API call ─────────────────────────────────────────────────────────── // ─── Core API call ───────────────────────────────────────────────────────────

View File

@@ -49,76 +49,35 @@
}); });
} }
// ─── Speech Recognition Setup ──────────────────────────────────────────── // ─── Offscreen Document Integration ──────────────────────────────────────
function initSpeechRecognition() { chrome.runtime.onMessage.addListener((message) => {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; if (message.type === 'OFFSCREEN_RECORDING_RESULT') {
if (!SpeechRecognition) {
console.warn('[ClaudeVoice] Speech recognition not supported in this browser');
return null;
}
const recog = new SpeechRecognition();
recog.continuous = true;
recog.interimResults = true;
recog.lang = settings.language;
recog.maxAlternatives = 1;
recog.onresult = (event) => {
lastSpeechTime = Date.now(); lastSpeechTime = Date.now();
interimText = ''; interimText = message.payload.interimText || '';
finalText = ''; finalText = message.payload.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;
}
}
updateInputField(); updateInputField();
// Reset silence timer on new speech
clearTimeout(silenceTimer); clearTimeout(silenceTimer);
silenceTimer = setTimeout(() => { silenceTimer = setTimeout(() => {
if (isListening && Date.now() - lastSpeechTime >= SILENCE_TIMEOUT) { if (isListening && Date.now() - lastSpeechTime >= SILENCE_TIMEOUT) {
stopListening(); stopListening();
} }
}, SILENCE_TIMEOUT); }, SILENCE_TIMEOUT);
}; } else if (message.type === 'OFFSCREEN_RECORDING_ERROR') {
console.error('[ClaudeVoice] Recognition error:', message.payload.error);
recog.onerror = (event) => { if (message.payload.error === 'no-speech') {
console.error('[ClaudeVoice] Recognition error:', event.error); return; // offscreen script restarts it automatically
if (event.error === 'no-speech') {
// No speech detected, restart if still listening
if (isListening) {
try { recog.stop(); } catch (e) { }
setTimeout(() => {
if (isListening) {
try { recog.start(); } catch (e) { }
}
}, 300);
}
return;
} }
stopListening(); stopListening();
showStatus('error', '❌ خطأ: ' + getArabicError(event.error)); showStatus('error', '❌ خطأ: ' + getArabicError(message.payload.error));
}; } else if (message.type === 'OFFSCREEN_RECORDING_END') {
recog.onend = () => {
// If we're still supposed to be listening, restart
if (isListening) { if (isListening) {
try { // Unexpected end from offscreen while we still consider ourselves listening
recog.start(); stopListening();
} catch (e) {
console.warn('[ClaudeVoice] Restart failed:', e);
}
} }
}; }
});
return recog;
}
function getArabicError(error) { function getArabicError(error) {
const errors = { const errors = {
@@ -137,59 +96,51 @@
async function startListening() { async function startListening() {
await loadSettings(); await loadSettings();
if (!recognition) {
recognition = initSpeechRecognition();
}
if (!recognition) {
showStatus('error', '❌ المتصفح لا يدعم التعرف على الصوت');
return;
}
// Update language in case it changed
recognition.lang = settings.language;
try { try {
recognition.start(); chrome.runtime.sendMessage({
isListening = true; type: 'START_RECORDING_FROM_CONTENT',
lastSpeechTime = Date.now(); payload: { language: settings.language }
updateMicButton(true); }, (response) => {
showStatus('listening', '🎤 جارٍ الاستماع...'); if (response && response.success) {
isListening = true;
lastSpeechTime = Date.now();
interimText = '';
finalText = '';
updateMicButton(true);
showStatus('listening', '🎤 جارٍ الاستماع...');
// Auto-stop after max time clearTimeout(autoSendTimer);
clearTimeout(autoSendTimer); autoSendTimer = setTimeout(() => {
autoSendTimer = setTimeout(() => { if (isListening) stopListening();
if (isListening) stopListening(); }, MAX_RECORDING_TIME);
}, MAX_RECORDING_TIME);
// Silence detection clearTimeout(silenceTimer);
clearTimeout(silenceTimer); silenceTimer = setTimeout(() => {
silenceTimer = setTimeout(() => { if (isListening && Date.now() - lastSpeechTime >= SILENCE_TIMEOUT) {
if (isListening && Date.now() - lastSpeechTime >= SILENCE_TIMEOUT) { stopListening();
stopListening(); }
}, SILENCE_TIMEOUT);
} else {
console.error('[ClaudeVoice] Start failed:', response?.error);
showStatus('error', '❌ فشل بدء التسجيل');
} }
}, SILENCE_TIMEOUT); });
} catch (e) { } catch (e) {
console.error('[ClaudeVoice] Start failed:', e); console.error('[ClaudeVoice] Start message failed:', e);
showStatus('error', '❌ فشل بدء التسجيل'); showStatus('error', '❌ فشل الاتصال بالإضافة');
} }
} }
async function stopListening() { async function stopListening() {
if (!recognition) return; if (!isListening) return;
isListening = false; isListening = false;
clearTimeout(silenceTimer); clearTimeout(silenceTimer);
clearTimeout(autoSendTimer); clearTimeout(autoSendTimer);
try { chrome.runtime.sendMessage({ type: 'STOP_RECORDING_FROM_CONTENT' });
recognition.stop();
} catch (e) { /* ignore */ }
updateMicButton(false); updateMicButton(false);
// If we have final text, process it
const text = finalText.trim() || interimText.trim(); const text = finalText.trim() || interimText.trim();
if (text) { if (text) {
showStatus('processing', '⏳ جارٍ المعالجة...'); showStatus('processing', '⏳ جارٍ المعالجة...');
@@ -197,7 +148,6 @@
if (settings.useGemini && settings.geminiApiKey) { if (settings.useGemini && settings.geminiApiKey) {
await processWithGemini(text); await processWithGemini(text);
} else { } else {
// Just insert the text directly
insertTextIntoClaude(text); insertTextIntoClaude(text);
showStatus('done', '✅ تم الإدراج'); showStatus('done', '✅ تم الإدراج');
setTimeout(() => hideStatus(), 2000); setTimeout(() => hideStatus(), 2000);

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Claude Arabic Voice Offscreen</title>
</head>
<body>
<script src="offscreen.js"></script>
</body>
</html>

View File

@@ -0,0 +1,102 @@
// offscreen.js - Handles webkitSpeechRecognition in isolation
let recognition = null;
let isRecording = false;
// Initialize recognition early if possible, or wait until start
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;
}
}
// Send results back to background script
chrome.runtime.sendMessage({
type: 'OFFSCREEN_RECORDING_RESULT',
payload: { interimText, finalText }
});
};
recognition.onerror = (event) => {
console.error('[Offscreen] Recognition error:', event.error);
chrome.runtime.sendMessage({
type: 'OFFSCREEN_RECORDING_ERROR',
payload: { error: event.error }
});
};
recognition.onend = () => {
// If we still want to be recording, restart
if (isRecording) {
try {
recognition.start();
} catch (e) {
console.warn('[Offscreen] Restart failed:', e);
}
} else {
chrome.runtime.sendMessage({ type: 'OFFSCREEN_RECORDING_END' });
}
};
return recognition;
}
// Listen for messages from background script
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'START_RECORDING') {
try {
const lang = message.payload?.language || 'ar-SA';
// Re-init if language changed or not init yet
if (!recognition || recognition.lang !== lang) {
recognition = initRecognition(lang);
}
if (!isRecording) {
recognition.start();
isRecording = true;
sendResponse({ success: true });
} else {
sendResponse({ success: true, warning: 'Already recording' });
}
} catch (e) {
console.error('[Offscreen] Failed to start:', e);
sendResponse({ success: false, error: e.message });
}
return true;
}
if (message.type === 'STOP_RECORDING') {
if (isRecording && recognition) {
isRecording = false;
try {
recognition.stop();
} catch (e) {
console.warn('[Offscreen] Stop failed:', e);
}
}
sendResponse({ success: true });
return true;
}
});

View File

@@ -8,7 +8,8 @@
"activeTab", "activeTab",
"scripting", "scripting",
"clipboardWrite", "clipboardWrite",
"microphone" "microphone",
"offscreen"
], ],
"host_permissions": [ "host_permissions": [
"https://www.linkedin.com/*", "https://www.linkedin.com/*",