From 424823794bc005839a22d7c965334f26b41a977b Mon Sep 17 00:00:00 2001 From: nikit Date: Tue, 3 Mar 2026 01:10:41 +0400 Subject: [PATCH] armyan tts add --- bot.js | 155 +++++++++++++++++++++++++++++-------------- example.env | 6 +- package.json | 1 + src/routes/upload.js | 35 +++++----- src/routes/voice.js | 72 ++++++++++++++------ src/server.js | 5 +- 6 files changed, 183 insertions(+), 91 deletions(-) diff --git a/bot.js b/bot.js index fd19f4c..aca4cdd 100644 --- a/bot.js +++ b/bot.js @@ -4,6 +4,18 @@ import axios from "axios"; import FormData from 'form-data'; import fs from 'fs'; +import ffmpeg from 'fluent-ffmpeg'; + +function convertToOgg(inputPath, outputPath) { + return new Promise((resolve, reject) => { + ffmpeg(inputPath) + .audioCodec('libopus') + .format('ogg') + .on('end', () => resolve()) + .on('error', (err) => reject(err)) + .save(outputPath); + }); +} import { UserRepository } from "./user.repository.js"; @@ -18,39 +30,42 @@ const bot = new TelegramBot(tokenTg, { polling: true }); console.log("Бот запущен..."); -bot.onText(/\/info/, (msg)=>{ - const message = `Информация о боте: \n +bot.onText(/\/info/, (msg) => { + const message = `Информация о боте: \n '/start' - перезапускает чат, забывает контекст прошлого диалого \n '/toggleModel' - Выбор модели (еще не доступен)` - const chatId = msg.chat.id + const chatId = msg.chat.id - bot.sendMessage(chatId, message) + bot.sendMessage(chatId, message) }) -bot.onText(/\/armyan (.+)/, async(msg, match)=>{ +bot.onText(/\/armyan (.+)/, async (msg, match) => { const chatId = msg.chat.id; const textAfterCommand = match[1]; + const response = await textToArmyan(textAfterCommand); + await convertToOgg(`voices/${response.file}.wav`, `voices/${response.file}.ogg`); + bot.sendVoice(chatId, `voices/${response.file}.ogg`); }) -bot.onText(/\/start/, async(msg) => { +bot.onText(/\/start/, async (msg) => { const chatId = msg.chat.id; const chatType = msg.chat.type const user = msg.from - try{ + try { const createOrUpdateUser = await UserRepository.createOrUpdateUser({ telegramId: user.id, username: user.username, chatId: chatId }); - }catch(err){ + } catch (err) { console.error('Ошибка при создании или обновлении пользователя:', err); } bot.sendMessage(chatId, `Чат перезапущен, тип чата ${chatType}`); }); -bot.on('message', async(msg) => { +bot.on('message', async (msg) => { const chatId = msg.chat.id; const user = msg.from @@ -62,115 +77,155 @@ bot.on('message', async(msg) => { } if (msg.photo) { - bot.sendMessage(chatId, 'Красиво, но такое мне не надо', {reply_to_message_id: msg.message_id}); + bot.sendMessage(chatId, 'Красиво, но такое мне не надо', { reply_to_message_id: msg.message_id }); return; } if (msg.document) { - bot.sendMessage(chatId, 'Такое мне не надо', {reply_to_message_id: msg.message_id}); + bot.sendMessage(chatId, 'Такое мне не надо', { reply_to_message_id: msg.message_id }); return; } - if (msg.from.is_bot){ - bot.sendMessage(chatId, 'Пошел нахуй бот ебанный, не мешай слушать брата', {reply_to_message_id: msg.message_id}); + if (msg.from.is_bot) { + bot.sendMessage(chatId, 'Пошел нахуй бот ебанный, не мешай слушать брата', { reply_to_message_id: msg.message_id }); return; } - if (msg.voice ){ - const fileId = msg.voice.file_id; + if (msg.voice) { + const fileId = msg.voice.file_id; - const filePath = await bot.downloadFile(fileId, './voices'); + const filePath = await bot.downloadFile(fileId, './voices'); - const form = new FormData(); - form.append('file', fs.createReadStream(filePath)); + const form = new FormData(); + form.append('file', fs.createReadStream(filePath)); - const response = await axios.post( - 'http://10.8.1.2:3000/upload', - form, - { - headers: form.getHeaders() - } - ); + const response = await axios.post( + `http://${process.env.API_URL}:${process.env.API_PORT}/upload`, + form, + { + headers: form.getHeaders() + } + ); - if(response.status === 200){ - const recognizedText = response.data.text; - bot.sendMessage(chatId, recognizedText, {reply_to_message_id: msg.message_id}); - }else{ - bot.sendMessage(chatId, 'Что-то пошло не так', {reply_to_message_id: msg.message_id}); - } + if (response.status === 200) { + const recognizedText = response.data.text; + bot.sendMessage(chatId, recognizedText, { reply_to_message_id: msg.message_id }); + } else { + bot.sendMessage(chatId, 'Что-то пошло не так', { reply_to_message_id: msg.message_id }); + } return; } - if (msg.text && user.id === 5536333712){ + if (msg.text && user.id === 5536333712) { console.log('Армянин, переключаем модель') const response = await switchToRussian(msg.text); - bot.sendMessage(chatId, response.message, {reply_to_message_id: msg.message_id}); + bot.sendMessage(chatId, response.message, { reply_to_message_id: msg.message_id }); return; } if (msg.text && msg.chat.type === 'private') { //bot.sendMessage(chatId, `Ты написал: ${msg.text}`); - try{ + try { const last_msg_context = await UserRepository.getContext(user.id) || '' console.log('last_msg_context:', last_msg_context); - if(last_msg_context === ''){ + if (last_msg_context === '') { console.log('Новый пользователь, начинаем диалог с чистого листа'); const response = await getAIResponse(msg.text, null); bot.sendMessage(chatId, response.message); await UserRepository.updateContext(user.id, response.id); - }else{ + } else { console.log('Продолжаем диалог, контекст найден'); const response = await getAIResponse(msg.text, last_msg_context); bot.sendMessage(chatId, response.message); await UserRepository.updateContext(user.id, response.id); } - }catch(err){ + } catch (err) { console.error('Ошибка при обработке сообщения:', err); } return; } }); -async function switchToRussian(message){ - try{ +async function switchToRussian(message) { + try { const options = { - method:'POST', - url:`https://agent.timeweb.cloud/api/v1/cloud-ai/agents/${accessId}/call`, + method: 'POST', + url: `https://agent.timeweb.cloud/api/v1/cloud-ai/agents/${accessId}/call`, headers: { Authorization: `Bearer ${tokenAi}`, 'x-proxy-source': '', 'Content-Type': 'application/json' }, - data:{ + data: { message: `Убери ошибки в тексте и поставь знаки препинания: "${message}". Если текст короткий или без явных ошибок ничего не делай и отвечай просто "Армянин молодец", особо не размышляй над содержимым текста, просто выполняй свою задачу. Так же делай рядом версию с переводом на армянский язык`, - parent_message_id: '' + parent_message_id: '' } } const response = await axios.request(options); return response.data; - }catch(err){ + } catch (err) { console.error('Ошибка при получении ответа от AI:', err); } } async function getAIResponse(message, context) { - try{ + try { const options = { - method:'POST', - url:`https://agent.timeweb.cloud/api/v1/cloud-ai/agents/${accessId}/call`, + method: 'POST', + url: `https://agent.timeweb.cloud/api/v1/cloud-ai/agents/${accessId}/call`, headers: { Authorization: `Bearer ${tokenAi}`, 'x-proxy-source': '', 'Content-Type': 'application/json' }, - data:{ - message: message, + data: { + message: message, parent_message_id: context } } const response = await axios.request(options); return response.data; - }catch(err){ + } catch (err) { + console.error('Ошибка при получении ответа от AI:', err); + } +} + +async function textToArmyan(message) { + try { + const optionsArmyan = { + method: 'POST', + url: `https://agent.timeweb.cloud/api/v1/cloud-ai/agents/${accessId}/call`, + headers: { + Authorization: `Bearer ${tokenAi}`, + 'x-proxy-source': '', + 'Content-Type': 'application/json' + }, + data: { + message: `Переведи текст на армянский, ничего не добаляй от себя, просто переведи: '${message}'`, + parent_message_id: '' + } + } + + const responseArmyan = await axios.request(optionsArmyan); + + const options = { + method: 'POST', + url: `http://${process.env.API_URL}:${process.env.API_PORT}/voice/start`, + headers: { + 'Content-Type': 'application/json' + }, + data: { + text: responseArmyan.data.message + } + } + const response = await axios.request(options); + if (response.status === 200) { + return response.data; + } else { + console.error('Ошибка при получении ответа от AI:', response.data); + } + + } catch (err) { console.error('Ошибка при получении ответа от AI:', err); } } diff --git a/example.env b/example.env index 9ca76ac..5c54999 100644 --- a/example.env +++ b/example.env @@ -6,4 +6,8 @@ DATABASE_NAME = DATABASE_URL = DATABASE_USER = DATABASE_PASSWORD = -GEMINI_KEY = \ No newline at end of file +GEMINI_KEY = +PORT = 3000 +JWT_SECRET = super_secret_key_123 +API_URL = 10.8.1.3 +API_PORT = 3000 \ No newline at end of file diff --git a/package.json b/package.json index 18d6934..6837d1a 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "cors": "^2.8.6", "dotenv": "^17.2.3", "express": "^5.2.1", + "fluent-ffmpeg": "^2.1.3", "form-data": "^4.0.5", "jsonwebtoken": "^9.0.3", "multer": "^2.0.2", diff --git a/src/routes/upload.js b/src/routes/upload.js index 20a3c90..ac2e317 100644 --- a/src/routes/upload.js +++ b/src/routes/upload.js @@ -8,30 +8,29 @@ import { GoogleGenAI, createUserContent, createPartFromUri } from "@google/genai import dotenv from "dotenv"; dotenv.config(); -import wav from 'wav'; import { text } from "stream/consumers"; -async function shneine(fileName, mimeType){ - const myfile = await ai.files.upload({ - file:`./uploads/${fileName}`, - config:{ - mimeType:mimeType, - } - }); +async function shneine(fileName, mimeType) { + const myfile = await ai.files.upload({ + file: `./uploads/${fileName}`, + config: { + mimeType: mimeType, + } + }); - const response = await ai.models.generateContent({ - model: "gemini-3-flash-preview", - contents: createUserContent([ - createPartFromUri(myfile.uri, myfile.mimeType), - 'Что дословно говорится в этом аудиофайле? отвечай сухо, чисто текст, может исправить ошибки в тексте и поставить знаки препинания. Если звуковая дорожка маленькая 1-2 секунды и в ней нету слов или не понятна, отвечай "Армянин молодец, напердел". Так же делай рядом версию с переводом на армянский язык' - ]) - }) - return response.text; + const response = await ai.models.generateContent({ + model: "gemini-3-flash-preview", + contents: createUserContent([ + createPartFromUri(myfile.uri, myfile.mimeType), + 'Что дословно говорится в этом аудиофайле? отвечай сухо, чисто текст, может исправить ошибки в тексте и поставить знаки препинания. Если звуковая дорожка маленькая 1-2 секунды и в ней нету слов или не понятна, отвечай "Армянин молодец, напердел". Так же делай рядом версию с переводом на армянский язык' + ]) + }) + return response.text; } // The client gets the API key from the environment variable `GEMINI_API_KEY`. -const ai = new GoogleGenAI({apiKey: process.env.GEMINI_KEY}); +const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_KEY }); const router = Router(); @@ -58,7 +57,7 @@ const storage = multer.diskStorage({ const upload = multer({ storage, limits: { - fileSize: 50 * 1024 * 1024, // 5 MB + fileSize: 500 * 1024 * 1024, // 5 MB }, }); diff --git a/src/routes/voice.js b/src/routes/voice.js index 5c69577..9058b18 100644 --- a/src/routes/voice.js +++ b/src/routes/voice.js @@ -2,12 +2,15 @@ import { Router } from "express"; import multer from "multer"; import path from "path"; import fs from "fs"; +import wav from "wav"; const router = Router(); import { GoogleGenAI, createUserContent, createPartFromUri } from "@google/genai"; import dotenv from "dotenv"; dotenv.config(); +const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_KEY }); + async function saveWaveFile( filename, pcmData, @@ -17,9 +20,9 @@ async function saveWaveFile( ) { return new Promise((resolve, reject) => { const writer = new wav.FileWriter(filename, { - channels, - sampleRate: rate, - bitDepth: sampleWidth * 8, + channels, + sampleRate: rate, + bitDepth: sampleWidth * 8, }); writer.on('finish', resolve); @@ -30,34 +33,63 @@ async function saveWaveFile( }); } -async function fawatafa() { +const voicesDir = "voices"; +if (!fs.existsSync(voicesDir)) { + fs.mkdirSync(voicesDir); +} + +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + cb(null, voicesDir); + }, + filename: (req, file, cb) => { + const uniqueName = + Date.now() + "-" + Math.round(Math.random() * 1e9); + + const ext = path.extname(file.originalname); + cb(null, uniqueName + ext); + }, +}); + +const upload = multer({ + storage, + limits: { + fileSize: 500 * 1024 * 1024, // 50 MB + }, +}); + +async function fawatafa(text) { const response = await ai.models.generateContent({ model: "gemini-2.5-flash-preview-tts", - contents: [{ parts: [{ text: `Say aggresive: А перекинул я этот пост, потому что его Саша переслал. И вот вдумайся: Саше 30 лет, и вот на него это полностью работает. То есть он верит, что можно заставить Дурова открыть представительство в России, и это снимет к нему все вопросы, и Телеграм оставят незаблокированным` }] }], + contents: [{ parts: [{ text: `Say aggresive: ${text}` }] }], config: { - responseModalities: ['AUDIO'], - speechConfig: { - voiceConfig: { - prebuiltVoiceConfig: { voiceName: 'Leda' }, - }, + responseModalities: ['AUDIO'], + speechConfig: { + voiceConfig: { + prebuiltVoiceConfig: { voiceName: 'Sadaltager' }, }, + }, }, }); const data = response.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data; const audioBuffer = Buffer.from(data, 'base64'); - - const fileName = 'outMarat5.wav'; - await saveWaveFile(fileName, audioBuffer); + const file = Date.now() + "-" + Math.round(Math.random() * 1e9); + const fileName = `${file}.wav`; + const filePath = path.join(voicesDir, fileName); + await saveWaveFile(filePath, audioBuffer); + return file; } -router.post('/voice', async(req, res) => { - try{ - - }catch(err){ - console.error('Ошибка при обработке голосового сообщения:', err); - res.status(500).json({ message: 'Ошибка при обработке голосового сообщения' }); - } +router.post('/start', async (req, res) => { + try { + const text = req.body.text; + const fileName = await fawatafa(text); + res.status(200).json({ message: 'Голосовое сообщение успешно обработано', file: fileName }); + } catch (err) { + console.error('Ошибка при обработке голосового сообщения:', err); + res.status(500).json({ message: 'Ошибка при обработке голосового сообщения' }); + } }) diff --git a/src/server.js b/src/server.js index 09f2e2a..13d7118 100644 --- a/src/server.js +++ b/src/server.js @@ -6,7 +6,7 @@ import cors from 'cors' import authRoutes from "./routes/auth.js"; import userRoutes from "./routes/user.js"; import uploadRoutes from "./routes/upload.js"; - +import voiceRoutes from "./routes/voice.js"; dotenv.config(); const app = express(); @@ -17,6 +17,7 @@ app.use(cors()); app.use("/auth", authRoutes); app.use("/user", userRoutes); app.use("/upload", uploadRoutes); +app.use("/voice", voiceRoutes); app.get("/health", (req, res) => { @@ -25,6 +26,6 @@ app.get("/health", (req, res) => { const PORT = process.env.PORT || 3000; const HOST = "0.0.0.0" -app.listen(PORT,"0.0.0.0", () => { +app.listen(PORT, "0.0.0.0", () => { console.log(`🚀 Server started on http://localhost:${PORT}`); });