2026-05-29 09:17:08 +00:00
|
|
|
|
"""FastAPI backend для сервиса транскрибации."""
|
|
|
|
|
|
|
|
|
|
|
|
import json
|
2026-05-29 09:30:39 +00:00
|
|
|
|
from contextlib import asynccontextmanager
|
2026-05-29 09:17:08 +00:00
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
from typing import List, Optional
|
|
|
|
|
|
|
|
|
|
|
|
from fastapi import FastAPI, File, UploadFile, WebSocket, WebSocketDisconnect
|
|
|
|
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
|
|
from fastapi.responses import FileResponse, PlainTextResponse, HTMLResponse
|
|
|
|
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
|
|
|
|
|
|
|
|
from backend.queue import (
|
|
|
|
|
|
UPLOAD_DIR,
|
|
|
|
|
|
PROCESSED_DIR,
|
|
|
|
|
|
enqueue,
|
|
|
|
|
|
get_all_tasks,
|
|
|
|
|
|
get_task_status,
|
|
|
|
|
|
get_processed_tree,
|
|
|
|
|
|
read_file_content,
|
|
|
|
|
|
set_progress_callback,
|
2026-05-29 09:30:39 +00:00
|
|
|
|
start_workers,
|
|
|
|
|
|
stop_workers,
|
2026-05-29 09:17:08 +00:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# WebSocket менеджер
|
|
|
|
|
|
class ConnectionManager:
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
self.active_connections: List[WebSocket] = []
|
|
|
|
|
|
|
|
|
|
|
|
async def connect(self, websocket: WebSocket):
|
|
|
|
|
|
await websocket.accept()
|
|
|
|
|
|
self.active_connections.append(websocket)
|
|
|
|
|
|
|
|
|
|
|
|
def disconnect(self, websocket: WebSocket):
|
|
|
|
|
|
if websocket in self.active_connections:
|
|
|
|
|
|
self.active_connections.remove(websocket)
|
|
|
|
|
|
|
|
|
|
|
|
async def broadcast(self, message: dict):
|
|
|
|
|
|
for conn in self.active_connections:
|
|
|
|
|
|
try:
|
|
|
|
|
|
await conn.send_json(message)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
manager = ConnectionManager()
|
|
|
|
|
|
|
|
|
|
|
|
# Устанавливаем callback для отправки прогресса через WebSocket
|
|
|
|
|
|
set_progress_callback(manager.broadcast)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-29 09:30:39 +00:00
|
|
|
|
@asynccontextmanager
|
|
|
|
|
|
async def lifespan(app: FastAPI):
|
|
|
|
|
|
"""Управление жизненным циклом приложения."""
|
|
|
|
|
|
print("🚀 Запуск рабочих процессов...")
|
|
|
|
|
|
start_workers(num_workers=1)
|
|
|
|
|
|
yield
|
|
|
|
|
|
print("🛑 Остановка рабочих процессов...")
|
|
|
|
|
|
stop_workers()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app = FastAPI(
|
|
|
|
|
|
title="Transcription Service",
|
|
|
|
|
|
version="1.0.0",
|
|
|
|
|
|
lifespan=lifespan,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# CORS
|
|
|
|
|
|
app.add_middleware(
|
|
|
|
|
|
CORSMiddleware,
|
|
|
|
|
|
allow_origins=["*"],
|
|
|
|
|
|
allow_credentials=True,
|
|
|
|
|
|
allow_methods=["*"],
|
|
|
|
|
|
allow_headers=["*"],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-29 09:17:08 +00:00
|
|
|
|
# === API Endpoints ===
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/", response_class=HTMLResponse)
|
|
|
|
|
|
async def root():
|
|
|
|
|
|
"""Главная страница."""
|
|
|
|
|
|
index_path = Path(__file__).parent / "static" / "index.html"
|
|
|
|
|
|
if index_path.exists():
|
|
|
|
|
|
return index_path.read_text(encoding="utf-8")
|
|
|
|
|
|
return "<h1>Transcription Service</h1><p>Frontend not built</p>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/upload")
|
|
|
|
|
|
async def upload_file(file: UploadFile = File(...)):
|
|
|
|
|
|
"""Загружает файл и добавляет в очередь обработки."""
|
|
|
|
|
|
# Сохраняем файл
|
|
|
|
|
|
file_path = UPLOAD_DIR / file.filename
|
|
|
|
|
|
with open(file_path, "wb") as f:
|
|
|
|
|
|
content = await file.read()
|
|
|
|
|
|
f.write(content)
|
|
|
|
|
|
|
|
|
|
|
|
# Добавляем в очередь
|
|
|
|
|
|
task_id = await enqueue(file_path)
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"task_id": task_id,
|
|
|
|
|
|
"filename": file.filename,
|
|
|
|
|
|
"status": "queued",
|
|
|
|
|
|
"message": "Файл добавлен в очередь обработки",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/upload-batch")
|
|
|
|
|
|
async def upload_batch(files: List[UploadFile] = File(...)):
|
|
|
|
|
|
"""Загружает несколько файлов пакетно."""
|
|
|
|
|
|
results = []
|
|
|
|
|
|
for file in files:
|
|
|
|
|
|
file_path = UPLOAD_DIR / file.filename
|
|
|
|
|
|
with open(file_path, "wb") as f:
|
|
|
|
|
|
content = await file.read()
|
|
|
|
|
|
f.write(content)
|
|
|
|
|
|
|
|
|
|
|
|
task_id = await enqueue(file_path)
|
|
|
|
|
|
results.append({
|
|
|
|
|
|
"task_id": task_id,
|
|
|
|
|
|
"filename": file.filename,
|
|
|
|
|
|
"status": "queued",
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"uploaded": len(results),
|
|
|
|
|
|
"tasks": results,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.websocket("/ws")
|
|
|
|
|
|
async def websocket_endpoint(websocket: WebSocket):
|
|
|
|
|
|
"""WebSocket для получения прогресса обработки."""
|
|
|
|
|
|
await manager.connect(websocket)
|
|
|
|
|
|
try:
|
|
|
|
|
|
while True:
|
|
|
|
|
|
# Ждём сообщения от клиента (ping/keepalive)
|
|
|
|
|
|
data = await websocket.receive_text()
|
|
|
|
|
|
msg = json.loads(data)
|
|
|
|
|
|
|
|
|
|
|
|
if msg.get("action") == "get_tasks":
|
|
|
|
|
|
tasks = get_all_tasks()
|
|
|
|
|
|
await websocket.send_json({
|
|
|
|
|
|
"type": "tasks_list",
|
|
|
|
|
|
"tasks": tasks,
|
|
|
|
|
|
})
|
|
|
|
|
|
elif msg.get("action") == "get_tree":
|
|
|
|
|
|
tree = get_processed_tree()
|
|
|
|
|
|
await websocket.send_json({
|
|
|
|
|
|
"type": "file_tree",
|
|
|
|
|
|
"tree": tree,
|
|
|
|
|
|
})
|
|
|
|
|
|
except WebSocketDisconnect:
|
|
|
|
|
|
manager.disconnect(websocket)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
manager.disconnect(websocket)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/api/tasks")
|
|
|
|
|
|
async def api_tasks():
|
|
|
|
|
|
"""Возвращает список всех задач."""
|
|
|
|
|
|
return {"tasks": get_all_tasks()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/api/tasks/{task_id}")
|
|
|
|
|
|
async def api_task(task_id: str):
|
|
|
|
|
|
"""Возвращает статус конкретной задачи."""
|
|
|
|
|
|
status = get_task_status(task_id)
|
|
|
|
|
|
if not status:
|
|
|
|
|
|
return {"error": "Task not found"}
|
|
|
|
|
|
return status
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/api/files")
|
|
|
|
|
|
async def api_files():
|
|
|
|
|
|
"""Возвращает дерево обработанных файлов."""
|
|
|
|
|
|
return {"tree": get_processed_tree()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/api/files/content")
|
|
|
|
|
|
async def api_file_content(path: str):
|
|
|
|
|
|
"""Возвращает содержимое файла."""
|
|
|
|
|
|
try:
|
|
|
|
|
|
content = read_file_content(path)
|
|
|
|
|
|
return {"content": content, "path": path}
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
return {"error": str(e)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/api/files/download")
|
|
|
|
|
|
async def api_download(path: str):
|
|
|
|
|
|
"""Скачивает файл."""
|
|
|
|
|
|
file_path = PROCESSED_DIR / path
|
|
|
|
|
|
if not file_path.exists():
|
|
|
|
|
|
return {"error": "File not found"}
|
|
|
|
|
|
return FileResponse(file_path, filename=file_path.name)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-29 09:52:51 +00:00
|
|
|
|
import shutil
|
|
|
|
|
|
|
|
|
|
|
|
@app.delete("/api/folders/{folder_name}")
|
|
|
|
|
|
async def api_delete_folder(folder_name: str):
|
|
|
|
|
|
"""Удаляет папку с обработанными файлами."""
|
|
|
|
|
|
folder_path = PROCESSED_DIR / folder_name
|
|
|
|
|
|
if not folder_path.exists():
|
|
|
|
|
|
return {"error": "Folder not found"}
|
|
|
|
|
|
try:
|
|
|
|
|
|
shutil.rmtree(folder_path)
|
|
|
|
|
|
return {"deleted": folder_name}
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
return {"error": str(e)}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-29 09:17:08 +00:00
|
|
|
|
# Статические файлы
|
|
|
|
|
|
app.mount("/static", StaticFiles(directory="backend/static"), name="static")
|