31 changed files with 1739 additions and 0 deletions
@ -0,0 +1,63 @@
|
||||
# Python |
||||
__pycache__/ |
||||
*.py[cod] |
||||
*$py.class |
||||
*.so |
||||
.Python |
||||
env/ |
||||
venv/ |
||||
ENV/ |
||||
build/ |
||||
develop-eggs/ |
||||
dist/ |
||||
downloads/ |
||||
eggs/ |
||||
.eggs/ |
||||
lib/ |
||||
lib64/ |
||||
parts/ |
||||
sdist/ |
||||
var/ |
||||
wheels/ |
||||
*.egg-info/ |
||||
.installed.cfg |
||||
*.egg |
||||
|
||||
# IDE |
||||
.vscode/ |
||||
.idea/ |
||||
*.swp |
||||
*.swo |
||||
*~ |
||||
|
||||
# OS |
||||
.DS_Store |
||||
Thumbs.db |
||||
|
||||
# Git |
||||
.git/ |
||||
.gitignore |
||||
|
||||
# Database |
||||
*.db |
||||
*.sqlite |
||||
*.sqlite3 |
||||
|
||||
# Uploads (в Docker создаются заново) |
||||
uploads/* |
||||
|
||||
# Docker |
||||
Dockerfile |
||||
.dockerignore |
||||
docker-compose.yml |
||||
|
||||
# Documentation |
||||
README*.md |
||||
*.md |
||||
|
||||
# Environment |
||||
.env |
||||
.env.local |
||||
|
||||
# Logs |
||||
*.log |
||||
@ -0,0 +1,17 @@
|
||||
# Database |
||||
# DATABASE_URL=sqlite:///./speech_analytics.db |
||||
# Для PostgreSQL: |
||||
# DATABASE_URL=postgresql://user:password@localhost/speech_analytics |
||||
|
||||
# API Settings |
||||
# API_V1_PREFIX=/api/v1 |
||||
# MAX_UPLOAD_SIZE=104857600 # 100MB in bytes |
||||
|
||||
# Application |
||||
# APP_TITLE=Speech Analytics API |
||||
# APP_VERSION=1.0.0 |
||||
|
||||
# Server |
||||
# HOST=0.0.0.0 |
||||
# PORT=8000 |
||||
# RELOAD=True |
||||
@ -0,0 +1,38 @@
|
||||
# Базовый образ с Python |
||||
FROM python:3.10-slim |
||||
|
||||
# Установка переменных окружения |
||||
ENV PYTHONDONTWRITEBYTECODE=1 \ |
||||
PYTHONUNBUFFERED=1 \ |
||||
PIP_NO_CACHE_DIR=1 \ |
||||
PIP_DISABLE_PIP_VERSION_CHECK=1 |
||||
|
||||
# Рабочая директория |
||||
WORKDIR /app |
||||
|
||||
# Установка системных зависимостей |
||||
RUN apt-get update && apt-get install -y --no-install-recommends \ |
||||
gcc \ |
||||
&& rm -rf /var/lib/apt/lists/* |
||||
|
||||
# Копирование requirements.txt |
||||
COPY requirements.txt . |
||||
|
||||
# Установка Python зависимостей |
||||
RUN pip install --upgrade pip && \ |
||||
pip install -r requirements.txt |
||||
|
||||
# Копирование приложения |
||||
COPY . . |
||||
|
||||
# Создание директории для загрузки файлов |
||||
RUN mkdir -p /app/uploads |
||||
|
||||
# Создание директории для базы данных (если используется SQLite) |
||||
RUN mkdir -p /app/data |
||||
|
||||
# Открытие порта |
||||
EXPOSE 8000 |
||||
|
||||
# Команда запуска приложения |
||||
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] |
||||
@ -0,0 +1,82 @@
|
||||
.PHONY: help build up down restart logs shell test clean install db-migrate |
||||
|
||||
# Default target
|
||||
help: |
||||
@echo "Available commands:"
|
||||
@echo " make install - Install Python dependencies"
|
||||
@echo " make build - Build Docker image"
|
||||
@echo " make up - Start Docker containers"
|
||||
@echo " make down - Stop Docker containers"
|
||||
@echo " make restart - Restart Docker containers"
|
||||
@echo " make logs - Show Docker logs"
|
||||
@echo " make shell - Open shell in container"
|
||||
@echo " make test - Run tests"
|
||||
@echo " make clean - Clean up containers and volumes"
|
||||
@echo " make db-migrate - Create database tables"
|
||||
|
||||
# Install dependencies
|
||||
install: |
||||
@echo "Installing dependencies..."
|
||||
pip3 install -r requirements.txt
|
||||
|
||||
# Build Docker image
|
||||
build: |
||||
@echo "Building Docker image..."
|
||||
docker-compose build
|
||||
|
||||
# Start containers
|
||||
up: |
||||
@echo "Starting containers..."
|
||||
docker-compose up -d
|
||||
@echo "✅ Application started at http://localhost:8000"
|
||||
@echo "📚 API Documentation: http://localhost:8000/api/v1/docs"
|
||||
|
||||
# Stop containers
|
||||
down: |
||||
@echo "Stopping containers..."
|
||||
docker-compose down
|
||||
|
||||
# Restart containers
|
||||
restart: down up |
||||
|
||||
# Show logs
|
||||
logs: |
||||
docker-compose logs -f app
|
||||
|
||||
# Open shell in container
|
||||
shell: |
||||
docker-compose exec app bash
|
||||
|
||||
# Run tests
|
||||
test: |
||||
@echo "Running tests..."
|
||||
python3 -m pytest tests/ -v
|
||||
|
||||
# Clean up
|
||||
clean: |
||||
@echo "Cleaning up..."
|
||||
docker-compose down -v
|
||||
rm -rf data/*.db
|
||||
rm -rf uploads/*
|
||||
|
||||
# Create database tables
|
||||
db-migrate: |
||||
@echo "Creating database tables..."
|
||||
python3 -c "from apiApp.database import Base, engine; Base.metadata.create_all(bind=engine); print('✅ Database tables created')"
|
||||
|
||||
# Development run
|
||||
dev: |
||||
@echo "Starting development server..."
|
||||
python3 run.py
|
||||
|
||||
# Format code
|
||||
format: |
||||
@echo "Formatting code..."
|
||||
black apiApp/
|
||||
isort apiApp/
|
||||
|
||||
# Lint code
|
||||
lint: |
||||
@echo "Linting code..."
|
||||
flake8 apiApp/
|
||||
mypy apiApp/
|
||||
@ -0,0 +1,221 @@
|
||||
# Docker Deployment Guide |
||||
|
||||
## Быстрый старт |
||||
|
||||
### Сборка и запуск с Docker Compose (рекомендуется) |
||||
|
||||
```bash |
||||
# Сборка и запуск |
||||
docker-compose up -d |
||||
|
||||
# Просмотр логов |
||||
docker-compose logs -f |
||||
|
||||
# Остановка |
||||
docker-compose down |
||||
``` |
||||
|
||||
Приложение будет доступно по адресу: http://localhost:8000 |
||||
|
||||
API документация: |
||||
- Swagger UI: http://localhost:8000/api/v1/docs |
||||
- ReDoc: http://localhost:8000/api/v1/redoc |
||||
|
||||
### Ручной запуск с Docker |
||||
|
||||
```bash |
||||
# Сборка образа |
||||
docker build -t file-audio-api . |
||||
|
||||
# Запуск контейнера |
||||
docker run -d \ |
||||
--name file-audio-api \ |
||||
-p 8000:8000 \ |
||||
-v $(pwd)/uploads:/app/uploads \ |
||||
-v $(pwd)/data:/app/data \ |
||||
-e DATABASE_URL=sqlite:////app/data/speech_analytics.db \ |
||||
file-audio-api |
||||
|
||||
# Просмотр логов |
||||
docker logs -f file-audio-api |
||||
|
||||
# Остановка и удаление контейнера |
||||
docker stop file-audio-api |
||||
docker rm file-audio-api |
||||
``` |
||||
|
||||
## Управление данными |
||||
|
||||
### Загруженные файлы |
||||
Файлы сохраняются в директории `./uploads` на хост-машине (монтируются в контейнер). |
||||
|
||||
### База данных |
||||
SQLite база данных находится в `./data/speech_analytics.db`. |
||||
|
||||
### Бэкап данных |
||||
|
||||
```bash |
||||
# Бэкап базы данных |
||||
docker exec file-audio-api cp /app/data/speech_analytics.db /app/backup_$(date +%Y%m%d).db |
||||
|
||||
# Бэкап загруженных файлов |
||||
tar -czf uploads_backup_$(date +%Y%m%d).tar.gz uploads/ |
||||
``` |
||||
|
||||
## Production конфигурация |
||||
|
||||
### Использование PostgreSQL |
||||
|
||||
Раскомментируйте секцию `db` в `docker-compose.yml` и измените `DATABASE_URL`: |
||||
|
||||
```yaml |
||||
environment: |
||||
- DATABASE_URL=postgresql://speechuser:speechpass@db:5432/speech_analytics |
||||
``` |
||||
|
||||
### Использование Redis для статусов задач |
||||
|
||||
Раскомментируйте секцию `redis` в `docker-compose.yml` и подключите в коде: |
||||
|
||||
```python |
||||
# apiApp/config.py |
||||
import redis |
||||
redis_client = redis.from_url("redis://redis:6379") |
||||
``` |
||||
|
||||
### Environment переменные |
||||
|
||||
Создайте `.env` файл: |
||||
|
||||
```bash |
||||
cp .env.example .env |
||||
``` |
||||
|
||||
И отредактируйте: |
||||
|
||||
```env |
||||
DATABASE_URL=postgresql://user:password@db:5432/speech_analytics |
||||
MAX_UPLOAD_SIZE=104857600 |
||||
``` |
||||
|
||||
## Health Check |
||||
|
||||
```bash |
||||
curl http://localhost:8000/health |
||||
``` |
||||
|
||||
Ожидаемый ответ: |
||||
```json |
||||
{"status": "healthy"} |
||||
``` |
||||
|
||||
## Логи и отладка |
||||
|
||||
```bash |
||||
# Просмотр логов |
||||
docker-compose logs -f app |
||||
|
||||
# Вход в контейнер для отладки |
||||
docker-compose exec app bash |
||||
|
||||
# Перезапуск с пересборкой |
||||
docker-compose up -d --build |
||||
``` |
||||
|
||||
## Производительность |
||||
|
||||
### Настройка количества workers |
||||
|
||||
Для production рекомендуется использовать несколько workers: |
||||
|
||||
```bash |
||||
docker run -d \ |
||||
--name file-audio-api \ |
||||
-p 8000:8000 \ |
||||
file-audio-api \ |
||||
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4 |
||||
``` |
||||
|
||||
Или в `docker-compose.yml`: |
||||
|
||||
```yaml |
||||
command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"] |
||||
``` |
||||
|
||||
## Безопасность |
||||
|
||||
### Ограничение CORS |
||||
|
||||
Отредактируйте `main.py`: |
||||
|
||||
```python |
||||
app.add_middleware( |
||||
CORSMiddleware, |
||||
allow_origins=["https://yourdomain.com"], # Замените на ваш домен |
||||
allow_credentials=True, |
||||
allow_methods=["*"], |
||||
allow_headers=["*"], |
||||
) |
||||
``` |
||||
|
||||
### Добавление SSL |
||||
|
||||
Используйте reverse proxy (nginx) или: |
||||
|
||||
```bash |
||||
docker run -d \ |
||||
--name file-audio-api \ |
||||
-p 8443:8443 \ |
||||
-v /path/to/certs:/app/certs \ |
||||
file-audio-api \ |
||||
uvicorn main:app --host 0.0.0.0 --port 8443 --ssl-keyfile /app/certs/key.pem --ssl-certfile /app/certs/cert.pem |
||||
``` |
||||
|
||||
## Troubleshooting |
||||
|
||||
### Контейнер не запускается |
||||
|
||||
```bash |
||||
# Проверка логов |
||||
docker-compose logs app |
||||
|
||||
# Проверка статуса |
||||
docker-compose ps |
||||
``` |
||||
|
||||
### Ошибки с базой данных |
||||
|
||||
```bash |
||||
# Перезапуск с очисткой volumes |
||||
docker-compose down -v |
||||
docker-compose up -d |
||||
``` |
||||
|
||||
### Проблемы с правами доступа |
||||
|
||||
```bash |
||||
# Установка прав на директорию uploads |
||||
chmod -R 755 uploads/ |
||||
``` |
||||
|
||||
## Мониторинг |
||||
|
||||
### Использование Docker stats |
||||
|
||||
```bash |
||||
docker stats file-audio-api |
||||
``` |
||||
|
||||
### Интеграция с Prometheus |
||||
|
||||
Добавьте в `docker-compose.yml`: |
||||
|
||||
```yaml |
||||
services: |
||||
prometheus: |
||||
image: prom/prometheus |
||||
volumes: |
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml |
||||
ports: |
||||
- "9090:9090" |
||||
``` |
||||
@ -0,0 +1,138 @@
|
||||
# Speech Analytics FastAPI |
||||
|
||||
Сервис для хранения аудиофайлов, индексации файлов, записи и выдачи результатов распознавания, реализованный на FastAPI. |
||||
|
||||
## Установка зависимостей |
||||
|
||||
```bash |
||||
pip install -r requirements.txt |
||||
``` |
||||
|
||||
## Запуск приложения |
||||
|
||||
### Разработка с автоперезагрузкой |
||||
```bash |
||||
python run.py |
||||
``` |
||||
|
||||
### Production режим |
||||
```bash |
||||
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4 |
||||
``` |
||||
|
||||
## API Эндпоинты |
||||
|
||||
Документация доступна по адресу: |
||||
- Swagger UI: http://localhost:8000/api/v1/docs |
||||
- ReDoc: http://localhost:8000/api/v1/redoc |
||||
|
||||
### Аудио файлы |
||||
|
||||
#### Загрузка аудиофайла |
||||
```http |
||||
POST /api/v1/upload |
||||
Content-Type: multipart/form-data |
||||
|
||||
file: <audio_file> |
||||
``` |
||||
|
||||
#### Список всех аудиофайлов |
||||
```http |
||||
GET /api/v1/audio/list?skip=0&limit=100 |
||||
``` |
||||
|
||||
#### Получить информацию об аудиофайле |
||||
```http |
||||
GET /api/v1/audio/{audio_id} |
||||
``` |
||||
|
||||
#### Скачать аудиофайл |
||||
```http |
||||
GET /api/v1/audio/file/{audio_id} |
||||
``` |
||||
|
||||
#### Удалить аудиофайл |
||||
```http |
||||
DELETE /api/v1/audio/delete/{audio_id} |
||||
``` |
||||
|
||||
### Распознавание |
||||
|
||||
#### Запустить распознавание |
||||
```http |
||||
POST /api/v1/recognize/{audio_id} |
||||
``` |
||||
|
||||
Возвращает: |
||||
- 202 Accepted - распознавание запущено |
||||
- 200 OK - уже есть активная задача |
||||
- 404 Not Found - аудио не найдено |
||||
|
||||
#### Получить статус распознавания |
||||
```http |
||||
GET /api/v1/recognize/{audio_id} |
||||
``` |
||||
|
||||
Статусы: |
||||
- `not_started` - распознавание не запускалось |
||||
- `processing` - в процессе |
||||
- `completed` - завершено |
||||
- `error` - ошибка |
||||
|
||||
#### Получить статус по task_id |
||||
```http |
||||
GET /api/v1/recognize/task/{task_id} |
||||
``` |
||||
|
||||
#### Получить результат распознавания |
||||
```http |
||||
GET /api/v1/recognize/{audio_id}/result |
||||
``` |
||||
|
||||
## Структура проекта |
||||
|
||||
``` |
||||
FileAudioAPI/ |
||||
├── main.py # Точка входа FastAPI приложения |
||||
├── run.py # Скрипт запуска |
||||
├── requirements.txt # Зависимости |
||||
├── apiApp/ |
||||
│ ├── __init__.py |
||||
│ ├── config.py # Конфигурация |
||||
│ ├── database.py # SQLAlchemy настройка |
||||
│ ├── schemas.py # Pydantic модели |
||||
│ ├── crud.py # CRUD операции |
||||
│ ├── routers/ # API routers |
||||
│ │ ├── __init__.py |
||||
│ │ ├── audio.py # Эндпоинты для аудио |
||||
│ │ └── recognition.py # Эндпоинты для распознавания |
||||
│ ├── database/ # SQLAlchemy модели |
||||
│ │ ├── Audio.py |
||||
│ │ ├── AiConclusion.py |
||||
│ │ ├── Operator.py |
||||
│ │ └── ConclusionVersion.py |
||||
│ └── dbApi/ # Legacy совместимость |
||||
│ └── __init__.py |
||||
└── uploads/ # Загруженные файлы |
||||
``` |
||||
|
||||
## Конфигурация |
||||
|
||||
Переменные окружения: |
||||
- `DATABASE_URL` - URL базы данных (по умолчанию: sqlite:///./speech_analytics.db) |
||||
|
||||
## Основные изменения от Flask версии |
||||
|
||||
1. **FastAPI вместо Flask**: Использование современного асинхронного фреймворка |
||||
2. **Pydantic схемы**: Валидация данных с помощью Pydantic |
||||
3. **Асинхронная обработка**: BackgroundTasks для фоновых задач |
||||
4. **Dependency Injection**: Внедрение зависимостей для сессий БД |
||||
5. **Автодокументация**: Автоматическая генерация Swagger/ReDoc |
||||
6. **Type hints**: Полная поддержка типизации |
||||
|
||||
## Примечания |
||||
|
||||
- Для production рекомендуется использовать PostgreSQL вместо SQLite |
||||
- Для хранения статусов задач в production рекомендуется использовать Redis |
||||
- Фоновые задачи обрабатываются асинхронно с помощью BackgroundTasks |
||||
- Поддерживается загрузка файлов до 100MB |
||||
@ -0,0 +1,4 @@
|
||||
# apiApp package |
||||
# FastAPI application for Speech Analytics |
||||
|
||||
__version__ = "1.0.0" |
||||
@ -0,0 +1,19 @@
|
||||
import os |
||||
from pathlib import Path |
||||
|
||||
# Базовые пути |
||||
BASE_DIR = Path(__file__).resolve().parent.parent |
||||
UPLOAD_FOLDER = BASE_DIR / "uploads" |
||||
UPLOAD_FOLDER.mkdir(exist_ok=True) |
||||
|
||||
# Database |
||||
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./speech_analytics.db") |
||||
|
||||
# API Settings |
||||
API_V1_PREFIX = "/api/v1" |
||||
MAX_UPLOAD_SIZE = 100 * 1024 * 1024 # 100MB |
||||
ALLOWED_AUDIO_EXTENSIONS = {".mp3", ".wav", ".ogg", ".flac", ".m4a", ".aac"} |
||||
|
||||
# Application |
||||
APP_TITLE = "Speech Analytics API" |
||||
APP_VERSION = "1.0.0" |
||||
@ -0,0 +1,25 @@
|
||||
from sqlalchemy import create_engine |
||||
from sqlalchemy.ext.declarative import declarative_base |
||||
from sqlalchemy.orm import sessionmaker |
||||
from apiApp.config import DATABASE_URL |
||||
|
||||
# Создание engine |
||||
engine = create_engine( |
||||
DATABASE_URL, |
||||
connect_args={"check_same_thread": False} if "sqlite" in DATABASE_URL else {} |
||||
) |
||||
|
||||
# SessionLocal |
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) |
||||
|
||||
# Base |
||||
Base = declarative_base() |
||||
|
||||
|
||||
# Зависимость для получения сессии БД |
||||
def get_db(): |
||||
db = SessionLocal() |
||||
try: |
||||
yield db |
||||
finally: |
||||
db.close() |
||||
@ -0,0 +1,21 @@
|
||||
from sqlalchemy import Column, UUID, ForeignKey, DateTime, JSON |
||||
from sqlalchemy.orm import relationship |
||||
from apiApp.database import Base |
||||
import uuid |
||||
from datetime import datetime |
||||
|
||||
|
||||
class AiConclusion(Base): |
||||
__tablename__ = "ai_conclusion" |
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) |
||||
audio_id = Column(UUID(as_uuid=True), ForeignKey("audio.id"), nullable=False) |
||||
conclusion = Column(JSON, default=lambda: { |
||||
"transcription": [], |
||||
"ai_transcription": [], |
||||
"conclusion": {} |
||||
}) |
||||
index_date = Column(DateTime, default=datetime.utcnow) |
||||
end_date = Column(DateTime) |
||||
|
||||
audio = relationship("Audio", back_populates="ai_conclusion") |
||||
@ -0,0 +1,28 @@
|
||||
from sqlalchemy import Column, String, DateTime, UUID, ForeignKey, Float, Integer |
||||
from sqlalchemy.orm import relationship |
||||
from apiApp.database import Base |
||||
import uuid |
||||
from datetime import datetime |
||||
|
||||
|
||||
class Audio(Base): |
||||
__tablename__ = "audio" |
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) |
||||
filename = Column(String(255), nullable=False) |
||||
index_date = Column(DateTime, default=datetime.utcnow) |
||||
file_path = Column(String(500)) |
||||
duration = Column(Float) |
||||
file_size = Column(Integer) |
||||
|
||||
ai_conclusion = relationship("AiConclusion", back_populates="audio", cascade="all, delete-orphan") |
||||
|
||||
def to_dict(self): |
||||
return { |
||||
"id": str(self.id), |
||||
"filename": self.filename, |
||||
"index_date": self.index_date.isoformat() if self.index_date else None, |
||||
"file_path": self.file_path, |
||||
"duration": self.duration, |
||||
"file_size": self.file_size |
||||
} |
||||
@ -0,0 +1,12 @@
|
||||
from sqlalchemy import Column, UUID, ForeignKey, Integer, Text |
||||
from apiApp.database import Base |
||||
import uuid |
||||
|
||||
|
||||
class ConclusionVersion(Base): |
||||
__tablename__ = "conclusion_version" |
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) |
||||
conclusion_id = Column(UUID(as_uuid=True), ForeignKey("conclusion.id")) |
||||
version = Column(Integer) |
||||
content = Column(Text) |
||||
@ -0,0 +1,16 @@
|
||||
from sqlalchemy import Column, UUID, String, Integer |
||||
from sqlalchemy.orm import relationship |
||||
from apiApp.database import Base |
||||
import uuid |
||||
|
||||
|
||||
class Operator(Base): |
||||
__tablename__ = "operator" |
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) |
||||
fio = Column(String(100)) |
||||
num = Column(Integer) |
||||
|
||||
calls = relationship("Call", back_populates="operator") |
||||
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
from sqlalchemy import create_engine |
||||
from sqlalchemy.ext.declarative import declarative_base |
||||
from sqlalchemy.orm import sessionmaker |
||||
from apiApp.config import DATABASE_URL |
||||
|
||||
# Создание engine |
||||
engine = create_engine( |
||||
DATABASE_URL, |
||||
connect_args={"check_same_thread": False} if "sqlite" in DATABASE_URL else {} |
||||
) |
||||
|
||||
# SessionLocal |
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) |
||||
|
||||
# Base |
||||
Base = declarative_base() |
||||
|
||||
|
||||
# Зависимость для получения сессии БД |
||||
def get_db(): |
||||
db = SessionLocal() |
||||
try: |
||||
yield db |
||||
finally: |
||||
db.close() |
||||
|
||||
|
||||
from apiApp.database.Operator import Operator |
||||
from apiApp.database.Audio import Audio |
||||
from apiApp.database.AiConclusion import AiConclusion |
||||
from apiApp.database.ConclusionVersion import ConclusionVersion |
||||
@ -0,0 +1,65 @@
|
||||
# Legacy database API support |
||||
# Этот модуль обеспечивает обратную совместимость со старым кодом |
||||
|
||||
from apiApp.crud import AudioCRUD, AiConclusionCRUD |
||||
|
||||
class AudioDB: |
||||
""" |
||||
Совместимый слой для старого API |
||||
""" |
||||
|
||||
@staticmethod |
||||
def list(): |
||||
from apiApp.database import SessionLocal |
||||
db = SessionLocal() |
||||
try: |
||||
return AudioCRUD.get_all(db) |
||||
finally: |
||||
db.close() |
||||
|
||||
@staticmethod |
||||
def get(audio_id): |
||||
from apiApp.database import SessionLocal |
||||
db = SessionLocal() |
||||
try: |
||||
return AudioCRUD.get_by_id(db, audio_id) |
||||
finally: |
||||
db.close() |
||||
|
||||
@staticmethod |
||||
def add(data): |
||||
from apiApp.database import SessionLocal |
||||
from apiApp.schemas import AudioCreate |
||||
db = SessionLocal() |
||||
try: |
||||
audio_data = AudioCreate(**data) |
||||
return AudioCRUD.create(db, audio_data, file_path=data.get('file_path', '')) |
||||
finally: |
||||
db.close() |
||||
|
||||
@staticmethod |
||||
def put(audio_id, data): |
||||
from apiApp.database import SessionLocal |
||||
db = SessionLocal() |
||||
try: |
||||
return AudioCRUD.update(db, audio_id, data) |
||||
finally: |
||||
db.close() |
||||
|
||||
@staticmethod |
||||
def delete(audio_id): |
||||
from apiApp.database import SessionLocal |
||||
db = SessionLocal() |
||||
try: |
||||
return AudioCRUD.delete(db, audio_id) |
||||
finally: |
||||
db.close() |
||||
|
||||
@staticmethod |
||||
def update_recognition_result(audio_id, result): |
||||
from apiApp.database import SessionLocal |
||||
db = SessionLocal() |
||||
try: |
||||
return AudioCRUD.update_recognition_result(db, audio_id, result) |
||||
finally: |
||||
db.close() |
||||
@ -0,0 +1,38 @@
|
||||
|
||||
from apiApp.database.Audio import Audio |
||||
from apiApp.database import db |
||||
import datetime |
||||
|
||||
class AudioDB: |
||||
|
||||
@staticmethod |
||||
def list(): |
||||
return Audio.query.all() |
||||
|
||||
@staticmethod |
||||
def get(audio_id): |
||||
return Audio.query.get(audio_id) |
||||
|
||||
@staticmethod |
||||
def add(data): |
||||
data['datetime'] = datetime.datetime.now() |
||||
audio = Audio(**data) |
||||
db.session.add(audio) |
||||
db.session.commit() |
||||
return audio |
||||
|
||||
@staticmethod |
||||
def put(audio_id, data): |
||||
audio = Audio.query.get(audio_id) |
||||
for key, value in data.items(): |
||||
setattr(audio, key, value) |
||||
db.session.commit() |
||||
return audio |
||||
|
||||
@staticmethod |
||||
def delete(audio_id): |
||||
audio = Audio.query.get(audio_id) |
||||
db.session.delete(audio) |
||||
db.session.commit() |
||||
return audio |
||||
|
||||
@ -0,0 +1,4 @@
|
||||
from apiApp.routers.audio import router as audio_router |
||||
from apiApp.routers.recognition import router as recognition_router |
||||
|
||||
__all__ = ["audio_router", "recognition_router"] |
||||
@ -0,0 +1,155 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File as FastAPIFile, status |
||||
from fastapi.responses import FileResponse |
||||
from sqlalchemy.orm import Session |
||||
from typing import List |
||||
import os |
||||
import uuid |
||||
import aiofiles |
||||
|
||||
from apiApp.database import get_db |
||||
from apiApp.schemas import ( |
||||
AudioCreate, |
||||
AudioResponse, |
||||
AudioListResponse, |
||||
MessageResponse |
||||
) |
||||
from apiApp.services import AudioCRUD |
||||
from apiApp.config import UPLOAD_FOLDER, ALLOWED_AUDIO_EXTENSIONS, MAX_UPLOAD_SIZE |
||||
|
||||
router = APIRouter() |
||||
|
||||
|
||||
@router.post("/upload", response_model=AudioResponse, status_code=status.HTTP_201_CREATED) |
||||
async def upload_audio_file( |
||||
file: UploadFile = FastAPIFile(...), |
||||
db: Session = Depends(get_db) |
||||
): |
||||
""" |
||||
Загрузка аудиофайла на сервер |
||||
""" |
||||
# Проверка расширения файла |
||||
file_ext = os.path.splitext(file.filename)[1].lower() |
||||
if file_ext not in ALLOWED_AUDIO_EXTENSIONS: |
||||
raise HTTPException( |
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, |
||||
detail=f"File extension not allowed. Allowed: {', '.join(ALLOWED_AUDIO_EXTENSIONS)}" |
||||
) |
||||
|
||||
# Чтение содержимого файла |
||||
content = await file.read() |
||||
|
||||
# Проверка размера файла |
||||
if len(content) > MAX_UPLOAD_SIZE: |
||||
raise HTTPException( |
||||
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, |
||||
detail=f"File too large. Maximum size: {MAX_UPLOAD_SIZE / (1024*1024)}MB" |
||||
) |
||||
|
||||
# Сохранение файла |
||||
file_path = UPLOAD_FOLDER / f"{uuid.uuid4()}{file_ext}" |
||||
|
||||
try: |
||||
async with aiofiles.open(file_path, 'wb') as f: |
||||
await f.write(content) |
||||
except Exception as e: |
||||
raise HTTPException( |
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
||||
detail=f"Error saving file: {str(e)}" |
||||
) |
||||
|
||||
# Создание записи в БД |
||||
try: |
||||
audio_data = AudioCreate(filename=file.filename) |
||||
audio = AudioCRUD.create( |
||||
db=db, |
||||
audio_data=audio_data, |
||||
file_path=str(file_path), |
||||
file_size=len(content) |
||||
) |
||||
return audio |
||||
except Exception as e: |
||||
# Удаление файла при ошибке записи в БД |
||||
if os.path.exists(file_path): |
||||
os.remove(file_path) |
||||
raise HTTPException( |
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
||||
detail=f"Error creating database record: {str(e)}" |
||||
) |
||||
|
||||
|
||||
@router.get("/audio/list", response_model=AudioListResponse) |
||||
async def list_audio_files( |
||||
skip: int = 0, |
||||
limit: int = 100, |
||||
db: Session = Depends(get_db) |
||||
): |
||||
""" |
||||
Получить список всех аудиофайлов |
||||
""" |
||||
audios = AudioCRUD.get_all(db) |
||||
return AudioListResponse( |
||||
audios=audios[skip:skip+limit], |
||||
count=len(audios) |
||||
) |
||||
|
||||
|
||||
@router.get("/audio/{audio_id}", response_model=AudioResponse) |
||||
async def get_audio( |
||||
audio_id: uuid.UUID, |
||||
db: Session = Depends(get_db) |
||||
): |
||||
""" |
||||
Получить информацию о аудиофайле по ID |
||||
""" |
||||
audio = AudioCRUD.get_by_id(db, audio_id) |
||||
if not audio: |
||||
raise HTTPException( |
||||
status_code=status.HTTP_404_NOT_FOUND, |
||||
detail="Audio not found" |
||||
) |
||||
return audio |
||||
|
||||
|
||||
@router.get("/audio/file/{audio_id}") |
||||
async def download_audio_file( |
||||
audio_id: uuid.UUID, |
||||
db: Session = Depends(get_db) |
||||
): |
||||
""" |
||||
Скачать аудиофайл по ID |
||||
""" |
||||
audio = AudioCRUD.get_by_id(db, audio_id) |
||||
if not audio: |
||||
raise HTTPException( |
||||
status_code=status.HTTP_404_NOT_FOUND, |
||||
detail="Audio not found" |
||||
) |
||||
|
||||
if not audio.file_path or not os.path.exists(audio.file_path): |
||||
raise HTTPException( |
||||
status_code=status.HTTP_404_NOT_FOUND, |
||||
detail="Audio file not found on disk" |
||||
) |
||||
|
||||
return FileResponse( |
||||
path=audio.file_path, |
||||
filename=audio.filename, |
||||
media_type='audio/mpeg' |
||||
) |
||||
|
||||
|
||||
@router.delete("/audio/delete/{audio_id}", response_model=AudioResponse) |
||||
async def delete_audio( |
||||
audio_id: uuid.UUID, |
||||
db: Session = Depends(get_db) |
||||
): |
||||
""" |
||||
Удалить аудиофайл |
||||
""" |
||||
audio = AudioCRUD.delete(db, audio_id) |
||||
if not audio: |
||||
raise HTTPException( |
||||
status_code=status.HTTP_404_NOT_FOUND, |
||||
detail="Audio not found" |
||||
) |
||||
return audio |
||||
@ -0,0 +1,264 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks, status |
||||
from sqlalchemy.orm import Session |
||||
from typing import Dict, Any |
||||
import uuid |
||||
import os |
||||
import asyncio |
||||
|
||||
from apiApp.database import get_db, Audio |
||||
from apiApp.schemas import ( |
||||
RecognitionStartResponse, |
||||
RecognitionStatus, |
||||
ErrorResponse |
||||
) |
||||
from apiApp.services import AudioCRUD, AiConclusionCRUD |
||||
from apiApp.config import UPLOAD_FOLDER |
||||
|
||||
router = APIRouter() |
||||
|
||||
# Глобальное хранилище статусов задач (в продакшене лучше использовать Redis) |
||||
recognition_tasks: Dict[str, Dict[str, Any]] = {} |
||||
|
||||
|
||||
async def process_recognition(audio_id: uuid.UUID, file_path: str, task_id: str): |
||||
""" |
||||
Фоновая задача для распознавания аудио |
||||
""" |
||||
try: |
||||
# Обновляем статус на processing |
||||
recognition_tasks[task_id] = { |
||||
'audio_id': audio_id, |
||||
'status': 'processing', |
||||
'result': None, |
||||
'error': None |
||||
} |
||||
|
||||
# Проверяем существование файла |
||||
if not os.path.exists(file_path): |
||||
recognition_tasks[task_id]['status'] = 'error' |
||||
recognition_tasks[task_id]['error'] = 'File not found on disk' |
||||
return |
||||
|
||||
# Здесь должна быть реальная логика распознавания |
||||
# Например, вызов внешнего API или локальной модели |
||||
# result = await your_recognize_function(file_path) |
||||
|
||||
# Симуляция обработки (в реальном коде убрать) |
||||
await asyncio.sleep(2) |
||||
|
||||
# Пример результата (заменить на реальный) |
||||
result = { |
||||
'text': 'Распознанный текст из аудио', |
||||
'confidence': 0.95, |
||||
'duration': 120.5, |
||||
'segments': [ |
||||
{ |
||||
'start': 0.0, |
||||
'end': 5.2, |
||||
'text': 'Привет, чем могу помочь?', |
||||
'speaker': 'SPEAKER_00' |
||||
}, |
||||
{ |
||||
'start': 5.5, |
||||
'end': 10.8, |
||||
'text': 'Мне нужна информация о услугах', |
||||
'speaker': 'SPEAKER_01' |
||||
} |
||||
] |
||||
} |
||||
|
||||
# Обновляем статус на completed |
||||
recognition_tasks[task_id]['status'] = 'completed' |
||||
recognition_tasks[task_id]['result'] = result |
||||
|
||||
# Получаем сессию БД (для фоновой задачи) |
||||
from apiApp.database import SessionLocal |
||||
db = SessionLocal() |
||||
|
||||
try: |
||||
# Создаем или обновляем AI Conclusion |
||||
conclusion = AiConclusionCRUD.get_by_audio_id(db, audio_id) |
||||
if conclusion: |
||||
AiConclusionCRUD.update( |
||||
db=db, |
||||
conclusion_id=conclusion.id, |
||||
conclusion_data={ |
||||
"transcription": result.get('segments', []), |
||||
"ai_transcription": [result.get('text', '')], |
||||
"conclusion": { |
||||
"confidence": result.get('confidence', 0.0), |
||||
"duration": result.get('duration', 0.0) |
||||
} |
||||
}, |
||||
end_date=True |
||||
) |
||||
else: |
||||
AiConclusionCRUD.create( |
||||
db=db, |
||||
audio_id=audio_id, |
||||
conclusion={ |
||||
"transcription": result.get('segments', []), |
||||
"ai_transcription": [result.get('text', '')], |
||||
"conclusion": { |
||||
"confidence": result.get('confidence', 0.0), |
||||
"duration": result.get('duration', 0.0) |
||||
} |
||||
} |
||||
) |
||||
|
||||
# Обновляем запись аудио |
||||
AudioCRUD.update_recognition_result(db, audio_id, result) |
||||
|
||||
finally: |
||||
db.close() |
||||
|
||||
except Exception as e: |
||||
recognition_tasks[task_id]['status'] = 'error' |
||||
recognition_tasks[task_id]['error'] = str(e) |
||||
|
||||
|
||||
@router.post("/recognize/{audio_id}", response_model=RecognitionStartResponse, status_code=status.HTTP_202_ACCEPTED) |
||||
async def start_recognition( |
||||
audio_id: uuid.UUID, |
||||
background_tasks: BackgroundTasks, |
||||
db: Session = Depends(get_db) |
||||
): |
||||
""" |
||||
Запуск распознавания аудиофайла |
||||
""" |
||||
# Проверяем существование аудио |
||||
audio = AudioCRUD.get_by_id(db, audio_id) |
||||
if not audio: |
||||
raise HTTPException( |
||||
status_code=status.HTTP_404_NOT_FOUND, |
||||
detail="Audio not found in database" |
||||
) |
||||
|
||||
# Проверяем, нет ли уже активной задачи |
||||
for task_id, task in recognition_tasks.items(): |
||||
if task['audio_id'] == audio_id and task['status'] == 'processing': |
||||
return RecognitionStartResponse( |
||||
status="info", |
||||
message="Recognition already in progress", |
||||
task_id=task_id, |
||||
audio_id=audio_id |
||||
) |
||||
|
||||
# Проверяем наличие файла |
||||
if not audio.file_path or not os.path.exists(audio.file_path): |
||||
raise HTTPException( |
||||
status_code=status.HTTP_404_NOT_FOUND, |
||||
detail="Audio file not found on disk" |
||||
) |
||||
|
||||
# Создаем task_id |
||||
task_id = str(uuid.uuid4()) |
||||
|
||||
# Добавляем фоновую задачу |
||||
background_tasks.add_task(process_recognition, audio_id, audio.file_path, task_id) |
||||
|
||||
return RecognitionStartResponse( |
||||
status="success", |
||||
message="Recognition started", |
||||
task_id=task_id, |
||||
audio_id=audio_id |
||||
) |
||||
|
||||
|
||||
@router.get("/recognize/{audio_id}", response_model=RecognitionStatus) |
||||
async def get_recognition_status( |
||||
audio_id: uuid.UUID, |
||||
db: Session = Depends(get_db) |
||||
): |
||||
""" |
||||
Получение статуса распознавания по audio_id |
||||
""" |
||||
# Проверяем существование аудио |
||||
audio = AudioCRUD.get_by_id(db, audio_id) |
||||
if not audio: |
||||
raise HTTPException( |
||||
status_code=status.HTTP_404_NOT_FOUND, |
||||
detail="Audio not found" |
||||
) |
||||
|
||||
# Ищем задачу для данного audio_id |
||||
task_info = None |
||||
for task_id, task in recognition_tasks.items(): |
||||
if task['audio_id'] == audio_id: |
||||
task_info = { |
||||
'task_id': task_id, |
||||
**task |
||||
} |
||||
break |
||||
|
||||
if not task_info: |
||||
# Проверяем, есть ли сохраненный результат |
||||
conclusion = AiConclusionCRUD.get_by_audio_id(db, audio_id) |
||||
if conclusion and conclusion.end_date: |
||||
return RecognitionStatus( |
||||
task_id="", |
||||
audio_id=audio_id, |
||||
status="completed", |
||||
result=conclusion.conclusion |
||||
) |
||||
|
||||
return RecognitionStatus( |
||||
task_id="", |
||||
audio_id=audio_id, |
||||
status="not_started" |
||||
) |
||||
|
||||
return RecognitionStatus(**task_info) |
||||
|
||||
|
||||
@router.get("/recognize/task/{task_id}", response_model=RecognitionStatus) |
||||
async def get_recognition_task(task_id: str): |
||||
""" |
||||
Получение статуса задачи по task_id |
||||
""" |
||||
if task_id not in recognition_tasks: |
||||
raise HTTPException( |
||||
status_code=status.HTTP_404_NOT_FOUND, |
||||
detail="Task not found" |
||||
) |
||||
|
||||
task = recognition_tasks[task_id] |
||||
return RecognitionStatus( |
||||
task_id=task_id, |
||||
audio_id=task['audio_id'], |
||||
status=task['status'], |
||||
result=task.get('result'), |
||||
error=task.get('error') |
||||
) |
||||
|
||||
|
||||
@router.get("/recognize/{audio_id}/result") |
||||
async def get_recognition_result( |
||||
audio_id: uuid.UUID, |
||||
db: Session = Depends(get_db) |
||||
): |
||||
""" |
||||
Получение результата распознавания из базы данных |
||||
""" |
||||
audio = AudioCRUD.get_by_id(db, audio_id) |
||||
if not audio: |
||||
raise HTTPException( |
||||
status_code=status.HTTP_404_NOT_FOUND, |
||||
detail="Audio not found" |
||||
) |
||||
|
||||
conclusion = AiConclusionCRUD.get_by_audio_id(db, audio_id) |
||||
if not conclusion or not conclusion.end_date: |
||||
return { |
||||
"status": "not_available", |
||||
"message": "Recognition result not available yet", |
||||
"audio_id": str(audio_id) |
||||
} |
||||
|
||||
return { |
||||
"status": "success", |
||||
"audio_id": str(audio_id), |
||||
"result": conclusion.conclusion, |
||||
"index_date": conclusion.index_date, |
||||
"end_date": conclusion.end_date |
||||
} |
||||
@ -0,0 +1,2 @@
|
||||
from apiApp.schemas.audio_schemas import * |
||||
from apiApp.schemas.ai_conclusions_schemas import * |
||||
@ -0,0 +1,45 @@
|
||||
from pydantic import BaseModel, Field |
||||
from typing import Optional, Dict, Any |
||||
from datetime import datetime |
||||
import uuid |
||||
|
||||
|
||||
class AiConclusionBase(BaseModel): |
||||
audio_id: uuid.UUID |
||||
conclusion: Optional[Dict[str, Any]] = None |
||||
|
||||
class AiConclusionCreate(AiConclusionBase): |
||||
pass |
||||
|
||||
|
||||
class AiConclusionResponse(BaseModel): |
||||
id: uuid.UUID |
||||
audio_id: uuid.UUID |
||||
conclusion: Dict[str, Any] |
||||
index_date: Optional[datetime] = None |
||||
end_date: Optional[datetime] = None |
||||
|
||||
class Config: |
||||
from_attributes = True |
||||
|
||||
class RecognitionStatus(BaseModel): |
||||
task_id: str |
||||
audio_id: uuid.UUID |
||||
status: str # pending, processing, completed, error |
||||
result: Optional[Dict[str, Any]] = None |
||||
error: Optional[str] = None |
||||
|
||||
|
||||
class RecognitionStartResponse(BaseModel): |
||||
status: str |
||||
message: str |
||||
task_id: str |
||||
audio_id: uuid.UUID |
||||
|
||||
|
||||
class MessageResponse(BaseModel): |
||||
message: str |
||||
|
||||
|
||||
class ErrorResponse(BaseModel): |
||||
detail: str |
||||
@ -0,0 +1,25 @@
|
||||
from pydantic import BaseModel, Field |
||||
from typing import Optional, Dict, Any |
||||
from datetime import datetime |
||||
import uuid |
||||
|
||||
|
||||
class AudioBase(BaseModel): |
||||
filename: str = Field(..., max_length=255) |
||||
file_path: Optional[str] = None |
||||
duration: Optional[float] = None |
||||
file_size: Optional[int] = None |
||||
|
||||
class AudioCreate(AudioBase): |
||||
pass |
||||
|
||||
class AudioResponse(AudioBase): |
||||
id: uuid.UUID |
||||
index_date: datetime |
||||
|
||||
class Config: |
||||
from_attributes = True |
||||
|
||||
class AudioListResponse(BaseModel): |
||||
audios: list[AudioResponse] |
||||
count: int |
||||
@ -0,0 +1,2 @@
|
||||
from apiApp.services.audio_service import AudioCRUD |
||||
from apiApp.services.ai_conclusion_service import AiConclusionCRUD |
||||
@ -0,0 +1,56 @@
|
||||
from sqlalchemy.orm import Session |
||||
from typing import List, Optional |
||||
import os |
||||
from apiApp.database import Audio |
||||
from apiApp.schemas import AudioCreate |
||||
from apiApp.config import UPLOAD_FOLDER |
||||
import datetime |
||||
import uuid |
||||
|
||||
|
||||
|
||||
class AiConclusionCRUD: |
||||
|
||||
@staticmethod |
||||
def get_by_audio_id(db: Session, audio_id: uuid.UUID): |
||||
"""Получить заключение по audio_id""" |
||||
from apiApp.database import AiConclusion |
||||
return db.query(AiConclusion).filter(AiConclusion.audio_id == audio_id).first() |
||||
|
||||
@staticmethod |
||||
def create(db: Session, audio_id: uuid.UUID, conclusion: dict = None): |
||||
"""Создать новое заключение""" |
||||
from apiApp.database import AiConclusion |
||||
|
||||
db_conclusion = AiConclusion( |
||||
audio_id=audio_id, |
||||
conclusion=conclusion or { |
||||
"transcription": [], |
||||
"ai_transcription": [], |
||||
"conclusion": {} |
||||
}, |
||||
index_date=datetime.datetime.utcnow() |
||||
) |
||||
db.add(db_conclusion) |
||||
db.commit() |
||||
db.refresh(db_conclusion) |
||||
return db_conclusion |
||||
|
||||
@staticmethod |
||||
def update(db: Session, conclusion_id: uuid.UUID, conclusion_data: dict, end_date: bool = False): |
||||
"""Обновить заключение""" |
||||
from apiApp.database import AiConclusion |
||||
|
||||
db_conclusion = db.query(AiConclusion).filter(AiConclusion.id == conclusion_id).first() |
||||
if not db_conclusion: |
||||
return None |
||||
|
||||
if conclusion_data: |
||||
db_conclusion.conclusion = conclusion_data |
||||
|
||||
if end_date: |
||||
db_conclusion.end_date = datetime.datetime.utcnow() |
||||
|
||||
db.commit() |
||||
db.refresh(db_conclusion) |
||||
return db_conclusion |
||||
@ -0,0 +1,79 @@
|
||||
from sqlalchemy.orm import Session |
||||
from typing import List, Optional |
||||
import os |
||||
from apiApp.database import Audio |
||||
from apiApp.schemas import AudioCreate |
||||
from apiApp.config import UPLOAD_FOLDER |
||||
import datetime |
||||
import uuid |
||||
|
||||
|
||||
class AudioCRUD: |
||||
|
||||
@staticmethod |
||||
def get_all(db: Session) -> List[Audio]: |
||||
"""Получить все аудиофайлы""" |
||||
return db.query(Audio).all() |
||||
|
||||
@staticmethod |
||||
def get_by_id(db: Session, audio_id: uuid.UUID) -> Optional[Audio]: |
||||
"""Получить аудиофайл по ID""" |
||||
return db.query(Audio).filter(Audio.id == audio_id).first() |
||||
|
||||
@staticmethod |
||||
def get_by_filename(db: Session, filename: str) -> Optional[Audio]: |
||||
"""Получить аудиофайл по имени файла""" |
||||
return db.query(Audio).filter(Audio.filename == filename).first() |
||||
|
||||
@staticmethod |
||||
def create(db: Session, audio_data: AudioCreate, file_path: str, file_size: int = None) -> Audio: |
||||
"""Создать новую запись аудиофайла""" |
||||
db_audio = Audio( |
||||
filename=audio_data.filename, |
||||
file_path=file_path, |
||||
index_date=datetime.datetime.utcnow(), |
||||
file_size=file_size |
||||
) |
||||
db.add(db_audio) |
||||
db.commit() |
||||
db.refresh(db_audio) |
||||
return db_audio |
||||
|
||||
@staticmethod |
||||
def update(db: Session, audio_id: uuid.UUID, audio_data: dict) -> Optional[Audio]: |
||||
"""Обновить запись аудиофайла""" |
||||
db_audio = db.query(Audio).filter(Audio.id == audio_id).first() |
||||
if not db_audio: |
||||
return None |
||||
|
||||
for key, value in audio_data.items(): |
||||
if hasattr(db_audio, key): |
||||
setattr(db_audio, key, value) |
||||
|
||||
db.commit() |
||||
db.refresh(db_audio) |
||||
return db_audio |
||||
|
||||
@staticmethod |
||||
def delete(db: Session, audio_id: uuid.UUID) -> Optional[Audio]: |
||||
"""Удалить аудиофайл""" |
||||
db_audio = db.query(Audio).filter(Audio.id == audio_id).first() |
||||
if not db_audio: |
||||
return None |
||||
|
||||
# Удаление файла с диска |
||||
if db_audio.file_path and os.path.exists(db_audio.file_path): |
||||
try: |
||||
os.remove(db_audio.file_path) |
||||
except Exception as e: |
||||
print(f"Error deleting file: {e}") |
||||
|
||||
db.delete(db_audio) |
||||
db.commit() |
||||
return db_audio |
||||
|
||||
@staticmethod |
||||
def update_recognition_result(db: Session, audio_id: uuid.UUID, result: dict) -> Optional[Audio]: |
||||
"""Обновить результат распознавания""" |
||||
return AudioCRUD.update(db, audio_id, {"recognition_result": result}) |
||||
|
||||
@ -0,0 +1,80 @@
|
||||
from fastapi import FastAPI, Request, status |
||||
from fastapi.responses import JSONResponse |
||||
from fastapi.staticfiles import StaticFiles |
||||
from fastapi.middleware.cors import CORSMiddleware |
||||
from sqlalchemy.exc import SQLAlchemyError |
||||
import logging |
||||
|
||||
from apiApp.config import APP_TITLE, APP_VERSION, API_V1_PREFIX, UPLOAD_FOLDER, DATABASE_URL |
||||
from apiApp.database import engine, Base |
||||
from apiApp.routers import audio_router, recognition_router |
||||
|
||||
# Настройка логирования |
||||
logging.basicConfig(level=logging.INFO) |
||||
logger = logging.getLogger(__name__) |
||||
|
||||
# Создание FastAPI приложения |
||||
app = FastAPI( |
||||
title=APP_TITLE, |
||||
version=APP_VERSION, |
||||
docs_url=f"{API_V1_PREFIX}/docs", |
||||
redoc_url=f"{API_V1_PREFIX}/redoc", |
||||
openapi_url=f"{API_V1_PREFIX}/openapi.json" |
||||
) |
||||
|
||||
# CORS middleware |
||||
app.add_middleware( |
||||
CORSMiddleware, |
||||
allow_origins=["*"], |
||||
allow_credentials=True, |
||||
allow_methods=["*"], |
||||
allow_headers=["*"], |
||||
) |
||||
|
||||
|
||||
# Глобальный обработчик ошибок SQLAlchemy |
||||
@app.exception_handler(SQLAlchemyError) |
||||
async def sqlalchemy_exception_handler(request: Request, exc: SQLAlchemyError): |
||||
logger.error(f"Database error: {exc}") |
||||
return JSONResponse( |
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, |
||||
content={"detail": "Database error occurred"}, |
||||
) |
||||
|
||||
|
||||
# Создание таблиц |
||||
@app.on_event("startup") |
||||
async def startup_event(): |
||||
"""Создание таблиц при запуске приложения""" |
||||
try: |
||||
Base.metadata.create_all(bind=engine) |
||||
logger.info("Database tables created successfully") |
||||
except Exception as e: |
||||
logger.error(f"Error creating database tables: {e}") |
||||
|
||||
|
||||
# Подключение routers |
||||
app.include_router(audio_router, prefix=API_V1_PREFIX, tags=["audio"]) |
||||
app.include_router(recognition_router, prefix=API_V1_PREFIX, tags=["recognition"]) |
||||
|
||||
# Статические файлы (для загрузки аудио) |
||||
app.mount("/uploads", StaticFiles(directory=str(UPLOAD_FOLDER)), name="uploads") |
||||
|
||||
|
||||
@app.get("/") |
||||
async def root(): |
||||
return { |
||||
"application": APP_TITLE, |
||||
"version": APP_VERSION, |
||||
"status": "running" |
||||
} |
||||
|
||||
|
||||
@app.get("/health") |
||||
async def health_check(): |
||||
return {"status": "healthy"} |
||||
|
||||
|
||||
if __name__ == "__main__": |
||||
import uvicorn |
||||
uvicorn.run(app, host="0.0.0.0", port=8000) |
||||
@ -0,0 +1,6 @@
|
||||
fastapi==0.115.0 |
||||
uvicorn[standard]==0.32.0 |
||||
sqlalchemy==2.0.35 |
||||
pydantic==2.9.2 |
||||
python-multipart==0.0.12 |
||||
aiofiles==24.1.0 |
||||
Loading…
Reference in new issue