whisper-api-fast / server /simple-server.js
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;