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