217 lines
6.3 KiB
Python
217 lines
6.3 KiB
Python
"""FastAPI backend для сервиса транскрибации."""
|
||
|
||
import json
|
||
from contextlib import asynccontextmanager
|
||
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,
|
||
start_workers,
|
||
stop_workers,
|
||
)
|
||
|
||
# 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)
|
||
|
||
|
||
@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=["*"],
|
||
)
|
||
|
||
|
||
# === 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)
|
||
|
||
|
||
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)}
|
||
|
||
|
||
# Статические файлы
|
||
app.mount("/static", StaticFiles(directory="backend/static"), name="static")
|