Spaces:
Sleeping
Sleeping
Evgeny Naumov
Исправлена проблема с правами доступа: создание папки uploads в Dockerfile и обновлен путь в сервере
a03b39f
| 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; |