133 lines
4.8 KiB
JavaScript
133 lines
4.8 KiB
JavaScript
import {
|
||
createMicrophoneRecorder,
|
||
isSpeechToTextConfigured,
|
||
transcribeAudioBySettings,
|
||
} from '../services/speech-tools-service.js';
|
||
import { state } from '../state.js';
|
||
|
||
function formatDuration(ms) {
|
||
const totalSec = Math.max(0, Math.floor(Number(ms || 0) / 1000));
|
||
const mm = String(Math.floor(totalSec / 60)).padStart(2, '0');
|
||
const ss = String(totalSec % 60).padStart(2, '0');
|
||
return `${mm}:${ss}`;
|
||
}
|
||
|
||
function showSttMissingConfigDialog(navigate) {
|
||
const goSettings = window.confirm(
|
||
'Распознавание речи не настроено. Перейти в настройки инструментов?'
|
||
);
|
||
if (goSettings) navigate('tools-settings-view');
|
||
}
|
||
|
||
export async function openSpeechInputModal({ navigate, onTextReady, onSendText, onSendQueued }) {
|
||
if (!isSpeechToTextConfigured(state.entrySettings)) {
|
||
showSttMissingConfigDialog(navigate);
|
||
return;
|
||
}
|
||
|
||
const root = document.getElementById('modal-root');
|
||
const host = document.createElement('div');
|
||
host.innerHTML = `
|
||
<div class="modal" id="speech-input-modal-layer">
|
||
<div class="modal-card stack">
|
||
<h3 class="modal-title">Голосовой ввод</h3>
|
||
<p class="meta-muted" id="speech-input-status">Идёт запись...</p>
|
||
<div class="voice-level-wrap"><div class="voice-level-fill" id="speech-level-fill"></div></div>
|
||
<p class="meta-muted" id="speech-input-time">00:00</p>
|
||
<p class="inline-error" id="speech-input-error"></p>
|
||
<div class="speech-actions-top">
|
||
<button class="secondary-btn" type="button" id="speech-cancel">Отмена</button>
|
||
<button class="primary-btn" type="button" id="speech-ok">OK</button>
|
||
</div>
|
||
<button class="primary-btn speech-send-now-btn" type="button" id="speech-send-now">Распознать и сразу отправить сообщение</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
root.append(host);
|
||
|
||
const statusEl = host.querySelector('#speech-input-status');
|
||
const timeEl = host.querySelector('#speech-input-time');
|
||
const levelEl = host.querySelector('#speech-level-fill');
|
||
const errorEl = host.querySelector('#speech-input-error');
|
||
const cancelBtn = host.querySelector('#speech-cancel');
|
||
const sendNowBtn = host.querySelector('#speech-send-now');
|
||
const okBtn = host.querySelector('#speech-ok');
|
||
const recorder = createMicrophoneRecorder();
|
||
let closed = false;
|
||
let busy = false;
|
||
|
||
const close = () => {
|
||
if (closed) return;
|
||
closed = true;
|
||
host.remove();
|
||
};
|
||
|
||
const setBusy = (flag) => {
|
||
busy = !!flag;
|
||
cancelBtn.disabled = busy;
|
||
sendNowBtn.disabled = busy;
|
||
okBtn.disabled = busy;
|
||
okBtn.textContent = busy ? 'Распознаю...' : 'OK';
|
||
sendNowBtn.textContent = busy ? 'Распознаю...' : 'Распознать и сразу отправить сообщение';
|
||
};
|
||
|
||
try {
|
||
await recorder.start(({ elapsedMs, level }) => {
|
||
if (timeEl) timeEl.textContent = formatDuration(elapsedMs);
|
||
if (levelEl) levelEl.style.width = `${Math.max(2, Math.round((Number(level) || 0) * 100))}%`;
|
||
});
|
||
} catch (error) {
|
||
close();
|
||
window.alert(`Не удалось получить доступ к микрофону: ${error?.message || 'unknown'}`);
|
||
return;
|
||
}
|
||
|
||
cancelBtn.addEventListener('click', () => {
|
||
recorder.cancel();
|
||
close();
|
||
});
|
||
|
||
okBtn.addEventListener('click', async () => {
|
||
if (busy) return;
|
||
setBusy(true);
|
||
try {
|
||
const audioBlob = await recorder.stop();
|
||
host.innerHTML = `
|
||
<div class="modal" id="speech-input-modal-layer">
|
||
<div class="modal-card stack">
|
||
<h3 class="modal-title">Голосовой ввод</h3>
|
||
<p class="meta-muted">Идёт распознавание текста...</p>
|
||
</div>
|
||
</div>
|
||
`;
|
||
const text = await transcribeAudioBySettings(audioBlob, state.entrySettings);
|
||
if (typeof onTextReady === 'function') onTextReady(text);
|
||
close();
|
||
} catch (error) {
|
||
setBusy(false);
|
||
statusEl.textContent = 'Идёт запись...';
|
||
errorEl.textContent = `Ошибка распознавания: ${error?.message || 'unknown'}`;
|
||
}
|
||
});
|
||
|
||
sendNowBtn.addEventListener('click', async () => {
|
||
if (busy) return;
|
||
setBusy(true);
|
||
try {
|
||
const audioBlob = await recorder.stop();
|
||
close();
|
||
if (typeof onSendQueued === 'function') onSendQueued();
|
||
const text = await transcribeAudioBySettings(audioBlob, state.entrySettings);
|
||
if (typeof onSendText === 'function') {
|
||
await onSendText(text);
|
||
} else if (typeof onTextReady === 'function') {
|
||
onTextReady(text);
|
||
}
|
||
} catch (error) {
|
||
setBusy(false);
|
||
close();
|
||
window.alert(`Ошибка распознавания: ${error?.message || 'unknown'}`);
|
||
}
|
||
});
|
||
}
|