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