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