Browse Source

API для работы с файлами, индексация файлов и результатов распощнавания

master
poturaevpetr 2 weeks ago
parent
commit
c39b348472
  1. 63
      .dockerignore
  2. 17
      .env.example
  3. 38
      Dockerfile
  4. 82
      Makefile
  5. 221
      README_DOCKER.md
  6. 138
      README_FASTAPI.md
  7. 184
      START.md
  8. 4
      apiApp/__init__.py
  9. 19
      apiApp/config.py
  10. 25
      apiApp/database.py
  11. 21
      apiApp/database/AiConclusion.py
  12. 28
      apiApp/database/Audio.py
  13. 12
      apiApp/database/ConclusionVersion.py
  14. 16
      apiApp/database/Operator.py
  15. 31
      apiApp/database/__init__.py
  16. 65
      apiApp/dbApi/__init__.py
  17. 38
      apiApp/dbApi/audio_db.py
  18. 0
      apiApp/dbApi/conclusion_db.py
  19. 4
      apiApp/routers/__init__.py
  20. 155
      apiApp/routers/audio.py
  21. 264
      apiApp/routers/recognition.py
  22. 2
      apiApp/schemas/__init__.py
  23. 45
      apiApp/schemas/ai_conclusions_schemas.py
  24. 25
      apiApp/schemas/audio_schemas.py
  25. 2
      apiApp/services/__init__.py
  26. 56
      apiApp/services/ai_conclusion_service.py
  27. 79
      apiApp/services/audio_service.py
  28. 80
      main.py
  29. 6
      requirements.txt
  30. 19
      run.py
  31. 0
      speech_analytics.db

63
.dockerignore

@ -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

17
.env.example

@ -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
Dockerfile

@ -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"]

82
Makefile

@ -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/

221
README_DOCKER.md

@ -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"
```

138
README_FASTAPI.md

@ -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

184
START.md

@ -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 в репозитории.

4
apiApp/__init__.py

@ -0,0 +1,4 @@
# apiApp package
# FastAPI application for Speech Analytics
__version__ = "1.0.0"

19
apiApp/config.py

@ -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"

25
apiApp/database.py

@ -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()

21
apiApp/database/AiConclusion.py

@ -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")

28
apiApp/database/Audio.py

@ -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
}

12
apiApp/database/ConclusionVersion.py

@ -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)

16
apiApp/database/Operator.py

@ -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")

31
apiApp/database/__init__.py

@ -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

65
apiApp/dbApi/__init__.py

@ -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()

38
apiApp/dbApi/audio_db.py

@ -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
apiApp/dbApi/conclusion_db.py

4
apiApp/routers/__init__.py

@ -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"]

155
apiApp/routers/audio.py

@ -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

264
apiApp/routers/recognition.py

@ -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
}

2
apiApp/schemas/__init__.py

@ -0,0 +1,2 @@
from apiApp.schemas.audio_schemas import *
from apiApp.schemas.ai_conclusions_schemas import *

45
apiApp/schemas/ai_conclusions_schemas.py

@ -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

25
apiApp/schemas/audio_schemas.py

@ -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

2
apiApp/services/__init__.py

@ -0,0 +1,2 @@
from apiApp.services.audio_service import AudioCRUD
from apiApp.services.ai_conclusion_service import AiConclusionCRUD

56
apiApp/services/ai_conclusion_service.py

@ -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

79
apiApp/services/audio_service.py

@ -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})

80
main.py

@ -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)

6
requirements.txt

@ -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

19
run.py

@ -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"
)

0
speech_analytics.db

Loading…
Cancel
Save