diff --git a/AGENTS.md b/AGENTS.md index 093e3f1..085b2b9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,3 +48,19 @@ - `call_declined` - `unknown_error` - В этих записях искать поля `reason`, `failureStage`, `pcConnectionState`, `pcIceConnectionState`, `routeLabel`, `configuredTurnHosts*`, `reachableTurnHosts*`. + +## Недопроверенные фичи (обязательно) +- Папка для учёта недопроверенных фич: `Dev_Docs/Pending_Features/`. +- По каждой новой доработке, которая требует ручной проверки, добавлять отдельный markdown-файл в `Dev_Docs/Pending_Features/`. +- Рекомендуемый формат имени файла: `YYYY-MM-DD_HHMM_.md`. +- Внутри файла обязательно указывать: + - краткое описание фичи; + - что именно проверять; + - ожидаемый результат; + - статус (например: `pending`, `in_progress`, `done`). +- После подтверждения, что фича проверена и работает корректно, соответствующий файл удалять. +- В `Dev_Docs/Pending_Features/README.md` вести краткий регламент и поддерживать актуальность. + +## Коммуникация в начале нового чата +- В начале каждого нового чата (в первом ответе пользователю) дополнительно сообщать, сколько сейчас недопроверенных фич лежит в `Dev_Docs/Pending_Features/` (без учёта `README.md`). +- В том же первом ответе обязательно уточнять у пользователя, проверил ли он эти фичи и можно ли пометить их как завершённые (удалить соответствующие файлы). diff --git a/Dev_Docs/Pending_Features/2026-05-13_0201_voice-tools-openai-tts-and-stt.md b/Dev_Docs/Pending_Features/2026-05-13_0201_voice-tools-openai-tts-and-stt.md new file mode 100644 index 0000000..b83af1c --- /dev/null +++ b/Dev_Docs/Pending_Features/2026-05-13_0201_voice-tools-openai-tts-and-stt.md @@ -0,0 +1,34 @@ +# Голосовой ввод и озвучка (STT/TTS) + +Статус: `pending` + +## Что сделано + +- Добавлен голосовой ввод в: + - личный чат; + - форму ответа в канале; + - форму нового сообщения в канале; + - форму ответа в треде. +- Добавлен инструмент «Прочесть вслух» для текста в чате. +- Добавлен экран `Настройки инструментов ввода`: + - STT через OpenAI (base URL, API key, качество, модель); + - TTS через Browser / Piper HTTP / OpenAI TTS. +- Для TTS добавлена кнопка «Проверить озвучку». +- Если инструмент не настроен, показывается предложение перейти в настройки. +- Настройки сохраняются локально и сохраняются между сессиями. + +## Как проверять + +1. Открыть `Настройки -> Настройки инструментов ввода`. +2. В блоке STT заполнить OpenAI API key (и при необходимости URL/модель). +3. В блоке TTS выбрать OpenAI, заполнить API key, при необходимости модель/голос. +4. Нажать «Проверить озвучку» и убедиться, что звук воспроизводится. +5. Открыть чат/канал/тред, нажать кнопку `🎤`, записать голос, нажать `OK`. +6. Убедиться, что распознанный текст подставился в поле ввода. +7. Отправить сообщение и проверить, что оно дошло. + +## Ожидаемый результат + +- Голосовой ввод работает во всех указанных формах. +- Озвучка через OpenAI TTS работает с тем же ключом, что и STT (если ключ имеет нужные права). +- При пустых настройках показывается понятный переход в настройки. diff --git a/Dev_Docs/Pending_Features/README.md b/Dev_Docs/Pending_Features/README.md new file mode 100644 index 0000000..bd4722f --- /dev/null +++ b/Dev_Docs/Pending_Features/README.md @@ -0,0 +1,19 @@ +# Недопроверенные фичи + +Эта папка хранит список доработок, которые уже реализованы, но ещё не подтверждены ручной проверкой. + +## Как использовать + +1. При каждом коммите с новыми пользовательскими фичами (если нужна ручная проверка) добавить новый файл: + - формат: `YYYY-MM-DD_HHMM_.md` +2. В файле указать: + - что сделано; + - как проверять; + - ожидаемый результат; + - текущий статус (`pending` / `in_progress` / `done`). +3. После подтверждения работоспособности — удалить файл фичи из этой папки. + +## Важно + +- `README.md` не удаляется. +- Количество недопроверенных фич = число файлов `*.md` в этой папке, кроме `README.md`. diff --git a/VERSION.properties b/VERSION.properties index 140d2ac..7775ae4 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.45 -server.version=1.2.39 +client.version=1.2.46 +server.version=1.2.40 diff --git a/shine-UI/js/app.js b/shine-UI/js/app.js index 0a918cc..d87ea22 100644 --- a/shine-UI/js/app.js +++ b/shine-UI/js/app.js @@ -48,6 +48,7 @@ import * as walletView from './pages/wallet-view.js'; import * as settingsView from './pages/settings-view.js'; import * as developerSettingsView from './pages/developer-settings-view.js'; import * as serverSettingsView from './pages/server-settings-view.js'; +import * as toolsSettingsView from './pages/tools-settings-view.js'; import * as deviceView from './pages/device-view.js'; import * as connectDeviceView from './pages/connect-device-view.js'; import * as deviceQrView from './pages/device-qr-view.js'; @@ -85,6 +86,7 @@ const routes = { 'settings-view': settingsView, 'developer-settings-view': developerSettingsView, 'server-settings-view': serverSettingsView, + 'tools-settings-view': toolsSettingsView, 'device-view': deviceView, 'connect-device-view': connectDeviceView, 'device-qr-view': deviceQrView, diff --git a/shine-UI/js/components/speech-input-modal.js b/shine-UI/js/components/speech-input-modal.js new file mode 100644 index 0000000..7384650 --- /dev/null +++ b/shine-UI/js/components/speech-input-modal.js @@ -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 = ` + + `; + + 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'}`; + } + }); +} diff --git a/shine-UI/js/pages/channel-thread-view.js b/shine-UI/js/pages/channel-thread-view.js index a9d99ef..5aa0a4c 100644 --- a/shine-UI/js/pages/channel-thread-view.js +++ b/shine-UI/js/pages/channel-thread-view.js @@ -10,6 +10,7 @@ import { showToast, softHaptic, } from '../services/channels-ux.js'; +import { openSpeechInputModal } from '../components/speech-input-modal.js'; export const pageMeta = { id: 'channel-thread-view', title: 'Тред' }; @@ -229,13 +230,16 @@ function resolveNodeText(node) { ); } -function openReplyModal({ onSubmit }) { +function openReplyModal({ onSubmit, navigate }) { const root = document.getElementById('modal-root'); root.innerHTML = `