API для работы с файлами, индексация файлов и результатов распощнавания
This commit is contained in:
@@ -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
|
||||||
+38
@@ -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,184 @@
|
|||||||
|
# FileAudioAPI - Quick Start Guide
|
||||||
|
|
||||||
|
## 🚀 Быстрый старт
|
||||||
|
|
||||||
|
### Вариант 1: Запуск с Docker (рекомендуется)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Клонировать репозиторий
|
||||||
|
cd /Users/petr/SpeechAnalytics/FileAudioAPI
|
||||||
|
|
||||||
|
# Запуск с Docker Compose
|
||||||
|
make up
|
||||||
|
|
||||||
|
# Или вручную
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Приложение будет доступно по адресу: **http://localhost:8000**
|
||||||
|
|
||||||
|
API документация:
|
||||||
|
- 📚 Swagger UI: http://localhost:8000/api/v1/docs
|
||||||
|
- 📖 ReDoc: http://localhost:8000/api/v1/redoc
|
||||||
|
|
||||||
|
### Вариант 2: Локальный запуск
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Установить зависимости
|
||||||
|
make install
|
||||||
|
# или
|
||||||
|
pip3 install -r requirements.txt
|
||||||
|
|
||||||
|
# 2. Создать базу данных
|
||||||
|
make db-migrate
|
||||||
|
|
||||||
|
# 3. Запустить приложение
|
||||||
|
make dev
|
||||||
|
# или
|
||||||
|
python3 run.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 Makefile команды
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make help # Показать все доступные команды
|
||||||
|
make build # Собрать Docker образ
|
||||||
|
make up # Запустить контейнеры
|
||||||
|
make down # Остановить контейнеры
|
||||||
|
make logs # Просмотр логов
|
||||||
|
make shell # Открыть shell в контейнере
|
||||||
|
make clean # Очистка контейнеров и volumes
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 Полная документация
|
||||||
|
|
||||||
|
- [FastAPI API Guide](README_FASTAPI.md) - Документация API
|
||||||
|
- [Docker Deployment](README_DOCKER.md) - Docker развертывание
|
||||||
|
|
||||||
|
## 📁 Структура проекта
|
||||||
|
|
||||||
|
```
|
||||||
|
FileAudioAPI/
|
||||||
|
├── apiApp/
|
||||||
|
│ ├── schemas/ # Pydantic модели валидации
|
||||||
|
│ ├── services/ # Business logic (CRUD)
|
||||||
|
│ ├── routers/ # API endpoints
|
||||||
|
│ ├── database/ # SQLAlchemy модели
|
||||||
|
│ ├── dbApi/ # Legacy совместимость
|
||||||
|
│ ├── config.py # Конфигурация
|
||||||
|
│ └── database.py # SQLAlchemy настройка
|
||||||
|
├── main.py # FastAPI приложение
|
||||||
|
├── Dockerfile # Docker образ
|
||||||
|
├── docker-compose.yml # Docker Compose конфиг
|
||||||
|
├── Makefile # Управление проектом
|
||||||
|
└── requirements.txt # Зависимости
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 Основные endpoints
|
||||||
|
|
||||||
|
### Загрузка файла
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/api/v1/upload \
|
||||||
|
-F "file=@audio.mp3"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Список файлов
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/api/v1/audio/list
|
||||||
|
```
|
||||||
|
|
||||||
|
### Запуск распознавания
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/api/v1/recognize/{audio_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Статус распознавания
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/api/v1/recognize/{audio_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🛠️ Разработка
|
||||||
|
|
||||||
|
### Локальный запуск с автоперезагрузкой
|
||||||
|
```bash
|
||||||
|
python3 run.py
|
||||||
|
# или
|
||||||
|
make dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Запуск тестов
|
||||||
|
```bash
|
||||||
|
make test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Форматирование кода
|
||||||
|
```bash
|
||||||
|
make format
|
||||||
|
```
|
||||||
|
|
||||||
|
### Линтер
|
||||||
|
```bash
|
||||||
|
make lint
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🐛 Troubleshooting
|
||||||
|
|
||||||
|
### Проблема: Module not found
|
||||||
|
```bash
|
||||||
|
# Переустановить зависимости
|
||||||
|
pip3 install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проблема: Database error
|
||||||
|
```bash
|
||||||
|
# Пересоздать базу данных
|
||||||
|
make clean
|
||||||
|
make db-migrate
|
||||||
|
make up
|
||||||
|
```
|
||||||
|
|
||||||
|
### Проблема: Port already in use
|
||||||
|
```bash
|
||||||
|
# Изменить порт в docker-compose.yml
|
||||||
|
# или в main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Environment переменные
|
||||||
|
|
||||||
|
Скопируйте `.env.example` в `.env` и настройте:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Доступные переменные:
|
||||||
|
- `DATABASE_URL` - URL базы данных
|
||||||
|
- `MAX_UPLOAD_SIZE` - Макс. размер загрузки
|
||||||
|
- `APP_TITLE` - Название приложения
|
||||||
|
- `APP_VERSION` - Версия приложения
|
||||||
|
|
||||||
|
## 🔒 Безопасность
|
||||||
|
|
||||||
|
Для production:
|
||||||
|
1. Измените `CORS` origins в `main.py`
|
||||||
|
2. Настройте `DATABASE_URL` на PostgreSQL
|
||||||
|
3. Используйте Redis для хранения статусов задач
|
||||||
|
4. Добавьте аутентификацию
|
||||||
|
5. Настройте SSL/HTTPS
|
||||||
|
|
||||||
|
## 📊 Мониторинг
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Health check
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
|
||||||
|
# Docker stats
|
||||||
|
docker stats file-audio-api
|
||||||
|
|
||||||
|
# Логи
|
||||||
|
make logs
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🤝 Поддержка
|
||||||
|
|
||||||
|
Для вопросов и предложений создайте issue в репозитории.
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
0?CA: FastAPI ?@8;>65=8O 4;O Speech Analytics API
|
||||||
|
"""
|
||||||
|
import uvicorn
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# >102;O5< :>@=52CN 48@5:B>@8N 2 Python path
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
uvicorn.run(
|
||||||
|
"main:app",
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=8000,
|
||||||
|
reload=True, # 2B><0B8G5A:0O ?5@5703@C7:0 ?@8 87<5=5=88 :>40
|
||||||
|
log_level="info"
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user