diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..22a2961
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,114 @@
+
+
+
+
+
+ Say — Whisper test
+
+
+
+
+
+
+
+
+
+
|
+
Say — Whisper test
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/server.js b/server.js
index 2b4a02c..2a7fc3b 100644
--- a/server.js
+++ b/server.js
@@ -1,2 +1,81 @@
-// Точка входа для managed-платформы (ждёт index.js в корне)
-require('./src/index.js');
+/**
+ * Say — минимальная версия (только Whisper).
+ * Работает: загрузка аудио → транскрибация → текст.
+ */
+
+const express = require('express');
+const multer = require('multer');
+const fetch = require('node-fetch');
+const FormData = require('form-data');
+const path = require('path');
+
+const PORT = parseInt(process.env.PORT, 10) || 3000;
+
+// --- Конфигурация Whisper ---
+const WHISPER_URL = process.env.WHISPER_URL || 'https://api.aillm.ru/v1/audio/transcriptions';
+const WHISPER_MODEL = process.env.WHISPER_MODEL || 'whisper-large-v3';
+const API_KEY = process.env.API_KEY || '';
+
+const upload = multer({ storage: multer.memoryStorage() });
+const app = express();
+
+// --- Статика ---
+app.use(express.static(path.join(__dirname, 'public')));
+
+// --- Роуты ---
+
+// Главная
+app.get('/', (_req, res) => {
+ res.sendFile(path.join(__dirname, 'public', 'index.html'));
+});
+
+// Health
+app.get('/health', (_req, res) => {
+ res.json({ status: 'ok' });
+});
+
+// Whisper: аудио → текст
+app.post('/api/speech', upload.single('audio'), async (req, res) => {
+ try {
+ if (!req.file || !req.file.buffer || req.file.size === 0) {
+ return res.status(400).json({ error: 'Нет аудио' });
+ }
+
+ const form = new FormData();
+ form.append('file', req.file.buffer, {
+ filename: 'audio.webm',
+ contentType: req.file.mimetype || 'audio/webm',
+ });
+ form.append('model', WHISPER_MODEL);
+ form.append('language', 'ru');
+
+ console.log(`Whisper: отправка ${req.file.size} байт`);
+
+ const resp = await fetch(WHISPER_URL, {
+ method: 'POST',
+ headers: { Authorization: `Bearer ${API_KEY}` },
+ body: form,
+ });
+
+ if (!resp.ok) {
+ const errText = await resp.text();
+ console.error(`Whisper error ${resp.status}: ${errText}`);
+ return res.status(500).json({ error: `Whisper: ${resp.status}` });
+ }
+
+ const data = await resp.json();
+ const text = (data.text || '').trim();
+
+ console.log(`Whisper: распознано "${text}"`);
+ res.json({ transcription: text });
+
+ } catch (err) {
+ console.error('ERROR:', err.message);
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// --- Запуск ---
+app.listen(PORT, () => {
+ console.log(`Say (Whisper only) listening on :${PORT}`);
+});
diff --git a/src/app.js b/src/app.js
deleted file mode 100644
index 8c60968..0000000
--- a/src/app.js
+++ /dev/null
@@ -1,29 +0,0 @@
-/**
- * Express-приложение Say.
- * Собирает middleware, роуты, отдаёт статику и шаблоны.
- */
-
-const express = require('express');
-const path = require('path');
-
-const pagesRouter = require('./routes/pages');
-const apiRouter = require('./routes/api');
-const errorHandler = require('./middleware/errorHandler');
-
-function createApp() {
- const app = express();
-
- // --- Статика (CSS, лого, favicon) — как в ipwhitelist: public/ → / ---
- app.use(express.static(path.join(__dirname, '..', 'public')));
-
- // --- Роуты ---
- app.use('/', pagesRouter);
- app.use('/api', apiRouter);
-
- // --- Обработка ошибок ---
- app.use(errorHandler);
-
- return app;
-}
-
-module.exports = createApp;
diff --git a/src/config.js b/src/config.js
deleted file mode 100644
index 738a117..0000000
--- a/src/config.js
+++ /dev/null
@@ -1,54 +0,0 @@
-/**
- * Централизованная конфигурация приложения Say.
- * Все секреты — из переменных окружения, без хардкода.
- */
-
-const PORT = parseInt(process.env.PORT, 10) || 3000;
-
-// --- API (единый эндпоинт aillm.ru) ---
-const API_BASE = process.env.API_BASE || 'https://api.aillm.ru';
-const API_KEY = process.env.API_KEY || '';
-
-// --- Whisper ---
-const WHISPER_URL = process.env.WHISPER_URL || `${API_BASE}/v1/audio/transcriptions`;
-const WHISPER_MODEL = process.env.WHISPER_MODEL || 'whisper-large-v3';
-
-// --- Три LLM-профиля ---
-const LLM_PROFILES = [
- {
- id: 'ddm',
- name: 'DDM-120',
- description: 'Быстрая модель, общие вопросы',
- url: process.env.LLM_URL_DDM || `${API_BASE}/v1/chat/completions`,
- model: process.env.LLM_MODEL_DDM || 'ddm-120',
- systemPrompt: process.env.LLM_SYSTEM_DDM || 'Ты — полезный ассистент. Отвечай кратко и по делу на том же языке, что и запрос.',
- temperature: parseFloat(process.env.LLM_TEMP_DDM || '0.7'),
- maxTokens: parseInt(process.env.LLM_TOKENS_DDM, 10) || 1024,
- },
- {
- id: 'qwen',
- name: 'Qwen',
- description: 'Мощная модель, сложные задачи',
- url: process.env.LLM_URL_QWEN || `${API_BASE}/v1/chat/completions`,
- model: process.env.LLM_MODEL_QWEN || 'qwen-plus',
- systemPrompt: process.env.LLM_SYSTEM_QWEN || 'Ты — эксперт-аналитик. Отвечай развернуто, структурированно, на языке запроса.',
- temperature: parseFloat(process.env.LLM_TEMP_QWEN || '0.7'),
- maxTokens: parseInt(process.env.LLM_TOKENS_QWEN, 10) || 2048,
- },
- {
- id: 'deepseek',
- name: 'DeepSeek',
- description: 'Глубокая аналитика, код, рассуждения',
- url: process.env.LLM_URL_DEEPSEEK || `${API_BASE}/v1/chat/completions`,
- model: process.env.LLM_MODEL_DEEPSEEK || 'deepseek-chat',
- systemPrompt: process.env.LLM_SYSTEM_DEEPSEEK || 'Ты — глубокий аналитик и программист. Думай step-by-step, отвечай на языке запроса.',
- temperature: parseFloat(process.env.LLM_TEMP_DEEPSEEK || '0.7'),
- maxTokens: parseInt(process.env.LLM_TOKENS_DEEPSEEK, 10) || 4096,
- },
-];
-
-module.exports = {
- PORT, API_BASE, API_KEY,
- WHISPER_URL, WHISPER_MODEL,
- LLM_PROFILES,
-};
diff --git a/src/index.js b/src/index.js
deleted file mode 100644
index 76453ea..0000000
--- a/src/index.js
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * Точка входа. Запускает HTTP-сервер.
- */
-
-const createApp = require('./app');
-const { PORT } = require('./config');
-
-const app = createApp();
-
-app.listen(PORT, () => {
- console.log(`Say server listening on :${PORT}`);
-});
diff --git a/src/middleware/errorHandler.js b/src/middleware/errorHandler.js
deleted file mode 100644
index 688fb38..0000000
--- a/src/middleware/errorHandler.js
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * Централизованный обработчик ошибок Express.
- *
- * Ловит все ошибки, проброшенные через next(err),
- * возвращает JSON с кодом и сообщением.
- */
-
-function errorHandler(err, _req, res, _next) {
- console.error('ERROR:', err.message || err);
-
- // Определяем HTTP-статус
- const status = err.status || err.statusCode || 500;
-
- res.status(status).json({
- error: err.message || 'Внутренняя ошибка сервера',
- });
-}
-
-module.exports = errorHandler;
diff --git a/src/routes/api.js b/src/routes/api.js
deleted file mode 100644
index 0cfaa2c..0000000
--- a/src/routes/api.js
+++ /dev/null
@@ -1,74 +0,0 @@
-/**
- * API-роуты.
- *
- * GET /api/profiles — список LLM-профилей.
- * POST /api/speech/:llmId — аудио → Whisper → LLM → ответ.
- */
-
-const { Router } = require('express');
-const multer = require('multer');
-
-const { transcribe } = require('../services/whisper');
-const { getPublicProfiles, chat } = require('../services/llm');
-
-const router = Router();
-
-// Загрузка аудио в память (файлы маленькие — webm с микрофона)
-const upload = multer({ storage: multer.memoryStorage() });
-
-/**
- * GET /api/profiles
- * Возвращает публичную информацию о доступных LLM.
- */
-router.get('/profiles', (_req, res) => {
- res.json(getPublicProfiles());
-});
-
-/**
- * POST /api/speech/:llmId
- *
- * Принимает multipart/form-data с полем "audio".
- * 1. Whisper → текст.
- * 2. LLM (по profileId) → ответ.
- * 3. JSON: { transcription, response, llm: { id, name } }
- */
-router.post('/speech/:llmId', upload.single('audio'), async (req, res, next) => {
- const { llmId } = req.params;
-
- // --- Валидация ---
- if (!req.file || !req.file.buffer || req.file.size === 0) {
- return res.status(400).json({ error: 'Аудиофайл пуст или отсутствует' });
- }
-
- const audioBuffer = req.file.buffer;
- const mimeType = req.file.mimetype || 'audio/webm';
-
- try {
- // Шаг 1: Whisper
- const transcription = await transcribe(audioBuffer, mimeType);
-
- if (!transcription) {
- return res.status(400).json({ error: 'Не удалось распознать речь', transcription: '' });
- }
-
- // Шаг 2: LLM
- const answer = await chat(llmId, transcription);
-
- // Шаг 3: Ответ
- const { getPublicProfiles } = require('../services/llm');
- const profile = getPublicProfiles().find(p => p.id === llmId);
-
- res.json({
- transcription,
- response: answer,
- llm: {
- id: profile ? profile.id : llmId,
- name: profile ? profile.name : llmId,
- },
- });
- } catch (err) {
- next(err);
- }
-});
-
-module.exports = router;
diff --git a/src/routes/pages.js b/src/routes/pages.js
deleted file mode 100644
index 871acb3..0000000
--- a/src/routes/pages.js
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * Роуты HTML-страниц.
- * GET / — главная страница (интерфейс с микрофоном).
- * GET /health — проверка живости.
- */
-
-const { Router } = require('express');
-const path = require('path');
-
-const router = Router();
-
-router.get('/', (_req, res) => {
- res.sendFile(path.join(__dirname, '..', '..', 'templates', 'index.html'));
-});
-
-router.get('/health', (_req, res) => {
- res.json({ status: 'ok' });
-});
-
-module.exports = router;
diff --git a/src/services/llm.js b/src/services/llm.js
deleted file mode 100644
index fc786f9..0000000
--- a/src/services/llm.js
+++ /dev/null
@@ -1,77 +0,0 @@
-/**
- * LLMService — обращение к языковой модели.
- *
- * Поддерживает несколько профилей (разные модели/промпты).
- * Пользователь выбирает профиль по id, сюда приходит текст — возвращается ответ.
- */
-
-const fetch = require('node-fetch');
-
-const { LLM_PROFILES, API_KEY } = require('../config');
-
-/** @type {Map} */
-const profiles = new Map();
-for (const p of LLM_PROFILES) {
- profiles.set(p.id, p);
- console.log(`LLM: зарегистрирован профиль '${p.id}' (${p.model})`);
-}
-
-/**
- * Публичный список профилей для фронтенда (без чувствительных полей).
- */
-function getPublicProfiles() {
- return LLM_PROFILES.map(p => ({
- id: p.id,
- name: p.name,
- description: p.description,
- }));
-}
-
-/**
- * @param {string} profileId — id профиля (ddm, qwen, deepseek)
- * @param {string} userText — текст пользователя
- * @returns {Promise} — ответ модели
- */
-async function chat(profileId, userText) {
- const profile = profiles.get(profileId);
- if (!profile) {
- const available = [...profiles.keys()].join(', ');
- throw new Error(`Неизвестный LLM-профиль '${profileId}'. Доступны: ${available}`);
- }
-
- const key = API_KEY;
- if (!key) throw new Error('API_KEY не задан в переменных окружения');
-
- console.log(`LLM [${profileId}]: отправка (${userText.length} символов)`);
-
- const resp = await fetch(profile.url, {
- method: 'POST',
- headers: {
- Authorization: `Bearer ${key}`,
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- model: profile.model,
- messages: [
- { role: 'system', content: profile.systemPrompt },
- { role: 'user', content: userText },
- ],
- temperature: profile.temperature,
- max_tokens: profile.maxTokens,
- }),
- timeout: 60000,
- });
-
- if (!resp.ok) {
- const text = await resp.text();
- throw new Error(`LLM ошибка ${resp.status}: ${text.slice(0, 200)}`);
- }
-
- const data = await resp.json();
- const answer = data.choices[0].message.content.trim();
-
- console.log(`LLM [${profileId}]: ответ (${answer.length} символов)`);
- return answer;
-}
-
-module.exports = { getPublicProfiles, chat };
diff --git a/src/services/whisper.js b/src/services/whisper.js
deleted file mode 100644
index ad2c334..0000000
--- a/src/services/whisper.js
+++ /dev/null
@@ -1,70 +0,0 @@
-/**
- * WhisperService — распознавание речи (speech-to-text).
- *
- * Принимает буфер аудио + MIME-тип,
- * отправляет на OpenAI-совместимый /v1/audio/transcriptions,
- * возвращает распознанный текст.
- */
-
-const fetch = require('node-fetch');
-const FormData = require('form-data');
-
-const { WHISPER_URL, WHISPER_MODEL, API_KEY } = require('../config');
-
-/**
- * @param {Buffer} audioBuffer — сырые байты аудио
- * @param {string} mimeType — MIME-тип (audio/webm, audio/wav, ...)
- * @param {string} language — язык распознавания (ISO 639-1)
- * @returns {Promise} — распознанный текст
- */
-async function transcribe(audioBuffer, mimeType = 'audio/webm', language = 'ru') {
- if (!API_KEY) throw new Error('API_KEY не задан в переменных окружения');
-
- const suffix = mimeToExt(mimeType);
-
- const form = new FormData();
- form.append('file', audioBuffer, {
- filename: `audio${suffix}`,
- contentType: mimeType,
- });
- form.append('model', WHISPER_MODEL);
- form.append('language', language);
-
- console.log(`Whisper: отправка ${audioBuffer.length} байт (${mimeType})`);
-
- const resp = await fetch(WHISPER_URL, {
- method: 'POST',
- headers: {
- Authorization: `Bearer ${API_KEY}`,
- },
- body: form,
- timeout: 30000,
- });
-
- if (!resp.ok) {
- const text = await resp.text();
- throw new Error(`Whisper ошибка ${resp.status}: ${text.slice(0, 200)}`);
- }
-
- const data = await resp.json();
- const result = (data.text || '').trim();
-
- console.log(`Whisper: распознано ${result.length} символов`);
- return result;
-}
-
-/** MIME → расширение файла */
-function mimeToExt(mime) {
- const map = {
- 'audio/webm': '.webm',
- 'audio/wav': '.wav',
- 'audio/wave': '.wav',
- 'audio/mp3': '.mp3',
- 'audio/mpeg': '.mp3',
- 'audio/ogg': '.ogg',
- 'audio/flac': '.flac',
- };
- return map[mime] || '.webm';
-}
-
-module.exports = { transcribe };
diff --git a/templates/index.html b/templates/index.html
deleted file mode 100644
index 8412588..0000000
--- a/templates/index.html
+++ /dev/null
@@ -1,283 +0,0 @@
-
-
-
-
-
- Say — Голосовой ассистент | Nubes
-
-
-
-
-
-
-
-
-
-
-
-
-
-
|
-
Say — Голосовой ассистент
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Загрузка моделей...
-
-
-
-
-
-
-
-
-
📝 Распознано (Whisper)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-