import http from 'http'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { pipeline } from 'stream/promises'; import crypto from 'crypto'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const PORT = 7860; // Порт для Hugging Face Spaces // Создаем папку для загрузок const uploadDir = path.join(__dirname, '..', 'uploads'); if (!fs.existsSync(uploadDir)) { fs.mkdirSync(uploadDir, { recursive: true }); } // Хранилище задач транскрипции const transcriptionTasks = new Map(); // Генерация ID задачи function generateTaskId() { return crypto.randomBytes(16).toString('hex'); } // Создание задачи function createTask(taskId, filename, language, model) { const task = { id: taskId, filename: filename, language: language, model: model, status: 'processing', progress: 0, result: null, error: null, createdAt: new Date(), updatedAt: new Date() }; transcriptionTasks.set(taskId, task); return task; } // Обновление статуса задачи function updateTask(taskId, updates) { const task = transcriptionTasks.get(taskId); if (task) { Object.assign(task, updates, { updatedAt: new Date() }); } return task; } // Функция для парсинга multipart/form-data function parseMultipartData(body, boundary) { const parts = body.split('--' + boundary); const result = {}; for (const part of parts) { if (part.includes('Content-Disposition: form-data')) { const lines = part.split('\r\n'); let name = ''; let value = ''; let isFile = false; let filename = ''; for (const line of lines) { if (line.startsWith('Content-Disposition: form-data; name=')) { name = line.split('name=')[1].replace(/"/g, ''); } if (line.includes('filename=')) { isFile = true; filename = line.split('filename=')[1].replace(/"/g, ''); } if (line === '' && !isFile) { const valueIndex = lines.indexOf(line) + 1; value = lines[valueIndex]; } } if (isFile) { result[name] = { filename, isFile: true }; } else { result[name] = value; } } } return result; } // Функция для отправки JSON ответа function sendJsonResponse(res, statusCode, data) { res.writeHead(statusCode, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(data)); } // Функция для отправки CORS заголовков function setCorsHeaders(res) { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); } // Имитация длинной транскрипции (замените на реальную) async function processTranscription(taskId, audioPath, language, model) { const task = transcriptionTasks.get(taskId); if (!task) return; try { // Имитируем прогресс for (let i = 0; i <= 100; i += 10) { updateTask(taskId, { progress: i }); await new Promise(resolve => setTimeout(resolve, 1000)); // 1 секунда на 10% } // Имитируем результат const result = { text: `[Транскрипция завершена: ${task.filename}]`, language: language, model: model, duration: 120.5, segments: [], timestamp: new Date().toISOString() }; updateTask(taskId, { status: 'completed', progress: 100, result: result }); } catch (error) { updateTask(taskId, { status: 'error', error: error.message }); } } // Создаем HTTP сервер const server = http.createServer(async (req, res) => { // Увеличиваем таймауты для длинных операций req.setTimeout(3600000); // 1 час res.setTimeout(3600000); // 1 час setCorsHeaders(res); // Обработка preflight запросов if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } const url = new URL(req.url, `http://${req.headers.host}`); const pathname = url.pathname; try { // API endpoints if (pathname === '/api/health' && req.method === 'GET') { sendJsonResponse(res, 200, { status: 'ok', message: 'Whisper API сервер работает', timestamp: new Date().toISOString() }); return; } if (pathname === '/api/models' && req.method === 'GET') { const models = [ { id: 'tiny', name: 'Tiny', size: '39 MB', languages: 'multilingual' }, { id: 'base', name: 'Base', size: '74 MB', languages: 'multilingual' }, { id: 'small', name: 'Small', size: '244 MB', languages: 'multilingual' }, { id: 'medium', name: 'Medium', size: '769 MB', languages: 'multilingual' }, { id: 'large', name: 'Large', size: '1550 MB', languages: 'multilingual' } ]; sendJsonResponse(res, 200, { models: models, default: 'base' }); return; } if (pathname === '/api/languages' && req.method === 'GET') { const languages = [ { code: 'auto', name: 'Автоопределение' }, { code: 'ru', name: 'Русский' }, { code: 'en', name: 'English' }, { code: 'es', name: 'Español' }, { code: 'fr', name: 'Français' }, { code: 'de', name: 'Deutsch' }, { code: 'it', name: 'Italiano' }, { code: 'pt', name: 'Português' }, { code: 'pl', name: 'Polski' }, { code: 'tr', name: 'Türkçe' }, { code: 'ja', name: '日本語' }, { code: 'ko', name: '한국어' }, { code: 'zh', name: '中文' } ]; sendJsonResponse(res, 200, { languages: languages, default: 'auto' }); return; } // Новый endpoint для асинхронной транскрипции if (pathname === '/api/transcribe' && req.method === 'POST') { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', () => { try { const contentType = req.headers['content-type'] || ''; if (contentType.includes('multipart/form-data')) { const boundary = contentType.split('boundary=')[1]; const formData = parseMultipartData(body, boundary); console.log('Получены данные формы:', formData); // Создаем задачу const taskId = generateTaskId(); const task = createTask( taskId, formData.audio?.filename || 'unknown', formData.language || 'auto', formData.model || 'base' ); // Запускаем транскрипцию асинхронно processTranscription(taskId, null, task.language, task.model); // Сразу возвращаем ID задачи sendJsonResponse(res, 200, { success: true, taskId: taskId, status: 'processing', message: 'Транскрипция началась', timestamp: new Date().toISOString() }); } else { sendJsonResponse(res, 400, { error: 'Неверный Content-Type. Ожидается multipart/form-data' }); } } catch (error) { console.error('Ошибка обработки запроса:', error); sendJsonResponse(res, 500, { error: 'Внутренняя ошибка сервера', details: error.message }); } }); return; } // Endpoint для проверки статуса задачи if (pathname.startsWith('/api/status/') && req.method === 'GET') { const taskId = pathname.split('/api/status/')[1]; const task = transcriptionTasks.get(taskId); if (!task) { sendJsonResponse(res, 404, { error: 'Задача не найдена' }); return; } sendJsonResponse(res, 200, { success: true, task: task, timestamp: new Date().toISOString() }); return; } // Endpoint для получения результата if (pathname.startsWith('/api/result/') && req.method === 'GET') { const taskId = pathname.split('/api/result/')[1]; const task = transcriptionTasks.get(taskId); if (!task) { sendJsonResponse(res, 404, { error: 'Задача не найдена' }); return; } if (task.status === 'processing') { sendJsonResponse(res, 202, { success: true, status: 'processing', progress: task.progress, message: 'Транскрипция еще выполняется' }); return; } if (task.status === 'error') { sendJsonResponse(res, 500, { success: false, status: 'error', error: task.error }); return; } sendJsonResponse(res, 200, { success: true, status: 'completed', transcription: task.result, timestamp: new Date().toISOString() }); return; } if (pathname === '/api/transcribe/url' && req.method === 'POST') { let body = ''; req.on('data', chunk => { body += chunk.toString(); }); req.on('end', () => { try { const data = JSON.parse(body); const { url, language = 'auto', model = 'base' } = data; if (!url) { sendJsonResponse(res, 400, { error: 'URL не предоставлен' }); return; } console.log(`Получен URL: ${url}`); console.log(`Язык: ${language}, Модель: ${model}`); // Создаем задачу для URL const taskId = generateTaskId(); const task = createTask(taskId, url, language, model); // Запускаем транскрипцию асинхронно processTranscription(taskId, url, language, model); sendJsonResponse(res, 200, { success: true, taskId: taskId, status: 'processing', message: 'Транскрипция по URL началась', timestamp: new Date().toISOString() }); } catch (error) { console.error('Ошибка обработки JSON:', error); sendJsonResponse(res, 400, { error: 'Неверный JSON формат' }); } }); return; } // API 404 if (pathname.startsWith('/api/')) { sendJsonResponse(res, 404, { error: 'API endpoint не найден' }); return; } // Serve static files let filePath = path.join(__dirname, '../dist', pathname === '/' ? 'index.html' : pathname); if (!fs.existsSync(filePath)) { filePath = path.join(__dirname, '../dist/index.html'); } const ext = path.extname(filePath); const contentType = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpg', '.gif': 'image/gif', '.svg': 'image/svg+xml' }[ext] || 'text/plain'; res.writeHead(200, { 'Content-Type': contentType }); fs.createReadStream(filePath).pipe(res); } catch (error) { console.error('Ошибка сервера:', error); sendJsonResponse(res, 500, { error: 'Внутренняя ошибка сервера', details: error.message }); } }); // Устанавливаем таймаут сервера server.timeout = 3600000; // 1 час // Запускаем сервер server.listen(PORT, () => { console.log(`🚀 Whisper API сервер запущен на порту ${PORT}`); console.log(`📝 API документация: http://localhost:${PORT}/api/health`); console.log(`🌐 Веб-приложение: http://localhost:${PORT}`); console.log(`⏱️ Таймауты увеличены до 1 часа для длинных транскрипций`); }); export default server;