Auto-deploy: 2026-06-02 18:15:27
This commit is contained in:
@@ -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 ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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
|
||||
|
||||
10
claude-arabic-voice/speech.html
Normal file
10
claude-arabic-voice/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>
|
||||
121
claude-arabic-voice/speech.js
Normal file
121
claude-arabic-voice/speech.js
Normal 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();
|
||||
}
|
||||
});
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user