@@ -343,15 +339,6 @@ function openReplyModal({ onSubmit, navigate }) {
};
root.querySelector('#reply-cancel')?.addEventListener('click', close);
- root.querySelector('#reply-voice')?.addEventListener('click', async () => {
- await openSpeechInputModal({
- navigate,
- onTextReady: (text) => {
- const prev = String(textEl?.value || '').trim();
- if (textEl) textEl.value = prev ? `${prev} ${text}` : text;
- },
- });
- });
submitEl?.addEventListener('click', async () => {
if (inFlight) return;
@@ -399,9 +386,6 @@ function openRepostModal({ navigate, channels = [], onSubmit }) {
@@ -429,15 +413,6 @@ function openRepostModal({ navigate, channels = [], onSubmit }) {
const close = () => { root.innerHTML = ''; };
root.querySelector('#repost-cancel')?.addEventListener('click', close);
- root.querySelector('#repost-voice')?.addEventListener('click', async () => {
- await openSpeechInputModal({
- navigate,
- onTextReady: (text) => {
- const prev = String(textEl?.value || '').trim();
- if (textEl) textEl.value = prev ? `${prev} ${text}` : text;
- },
- });
- });
submitEl?.addEventListener('click', async () => {
if (inFlight) return;
const idx = Number(selectEl?.value ?? -1);
diff --git a/shine-UI/js/pages/chat-view.js b/shine-UI/js/pages/chat-view.js
index 0cf39353..9fbbe469 100644
--- a/shine-UI/js/pages/chat-view.js
+++ b/shine-UI/js/pages/chat-view.js
@@ -313,27 +313,22 @@ function openChatActionsMenu({
});
}
-function showTtsMissingConfigDialog(navigate) {
+function showTtsMissingConfigDialog() {
const root = document.getElementById('modal-root');
if (!root) return;
root.innerHTML = `
Озвучка не настроена
-
Перейти в настройки инструментов?
+
Функция голосовых инструментов сейчас отключена.
-
-
+
`;
const close = () => { root.innerHTML = ''; };
- root.querySelector('#chat-tts-no')?.addEventListener('click', close);
- root.querySelector('#chat-tts-yes')?.addEventListener('click', () => {
- close();
- navigate('tools-settings-view');
- });
+ root.querySelector('#chat-tts-ok')?.addEventListener('click', close);
}
function autoResizeComposer(textarea) {
@@ -787,7 +782,7 @@ export function render({ navigate, route }) {
const handleReadAloud = async (msg) => {
if (!isTextToSpeechConfigured(state.entrySettings)) {
- showTtsMissingConfigDialog(navigate);
+ showTtsMissingConfigDialog();
return;
}
const parsedText = parseDmTechBlocks(String(msg?.text || ''));
diff --git a/shine-UI/js/pages/settings-view.js b/shine-UI/js/pages/settings-view.js
index 6630a53a..a66c16ec 100644
--- a/shine-UI/js/pages/settings-view.js
+++ b/shine-UI/js/pages/settings-view.js
@@ -1,5 +1,5 @@
import { renderHeader } from '../components/header.js';
-import { addAppLogEntry, authService, closeCurrentSessionAndSignOut, state } from '../state.js';
+import { addAppLogEntry, authService, closeCurrentSessionAndSignOut } from '../state.js';
export const pageMeta = { id: 'settings-view', title: 'Настройки' };
@@ -43,7 +43,6 @@ export function render({ navigate }) {
-
`;
@@ -51,7 +50,6 @@ export function render({ navigate }) {
card.querySelector('#settings-device').addEventListener('click', () => navigate('device-view'));
card.querySelector('#settings-remote-addblock').addEventListener('click', () => navigate('remote-addblock-session-view'));
card.querySelector('#settings-servers').addEventListener('click', () => navigate('server-settings-view'));
- card.querySelector('#settings-tools').addEventListener('click', () => navigate('tools-settings-view'));
card.querySelector('#settings-language').addEventListener('click', () => navigate('language-view'));
const signOutBtn = card.querySelector('#settings-signout');
diff --git a/shine-UI/js/pages/tools-settings-view.js b/shine-UI/js/pages/tools-settings-view.js
deleted file mode 100644
index b2391b7a..00000000
--- a/shine-UI/js/pages/tools-settings-view.js
+++ /dev/null
@@ -1,168 +0,0 @@
-import { renderHeader } from '../components/header.js';
-import { saveEntrySettings, state } from '../state.js';
-import { speakTextBySettings } from '../services/speech-tools-service.js';
-
-export const pageMeta = { id: 'tools-settings-view', title: 'Настройки инструментов' };
-
-function optionsMarkup(options, selected) {
- return options.map((opt) => (
- `
`
- )).join('');
-}
-
-export function render({ navigate }) {
- const screen = document.createElement('section');
- screen.className = 'stack';
-
- const stt = state.entrySettings.tools?.speechToText || {};
- const tts = state.entrySettings.tools?.textToSpeech || {};
-
- const card = document.createElement('div');
- card.className = 'card stack';
- card.innerHTML = `
-
Распознавание речи
-
-
-
-
-
-
-
-
-
-
- `;
-
- const card2 = document.createElement('div');
- card2.className = 'card stack';
- card2.innerHTML = `
-
Прочесть вслух (TTS)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Для офлайн-озвучки через Piper используйте локальный HTTP-обёртчик. Кнопка «шаблон» подставляет базовые значения.
- `;
-
- const actions = document.createElement('div');
- actions.className = 'auth-footer-actions';
- actions.innerHTML = `
-
-
- `;
-
- actions.querySelector('#tools-cancel')?.addEventListener('click', () => navigate('settings-view'));
- actions.querySelector('#tools-save')?.addEventListener('click', async () => {
- const next = {
- ...state.entrySettings,
- tools: {
- speechToText: {
- provider: card.querySelector('#stt-provider')?.value || 'openai',
- quality: card.querySelector('#stt-quality')?.value || 'medium',
- baseUrl: String(card.querySelector('#stt-base-url')?.value || '').trim(),
- apiKey: String(card.querySelector('#stt-api-key')?.value || '').trim(),
- model: String(card.querySelector('#stt-model')?.value || '').trim(),
- },
- textToSpeech: {
- provider: card2.querySelector('#tts-provider')?.value || 'browser',
- quality: card2.querySelector('#tts-quality')?.value || 'medium',
- voice: String(card2.querySelector('#tts-voice')?.value || '').trim(),
- piperBaseUrl: String(card2.querySelector('#tts-piper-url')?.value || '').trim(),
- externalBaseUrl: String(card2.querySelector('#tts-external-url')?.value || '').trim(),
- apiKey: String(card2.querySelector('#tts-api-key')?.value || '').trim(),
- model: String(card2.querySelector('#tts-model')?.value || '').trim(),
- },
- },
- };
- await saveEntrySettings(next);
- navigate('settings-view');
- });
-
- card2.querySelector('#piper-autofill')?.addEventListener('click', () => {
- card2.querySelector('#tts-provider').value = 'piper-http';
- card2.querySelector('#tts-piper-url').value = 'http://127.0.0.1:5000';
- card2.querySelector('#tts-quality').value = 'medium';
- if (!String(card2.querySelector('#tts-voice').value || '').trim()) {
- card2.querySelector('#tts-voice').value = 'ru_RU-irina-medium';
- }
- });
-
- card2.querySelector('#piper-links')?.addEventListener('click', () => {
- window.open('https://github.com/rhasspy/piper', '_blank', 'noopener,noreferrer');
- window.open('https://huggingface.co/rhasspy/piper-voices/tree/main', '_blank', 'noopener,noreferrer');
- });
-
- card2.querySelector('#tts-test-btn')?.addEventListener('click', async () => {
- const ttsProvider = card2.querySelector('#tts-provider')?.value || 'openai';
- const text = String(card2.querySelector('#tts-test-text')?.value || '').trim();
- const runtimeSettings = {
- ...state.entrySettings,
- tools: {
- ...state.entrySettings.tools,
- textToSpeech: {
- provider: ttsProvider,
- quality: card2.querySelector('#tts-quality')?.value || 'medium',
- voice: String(card2.querySelector('#tts-voice')?.value || '').trim(),
- piperBaseUrl: String(card2.querySelector('#tts-piper-url')?.value || '').trim(),
- externalBaseUrl: String(card2.querySelector('#tts-external-url')?.value || '').trim(),
- apiKey: String(card2.querySelector('#tts-api-key')?.value || '').trim(),
- model: String(card2.querySelector('#tts-model')?.value || '').trim(),
- },
- },
- };
- try {
- await speakTextBySettings(text || 'Проверка озвучки', runtimeSettings);
- } catch (error) {
- window.alert(`Ошибка озвучки: ${error?.message || 'unknown'}`);
- }
- });
-
- screen.append(
- renderHeader({
- title: 'Настройки инструментов',
- leftAction: { label: '←', onClick: () => navigate('settings-view') },
- }),
- card,
- card2,
- actions,
- );
-
- return screen;
-}
diff --git a/shine-UI/js/router.js b/shine-UI/js/router.js
index d07f52ad..fa65c242 100644
--- a/shine-UI/js/router.js
+++ b/shine-UI/js/router.js
@@ -30,7 +30,6 @@ const PRETTY_PATHS = new Map([
['notifications-view', 'notifications'],
['settings-view', 'settings'],
['server-settings-view', 'settings/servers'],
- ['tools-settings-view', 'settings/tools'],
['developer-settings-view', 'settings/developer'],
['trusted-device-login-settings-view', 'settings/device-login'],
['language-view', 'settings/language'],
@@ -260,7 +259,6 @@ export function getRoute() {
if (pageId === 'settings') {
const sub = decodePart(segments[1] || '').toLowerCase();
if (sub === 'servers') return { pageId: 'server-settings-view', params: {} };
- if (sub === 'tools') return { pageId: 'tools-settings-view', params: {} };
if (sub === 'developer') return { pageId: 'developer-settings-view', params: {} };
if (sub === 'device-login') return { pageId: 'trusted-device-login-settings-view', params: {} };
if (sub === 'language') return { pageId: 'language-view', params: {} };
@@ -372,7 +370,6 @@ export function resolveToolbarActive(pageId) {
pageId === 'settings-view' ||
pageId === 'developer-settings-view' ||
pageId === 'server-settings-view' ||
- pageId === 'tools-settings-view' ||
pageId === 'remote-addblock-session-view' ||
pageId === 'device-view' ||
pageId === 'connect-device-view' ||
diff --git a/shine-UI/js/services/speech-tools-service.js b/shine-UI/js/services/speech-tools-service.js
index e4580616..bde1ef62 100644
--- a/shine-UI/js/services/speech-tools-service.js
+++ b/shine-UI/js/services/speech-tools-service.js
@@ -1,9 +1,3 @@
-const OPENAI_MODELS_BY_QUALITY = {
- easy: 'whisper-1',
- medium: 'gpt-4o-mini-transcribe',
- hard: 'gpt-4o-transcribe',
-};
-
const PIPER_LENGTH_SCALE_BY_QUALITY = {
easy: '1.15',
medium: '1.0',
@@ -16,18 +10,6 @@ function normalizeOpenAiBaseUrl(url) {
return value.replace(/\/+$/, '');
}
-function resolveSttModel(config) {
- const quality = String(config?.quality || 'medium').toLowerCase();
- const customModel = String(config?.model || '').trim();
- return customModel || OPENAI_MODELS_BY_QUALITY[quality] || OPENAI_MODELS_BY_QUALITY.medium;
-}
-
-export function isSpeechToTextConfigured(entrySettings) {
- const cfg = entrySettings?.tools?.speechToText || {};
- if (String(cfg.provider || 'openai') !== 'openai') return false;
- return !!String(cfg.apiKey || '').trim();
-}
-
export function isTextToSpeechConfigured(entrySettings) {
const cfg = entrySettings?.tools?.textToSpeech || {};
const provider = String(cfg.provider || 'browser');
@@ -37,135 +19,6 @@ export function isTextToSpeechConfigured(entrySettings) {
return false;
}
-export async function transcribeAudioBySettings(audioBlob, entrySettings) {
- const cfg = entrySettings?.tools?.speechToText || {};
- const provider = String(cfg.provider || 'openai');
- if (provider !== 'openai') {
- throw new Error('Поддерживается только провайдер OpenAI для распознавания.');
- }
-
- const apiKey = String(cfg.apiKey || '').trim();
- if (!apiKey) throw new Error('Не заполнен OpenAI API key.');
-
- const model = resolveSttModel(cfg);
- const baseUrl = normalizeOpenAiBaseUrl(cfg.baseUrl);
-
- const form = new FormData();
- form.append('model', model);
- form.append('language', 'ru');
- form.append('response_format', 'json');
- form.append('file', audioBlob, 'voice-input.webm');
-
- const response = await fetch(`${baseUrl}/audio/transcriptions`, {
- method: 'POST',
- headers: {
- Authorization: `Bearer ${apiKey}`,
- },
- body: form,
- });
-
- if (!response.ok) {
- const body = await response.text().catch(() => '');
- throw new Error(`Ошибка STT API (${response.status}): ${body || 'unknown error'}`);
- }
-
- const payload = await response.json();
- const text = String(payload?.text || '').trim();
- if (!text) throw new Error('Пустой ответ распознавания.');
- return text;
-}
-
-export function createMicrophoneRecorder() {
- const Ctx = window.AudioContext || window.webkitAudioContext;
- let stream = null;
- let recorder = null;
- let startedAtMs = 0;
- let chunks = [];
- let timerId = 0;
- let level = 0;
- let analyser = null;
- let rafId = 0;
-
- async function start(onTick) {
- stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
- recorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });
- startedAtMs = Date.now();
- chunks = [];
-
- recorder.ondataavailable = (event) => {
- if (event?.data?.size > 0) chunks.push(event.data);
- };
- recorder.start(250);
-
- timerId = window.setInterval(() => {
- if (typeof onTick === 'function') {
- onTick({
- elapsedMs: Date.now() - startedAtMs,
- level,
- });
- }
- }, 120);
-
- if (Ctx) {
- const audioCtx = new Ctx();
- const source = audioCtx.createMediaStreamSource(stream);
- analyser = audioCtx.createAnalyser();
- analyser.fftSize = 256;
- source.connect(analyser);
- const data = new Uint8Array(analyser.frequencyBinCount);
- const read = () => {
- if (!analyser) return;
- analyser.getByteFrequencyData(data);
- let sum = 0;
- for (let i = 0; i < data.length; i += 1) sum += data[i];
- level = data.length > 0 ? Math.max(0, Math.min(1, (sum / data.length) / 255)) : 0;
- rafId = window.requestAnimationFrame(read);
- };
- read();
- }
- }
-
- async function stop() {
- if (!recorder) return null;
- const blob = await new Promise((resolve) => {
- recorder.onstop = () => resolve(new Blob(chunks, { type: 'audio/webm' }));
- recorder.stop();
- });
- cleanup();
- return blob;
- }
-
- function cancel() {
- try {
- recorder?.stop();
- } catch {
- // ignore
- }
- cleanup();
- }
-
- function cleanup() {
- if (timerId) window.clearInterval(timerId);
- if (rafId) window.cancelAnimationFrame(rafId);
- timerId = 0;
- rafId = 0;
- analyser = null;
- if (stream) {
- stream.getTracks().forEach((track) => {
- try {
- track.stop();
- } catch {
- // ignore
- }
- });
- }
- stream = null;
- recorder = null;
- }
-
- return { start, stop, cancel };
-}
-
export async function speakTextBySettings(text, entrySettings) {
const value = String(text || '').trim();
if (!value) return;
diff --git a/shine-UI/js/state.js b/shine-UI/js/state.js
index a26c0260..bcbab5ab 100644
--- a/shine-UI/js/state.js
+++ b/shine-UI/js/state.js
@@ -95,7 +95,6 @@ const DEFAULT_SHINE_SERVER_LOGIN_VALUE = DEFAULT_SHINE_SERVER_LOGIN;
const DEFAULT_SHINE_SERVER_HTTP_VALUE = DEFAULT_SHINE_SERVER_HTTP;
const DEFAULT_ARWEAVE_SERVER = 'https://arweave.net';
const DEFAULT_CALL_PREFLIGHT_TIMEOUT_MS = 6000;
-const DEFAULT_OPENAI_BASE_URL = 'https://api.openai.com/v1';
function normalizeStoredSolanaServer(value) {
const raw = String(value || '').trim();
@@ -109,16 +108,8 @@ export function normalizeDmChatId(value) {
function normalizeToolsSettings(rawTools) {
const source = rawTools && typeof rawTools === 'object' ? rawTools : {};
- const stt = source.speechToText && typeof source.speechToText === 'object' ? source.speechToText : {};
const tts = source.textToSpeech && typeof source.textToSpeech === 'object' ? source.textToSpeech : {};
return {
- speechToText: {
- provider: String(stt.provider || 'openai'),
- baseUrl: String(stt.baseUrl || DEFAULT_OPENAI_BASE_URL),
- apiKey: String(stt.apiKey || ''),
- quality: String(stt.quality || 'medium'),
- model: String(stt.model || ''),
- },
textToSpeech: {
provider: String(tts.provider || 'openai'),
quality: String(tts.quality || 'medium'),
diff --git a/shine-UI/styles/components.css b/shine-UI/styles/components.css
index 522b8577..33b33a52 100644
--- a/shine-UI/styles/components.css
+++ b/shine-UI/styles/components.css
@@ -5029,22 +5029,6 @@ html, body { overflow-x: hidden; }
height: 0;
}
-.voice-level-wrap {
- width: 100%;
- height: 8px;
- border-radius: 999px;
- background: rgba(255, 255, 255, 0.15);
- overflow: hidden;
-}
-
-.voice-level-fill {
- width: 2%;
- height: 100%;
- border-radius: 999px;
- background: rgba(212, 175, 55, 0.95);
- transition: width 80ms linear;
-}
-
.dm-screen .input,
.dm-input {
background: rgba(255, 255, 255, 0.04);
@@ -5140,16 +5124,6 @@ html, body { overflow-x: hidden; }
margin-top: 2px;
}
-.speech-actions-top {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 8px;
-}
-
-.speech-send-now-btn {
- width: 100%;
-}
-
.dm-screen .dm-list > .card.meta-muted {
width: calc(100% - 40px);
margin: 0 20px;