Auto-deploy: 2026-06-02 18:15:27

This commit is contained in:
Hamza-Ayed
2026-06-02 18:15:27 +03:00
parent a0c956de21
commit 6f6cec0617
5 changed files with 218 additions and 111 deletions

View File

@@ -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 ───────────────────────────────────────────────────────────

View File

@@ -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 || '';
// ─── Iframe Integration ──────────────────────────────────────────────────
let speechIframe = null;
updateInputField();
function initSpeechIframe() {
if (speechIframe) return;
clearTimeout(silenceTimer);
silenceTimer = setTimeout(() => {
if (isListening && Date.now() - lastSpeechTime >= SILENCE_TIMEOUT) {
stopListening();
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

View 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>

View File

@@ -0,0 +1,121 @@
// speech.js - Handles webkitSpeechRecognition inside the 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;
}
}
window.parent.postMessage({
type: 'SPEECH_RESULT',
payload: { interimText, finalText }
}, '*');
};
recognition.onerror = (event) => {
console.error('[Speech Iframe] Recognition error:', event.error);
if (event.error !== 'no-speech') {
isRecording = false;
stopMediaTracks();
}
window.parent.postMessage({
type: 'SPEECH_ERROR',
payload: { error: event.error }
}, '*');
};
recognition.onend = () => {
if (isRecording) {
try {
recognition.start();
} catch (e) {
console.warn('[Speech Iframe] Restart failed:', e);
}
} else {
stopMediaTracks();
window.parent.postMessage({ type: 'SPEECH_END' }, '*');
}
};
return recognition;
}
// Listen for messages from parent window
window.addEventListener('message', (event) => {
// Basic security check (only accept messages from the parent Claude page)
if (!event.origin.includes('claude.ai')) return;
const message = event.data;
if (message.type === 'START_RECORDING') {
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;
window.parent.postMessage({ type: 'SPEECH_START_SUCCESS' }, '*');
}
} catch (e) {
console.error('[Speech Iframe] Failed to start:', e);
window.parent.postMessage({ type: 'SPEECH_ERROR', payload: { error: e.message } }, '*');
}
})
.catch((err) => {
console.error('[Speech Iframe] getUserMedia failed:', err);
window.parent.postMessage({ type: 'SPEECH_ERROR', payload: { error: 'not-allowed' } }, '*');
});
}
if (message.type === 'STOP_RECORDING') {
if (isRecording && recognition) {
isRecording = false;
try {
recognition.stop();
} catch (e) {
console.warn('[Speech Iframe] Stop failed:', e);
}
}
stopMediaTracks();
}
});

View File

@@ -63,6 +63,15 @@
"128": "icons/icon128.png"
}
},
"web_accessible_resources": [
{
"resources": [
"claude-arabic-voice/speech.html",
"claude-arabic-voice/speech.js"
],
"matches": ["https://claude.ai/*"]
}
],
"background": {
"service_worker": "background.js"
},