/
Dmitry-F
/
cbt_tool
Обзор
Документация
Войти
/
Dmitry-F
/
cbt_tool
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop
app/javascript/controllers/audio_recorder_controller.js
210 строк
7 KB
Dmitriy-Filatov
🧹 ФИНАЛЬНАЯ СТЕРИЛИЗАЦИЯ ИНФРАСТРУКТУРЫ
17 мар 2026, 10:59
17 мар 2026, 10:59
d3fec0b
Код
Авторство
О чём код?
// app/javascript/controllers/audio_recorder_controller.js import { Controller } from "@hotwired/stimulus"; export default class extends Controller { static targets = ["input", "status", "icon", "fileField"]; static values = { recording: { type: Boolean, default: false }, }; connect() { // Больше НЕ запрашиваем микрофон здесь — только настраиваем API распознавания this.setupRecognition(); // MediaRecorder будет создаваться динамически при старте записи } // 1. ТЕКСТОВЫЙ ПОТОК (Web Speech API) — настраивается один раз setupRecognition() { const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; if (!SpeechRecognition) { this.showStatus("Распознавание речи не поддерживается"); return; } this.recognition = new SpeechRecognition(); this.recognition.continuous = false; this.recognition.interimResults = false; this.recognition.lang = "ru-RU"; this.recognition.onresult = (event) => { const lastResult = event.results[event.results.length - 1]; if (lastResult.isFinal) { const transcript = lastResult[0].transcript; const currentValue = this.inputTarget.value; this.inputTarget.value = currentValue ? currentValue + " " + transcript : transcript; } }; this.recognition.onerror = (event) => { console.error("Ошибка распознавания:", event.error); this.stopRecording(); }; this.recognition.onend = () => { // Автоматически останавливаем запись, если распознавание закончилось if (this.recordingValue) { this.stopRecording(); } }; } // 2. СОЗДАНИЕ MediaRecorder (ТОЛЬКО ПРИ СТАРТЕ ЗАПИСИ) async setupMediaRecorder() { try { // ✨ ФИКС: Запрашиваем микрофон ТОЛЬКО сейчас, при старте записи const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); this.stream = stream; this.mediaRecorder = new MediaRecorder(stream); this.audioChunks = []; this.mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0) { this.audioChunks.push(e.data); } }; this.mediaRecorder.onstop = () => { const audioBlob = new Blob(this.audioChunks, { type: "audio/webm" }); this.attachAudioToForm(audioBlob); this.audioChunks = []; // Останавливаем все треки, чтобы освободить микрофон if (this.stream) { this.stream.getTracks().forEach((track) => track.stop()); this.stream = null; } this.mediaRecorder = null; }; // Запускаем запись this.mediaRecorder.start(); return true; } catch (err) { this.showStatus("Доступ к микрофону запрещён"); console.error("MediaRecorder error:", err); return false; } } // 3. ПРИКРЕПЛЕНИЕ ФАЙЛА К ФОРМЕ (DataTransfer) attachAudioToForm(blob) { if (!this.hasFileFieldTarget) { console.warn("Нет fileField target для прикрепления аудио"); return; } try { const file = new File([blob], `recording-${Date.now()}.webm`, { type: "audio/webm", }); const dataTransfer = new DataTransfer(); dataTransfer.items.add(file); this.fileFieldTarget.files = dataTransfer.files; this.showStatus(`Аудио готово (${(blob.size / 1024).toFixed(0)} KB)`); } catch (err) { console.error("Ошибка прикрепления файла:", err); this.showStatus("Ошибка сохранения аудио"); } } // 4. УПРАВЛЕНИЕ ЗАПИСЬЮ async startRecording(event) { event?.preventDefault(); if (this.recordingValue) return; this.recordingValue = true; this.showStatus("Запись..."); this.iconTarget?.classList.add("text-red-500", "animate-pulse"); // ✨ ФИКС: Создаём MediaRecorder прямо сейчас, с новым потоком const mediaRecorderStarted = await this.setupMediaRecorder(); if (mediaRecorderStarted) { // Запускаем распознавание речи try { this.recognition?.start(); } catch (err) { console.error("Ошибка старта распознавания:", err); // Если распознавание не запустилось, всё равно продолжаем с аудио } } else { // Если не удалось получить микрофон, отменяем запись this.recordingValue = false; this.iconTarget?.classList.remove("text-red-500", "animate-pulse"); } } stopRecording(event) { event?.preventDefault(); if (!this.recordingValue) return; this.recordingValue = false; this.iconTarget?.classList.remove("text-red-500", "animate-pulse"); this.showStatus("Обработка..."); // Останавливаем распознавание try { this.recognition?.stop(); } catch (err) { console.error("Ошибка остановки распознавания:", err); } // Останавливаем MediaRecorder if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") { this.mediaRecorder.stop(); } } // 5. МЕТОД ДЛЯ КНОПКИ (обрабатывает и мышь, и касания) toggleRecording(event) { if (event.type === "mousedown" || event.type === "touchstart") { this.startRecording(event); } else if ( event.type === "mouseup" || event.type === "mouseleave" || event.type === "touchend" ) { this.stopRecording(event); } } showStatus(message) { if (this.hasStatusTarget) { this.statusTarget.textContent = message; if (message.includes("готово") || message.includes("Ошибка")) { setTimeout(() => { if (this.hasStatusTarget && !this.recordingValue) { this.statusTarget.textContent = ""; } }, 3000); } } } // Очистка при размонтировании disconnect() { if (this.stream) { this.stream.getTracks().forEach((track) => track.stop()); } if (this.recognition) { try { this.recognition.stop(); } catch (e) {} } if (this.mediaRecorder && this.mediaRecorder.state !== "inactive") { try { this.mediaRecorder.stop(); } catch (e) {} } } }