SHA256
UI: голосовой ввод/STT, TTS через OpenAI, настройки инструментов + учёт недопроверенных фич
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
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 }) {
|
||||
if (!isSpeechToTextConfigured(state.entrySettings)) {
|
||||
showSttMissingConfigDialog(navigate);
|
||||
return;
|
||||
}
|
||||
|
||||
const root = document.getElementById('modal-root');
|
||||
root.innerHTML = `
|
||||
<div class="modal" id="speech-input-modal">
|
||||
<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="form-actions-grid">
|
||||
<button class="secondary-btn" type="button" id="speech-cancel">Отмена</button>
|
||||
<button class="primary-btn" type="button" id="speech-ok">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const statusEl = root.querySelector('#speech-input-status');
|
||||
const timeEl = root.querySelector('#speech-input-time');
|
||||
const levelEl = root.querySelector('#speech-level-fill');
|
||||
const errorEl = root.querySelector('#speech-input-error');
|
||||
const cancelBtn = root.querySelector('#speech-cancel');
|
||||
const okBtn = root.querySelector('#speech-ok');
|
||||
const recorder = createMicrophoneRecorder();
|
||||
let closed = false;
|
||||
let busy = false;
|
||||
|
||||
const close = () => {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
root.innerHTML = '';
|
||||
};
|
||||
|
||||
const setBusy = (flag) => {
|
||||
busy = !!flag;
|
||||
cancelBtn.disabled = busy;
|
||||
okBtn.disabled = busy;
|
||||
okBtn.textContent = busy ? 'Распознаю...' : 'OK';
|
||||
};
|
||||
|
||||
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);
|
||||
errorEl.textContent = '';
|
||||
statusEl.textContent = 'Распознаю речь...';
|
||||
try {
|
||||
const audioBlob = await recorder.stop();
|
||||
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'}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user