Compare commits

...

32 Commits

Author SHA1 Message Date
poturaevpetr 4dca198d3c обновлена документация по всем сервисам 2026-04-16 22:39:51 +05:00
poturaevpetr fe0bed7f58 Merge remote-tracking branch 'origin/dev' into dev 2026-03-25 16:22:56 +05:00
poturaevpetr 94633288c3 логирование ошибок транскрибации для битых файлов 2026-03-25 16:21:44 +05:00
PoturaevPetr 15925c0217 Merge remote-tracking branch 'origin/dev' into dev 2026-03-18 01:33:24 +05:00
PoturaevPetr e161681bc9 не отправлять callback если это внешнй файл 2026-03-18 01:33:15 +05:00
poturaevpetr 5360ea7f9c запрос на получение заклюсения по распознаванию файла 2026-03-18 01:31:29 +05:00
poturaevpetr d460728b2e fix start recognition external file 2026-03-18 01:14:41 +05:00
poturaevpetr 8e1175c9bf fix start recognition external file 2026-03-18 00:49:26 +05:00
poturaevpetr 014271e929 add external service 2026-03-18 00:35:57 +05:00
poturaevpetr ae4cfa0119 add external service 2026-03-18 00:33:01 +05:00
poturaevpetr db62f9ef74 add external service 2026-03-18 00:29:43 +05:00
poturaevpetr bf507b5363 add external service 2026-03-18 00:27:49 +05:00
poturaevpetr 9fc60e8d19 загрузка и распознвание внешний файлов 2026-03-18 00:04:01 +05:00
poturaevpetr f3e3da1696 удаление кэша сегментов аудио после обработки, добавлена инструкция для очистки контейнеров и образов 2026-02-24 13:48:24 +05:00
PoturaevPetr b8da32f44a Merge remote-tracking branch 'origin/dev' into dev 2026-01-26 18:55:04 +05:00
PoturaevPetr 688417990f fix bugs prod 2026-01-26 18:53:18 +05:00
poturaevpetr e31440f91e модальное окно для управление перераспознанием с выбором промпта 2026-01-26 18:49:55 +05:00
poturaevpetr 163b019b31 pool models, parallel workers 2026-01-16 12:21:23 +05:00
poturaevpetr cf25f364d1 add check dublicate, add balancer 2026-01-16 11:11:17 +05:00
poturaevpetr 34a31cc4d6 fix db connection 2025-12-30 13:57:36 +05:00
PoturaevPetr ebef51aa48 fix cnnect bp 2025-12-30 12:01:27 +05:00
poturaevpetr 013dd9a7d4 fix 2025-12-30 02:51:00 +05:00
PoturaevPetr c1a9ef27f1 fix settings 2025-12-30 02:43:34 +05:00
poturaevpetr ac8e881ee3 fix 2025-12-30 02:40:27 +05:00
poturaevpetr 21dafb5681 fix 2025-12-30 02:22:22 +05:00
poturaevpetr 9f65a9398a fix 2025-12-30 02:10:56 +05:00
poturaevpetr b8af963e9f fix 2025-12-30 02:07:40 +05:00
poturaevpetr c4dec7644d fix 2025-12-30 02:00:38 +05:00
poturaevpetr 2df1761d34 fix 2025-12-30 01:53:03 +05:00
poturaevpetr 59b5676c84 fix 2025-12-30 01:47:36 +05:00
poturaevpetr ddbebea001 add autorestart rec 2025-12-30 00:34:51 +05:00
poturaevpetr 86245ce1fb add autorestart rec 2025-12-30 00:31:11 +05:00
16 changed files with 927 additions and 528 deletions
+3 -2
View File
@@ -32,7 +32,8 @@ RUN mkdir -p /app/uploads
RUN mkdir -p /app/data
# Открытие порта
EXPOSE 8000
EXPOSE 5008
# Команда запуска приложения
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
CMD ["python", "run.py"]
+224 -1
View File
@@ -1,3 +1,226 @@
# FileAudioAPI
Сервис для хранения файлов аудио, индексации файлов, записи и выдачи результатов распознавания
## Назначение и роль в экосистеме
**FileAudioAPI** — сервис **учёта аудио и результатов распознавания** между диском, GPU-распознаванием и веб-клиентом. На **FastAPI**: хранит метаданные и статусы в БД, складывает файлы в общий каталог **`AUDIOFILES_PATH`** (тот же volume `audiofiles`, что у **GigaAM_API** и **Calls_WEB_Client_main**), инициирует ASR в **GigaAM_API**, принимает обратно текст/заключение, при необходимости дергает webhook в **Calls_WEB_Client_main**.
| Направление | Роль |
|-------------|------|
| **Внутренние файлы** | Регистрация, очереди `process-all`, статусы `recognition_*`, связь с `AiConclusion`. |
| **Внешние файлы** | Загрузка с опциональным `callback_url`, сохранение под UUID-именем в общей папке, **всегда** постановка на распознавание в GigaAM. |
| **Интеграция** | `GIGAAM_API_URL`, `CALLS_WEB_CLIENT_URL` / `WEBHOOK_API_KEY` для доставки результатов в основное приложение. |
Swagger: **`/api/v1/docs`** (см. ниже).
---
Сервис для хранения аудиофайлов, индексации файлов, записи и выдачи результатов распознавания, реализованный на **FastAPI**.
## 🚀 Быстрый старт
```bash
# Перейти в директорию сервиса (от корня репозитория SpeechAnalytics)
cd FileAudioAPI
# Запуск с Docker (рекомендуется)
docker-compose up -d
# Или использовать Makefile
make up
```
Приложение будет доступно по адресу: **http://localhost:8000**
API документация:
- 📚 **Swagger UI**: http://localhost:8000/api/v1/docs
- 📖 **ReDoc**: http://localhost:8000/api/v1/redoc
## 📚 Документация
- 📘 [Quick Start Guide](START.md) - Быстрый старт
- 📗 [API Documentation](README_FASTAPI.md) - Полная документация API
- 📕 [Docker Deployment](README_DOCKER.md) - Развертывание в Docker
## 🎯 Возможности
- ✅ Загрузка аудиофайлов (MP3, WAV, OGG, FLAC, M4A, AAC)
- ✅ Хранение файлов с автоматической индексацией
- ✅ Асинхронное распознавание аудио в фоне
- ✅ Отслеживание статуса распознавания
- ✅ Получение результатов распознавания
- ✅ RESTful API с автоматической документацией
- ✅ Валидация данных с Pydantic
- ✅ Docker поддержка
## 🏗️ Архитектура
```
apiApp/
├── schemas/ # Pydantic модели (валидация)
├── services/ # Business logic (CRUD операции)
├── routers/ # API endpoints (контроллеры)
├── database/ # SQLAlchemy модели
├── dbApi/ # Legacy compatibility layer
├── config.py # Конфигурация
└── database.py # SQLAlchemy настройка
```
## 🔧 Технологии
- **FastAPI** - Современный веб-фреймворк
- **SQLAlchemy 2.0** - ORM для работы с БД
- **Pydantic** - Валидация данных
- **Uvicorn** - ASGI сервер
- **Docker** - Контейнеризация
## 📋 Основные endpoints
| Метод | Endpoint | Описание |
|-------|----------|----------|
| POST | `/api/v1/upload` | Загрузка аудиофайла |
| GET | `/api/v1/audio/list` | Список всех файлов |
| GET | `/api/v1/audio/{id}` | Информация о файле |
| GET | `/api/v1/audio/file/{id}` | Скачать файл |
| DELETE | `/api/v1/audio/delete/{id}` | Удалить файл |
| POST | `/api/v1/recognize/{id}` | Запустить распознавание |
| GET | `/api/v1/recognize/{id}` | Статус распознавания |
| GET | `/api/v1/recognize/task/{task_id}` | Статус по task_id |
| GET | `/api/v1/recognize/{id}/result` | Результат распознавания |
## 🛠️ Установка и запуск
### С Docker (рекомендуется)
```bash
# Использование Makefile
make up
# Или напрямую с Docker Compose
docker-compose up -d
# Просмотр логов
make logs
# или
docker-compose logs -f
```
### Локально
```bash
# Установка зависимостей
make install
# или
pip3 install -r requirements.txt
# Создание базы данных
make db-migrate
# Запуск
make dev
# или
python3 run.py
```
## 🔧 Makefile команды
```bash
make help # Показать все команды
make build # Собрать Docker образ
make up # Запустить контейнеры
make down # Остановить контейнеры
make restart # Перезапустить
make logs # Логи
make shell # Shell в контейнере
make test # Запуск тестов
make clean # Очистка
```
## 📝 Примеры использования
### Загрузка файла
```bash
curl -X POST http://localhost:8000/api/v1/upload \
-F "file=@audio.mp3"
```
### Запуск распознавания
```bash
curl -X POST http://localhost:8000/api/v1/recognize/{audio_id}
```
### Получение статуса
```bash
curl http://localhost:8000/api/v1/recognize/{audio_id}
```
### Получение результата
```bash
curl http://localhost:8000/api/v1/recognize/{audio_id}/result
```
## 🔒 Production
Для production развертывания:
1. Используйте PostgreSQL вместо SQLite
2. Настройте Redis для хранения статусов задач
3. Ограничьте CORS origins
4. Добавьте аутентификацию
5. Настройте SSL/HTTPS
6. Используйте несколько workers
Подробнее в [Docker Deployment Guide](README_DOCKER.md)
## 🧪 Тестирование
```bash
# Запуск тестов
make test
# Или напрямую
pytest tests/ -v
```
## 📊 Мониторинг
```bash
# Health check
curl http://localhost:8000/health
# Docker stats
docker stats file-audio-api
# Логи
docker-compose logs -f app
```
## 🤝 Рефакторинг
Проект был успешно рефакторин из Flask в FastAPI с улучшением архитектуры:
**Основные изменения:**
- ✅ Многослойная архитектура (schemas → services → routers)
- ✅ Разделение по доменам
- ✅ Асинхронная обработка задач
- ✅ Автодокументация API
- ✅ Type hints и валидация
- ✅ Docker поддержка
## 📄 Лицензия
[MIT License](LICENSE)
## 👨‍💻 Автор
Speech Analytics Team
---
📖 Более подробная информация в документации:
- [Quick Start](START.md)
- [API Docs](README_FASTAPI.md)
- [Docker Guide](README_DOCKER.md)
+8 -3
View File
@@ -21,16 +21,18 @@ ALLOWED_AUDIO_EXTENSIONS = {".mp3", ".wav", ".ogg", ".flac", ".m4a", ".aac"}
APP_TITLE = "Speech Analytics API"
APP_VERSION = "1.0.0"
PORT = int(os.getenv("PORT", "8000"))
PORT = int(os.getenv("PORT", "5008"))
HOST = os.getenv("HOST", "localhost")
# GigaAM API Configuration
GIGAAM_API_URL = os.getenv("GIGAAM_API_URL", "http://gigaam_api:5001")
GIGAAM_API_URL = os.getenv("GIGAAM_API_URL", "http://192.168.1.73:5002")
AUDIOFILES_PATH = os.path.join(os.getcwd(), os.getenv("FILESAPTH", "audiofiles"))
# Calls_WEB_Client_main Webhook Configuration
CALLS_WEB_CLIENT_URL = os.getenv(
"CALLS_WEB_CLIENT_URL",
"http://calls_web_client:8000"
"http://192.168.1.73:8642"
)
WEBHOOK_ENDPOINT = f"{CALLS_WEB_CLIENT_URL}/api/transcription/webhook"
WEBHOOK_API_KEY = os.getenv("WEBHOOK_API_KEY", "webhook_secret_key")
@@ -39,3 +41,6 @@ WEBHOOK_API_KEY = os.getenv("WEBHOOK_API_KEY", "webhook_secret_key")
ENABLE_AUTO_RESTORE = os.getenv("ENABLE_AUTO_RESTORE", "true").lower() == "true"
AUTO_RESTORE_LIMIT = int(os.getenv("AUTO_RESTORE_LIMIT", "100")) # Максимум файлов для восстановления
AUTO_RESTORE_DELAY = int(os.getenv("AUTO_RESTORE_DELAY", "5")) # Задержка перед запуском (секунды)
# Recognition retry policy (FileAudioAPI side)
MAX_RECOGNITION_ATTEMPTS = int(os.getenv("MAX_RECOGNITION_ATTEMPTS", "3"))
+5 -1
View File
@@ -6,7 +6,11 @@ from apiApp.config import DATABASE_URL
# Создание engine
engine = create_engine(
DATABASE_URL,
connect_args={"check_same_thread": False} if "sqlite" in DATABASE_URL else {}
connect_args={"check_same_thread": False} if "sqlite" in DATABASE_URL else {},
pool_pre_ping=True, # Проверять соединение перед использованием
pool_recycle=600, # Пересоздавать соединения каждые 10 минут
pool_size=10, # Размер пула соединений
max_overflow=20 # Дополнительные соединения при пиковой нагрузке
)
# SessionLocal
+11 -1
View File
@@ -14,6 +14,11 @@ class Audio(Base):
file_path = Column(Text)
duration = Column(Float)
file_size = Column(Integer)
sourse = Column(Text, default="internal")
recognition_status = Column(Text, default="pending", index=True) # pending, processing, completed, failed
recognition_attempts = Column(Integer, default=0)
recognition_last_error = Column(Text, nullable=True)
recognition_last_attempt_at = Column(DateTime, nullable=True)
ai_conclusion = relationship("AiConclusion", back_populates="audio", cascade="all, delete-orphan")
@@ -24,5 +29,10 @@ class Audio(Base):
"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
"file_size": self.file_size,
"sourse": self.sourse,
"recognition_status": self.recognition_status,
"recognition_attempts": self.recognition_attempts,
"recognition_last_error": self.recognition_last_error,
"recognition_last_attempt_at": self.recognition_last_attempt_at.isoformat() if self.recognition_last_attempt_at else None,
}
+2 -2
View File
@@ -1,4 +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"]
from apiApp.routers.external_audio import router as external_audio_router
__all__ = ["audio_router", "recognition_router", "external_audio_router"]
+98
View File
@@ -16,6 +16,31 @@ from apiApp.config import WEBHOOK_ENDPOINT, WEBHOOK_API_KEY
logger = logging.getLogger(__name__)
ai_conclusion_router = APIRouter()
def _send_callback(callback_url: str, audio: Audio, conclusion_data: Dict[str, Any]) -> None:
"""Отправляем результат клиенту по callback_url (не храним в БД)."""
try:
callback_url = (callback_url or "").strip()
if not callback_url:
return
if not callback_url.startswith(("http://", "https://")):
logger.warning(f"⚠️ Некорректный callback_url для {audio.filename}: {callback_url}")
return
payload = {
"audio_id": str(audio.id),
"filename": audio.filename,
"result": conclusion_data
}
resp = requests.post(callback_url, json=payload, timeout=30)
if 200 <= resp.status_code < 300:
logger.info(f"✅ Callback успешно отправлен для {audio.filename}")
else:
logger.warning(f"⚠️ Callback вернул статус {resp.status_code} для {audio.filename}")
logger.warning(f"Response: {resp.text}")
except Exception as e:
logger.error(f"❌ Ошибка при отправке callback для {audio.filename}: {e}")
class AiConclusionRequest(BaseModel):
"""Модель запроса для сохранения AI заключения"""
@@ -25,6 +50,7 @@ class AiConclusionRequest(BaseModel):
analysis: Dict[str, Any]
segments: Optional[List[Dict[str, Any]]] = []
processing_time_seconds: Optional[float] = 0
callback_url: Optional[str] = None
class AiConclusionResponse(BaseModel):
@@ -36,6 +62,46 @@ class AiConclusionResponse(BaseModel):
error: Optional[str] = None
class RecognitionFailedRequest(BaseModel):
filename: str
error: str
class ConclusionByFilenameResponse(BaseModel):
"""Заключение по имени файла"""
filename: str
audio_id: str
conclusion: Dict[str, Any]
index_date: Optional[datetime] = None
end_date: Optional[datetime] = None
@ai_conclusion_router.get("/conclusion/by-filename/{filename}", response_model=ConclusionByFilenameResponse)
async def get_conclusion_by_filename(filename: str, db: Session = Depends(get_db)):
"""
Возвращает заключение по распознаванию по имени файла.
Имя файла задаётся в path (то же, что сохранено в БД при загрузке).
"""
audio = db.query(Audio).filter(Audio.filename == filename).first()
if not audio:
raise HTTPException(status_code=404, detail=f"Файл не найден: {filename}")
conclusion_row = db.query(AiConclusion).filter(AiConclusion.audio_id == audio.id).first()
if not conclusion_row:
raise HTTPException(
status_code=404,
detail=f"Заключение по распознаванию для файла не найдено: {filename}"
)
return ConclusionByFilenameResponse(
filename=audio.filename,
audio_id=str(audio.id),
conclusion=conclusion_row.conclusion or {},
index_date=conclusion_row.index_date,
end_date=conclusion_row.end_date,
)
@ai_conclusion_router.post("/conclusion/save", response_model=AiConclusionResponse)
async def save_ai_conclusion(request: AiConclusionRequest, db: Session = Depends(get_db)):
"""
@@ -93,8 +159,18 @@ async def save_ai_conclusion(request: AiConclusionRequest, db: Session = Depends
db.commit()
logger.info(f"✅ Заключение сохранено для {request.filename}")
# Обновляем статус распознавания у Audio
audio.recognition_status = "completed"
audio.recognition_last_error = None
db.commit()
# Для внешних файлов — отправляем результат клиенту из FileAudioAPI
if (audio.sourse or "").lower() == "external" and request.callback_url:
_send_callback(request.callback_url, audio, conclusion_data)
# Отправляем webhook в Calls_WEB_Client_main для анализа
try:
if (audio.sourse or "").lower() != "external":
logger.info(f"📤 Отправка webhook в Calls_WEB_Client_main для {request.filename}")
webhook_payload = {
@@ -142,3 +218,25 @@ async def save_ai_conclusion(request: AiConclusionRequest, db: Session = Depends
status_code=500,
detail=str(e)
)
@ai_conclusion_router.post("/conclusion/failed", response_model=AiConclusionResponse)
async def mark_recognition_failed(request: RecognitionFailedRequest, db: Session = Depends(get_db)):
"""
Помечает распознавание как failed для файла (чтобы auto-restore не пытался бесконечно).
Используется GigaAM_API при невозможности получить результат.
"""
audio = db.query(Audio).filter(Audio.filename == request.filename).first()
if not audio:
raise HTTPException(status_code=404, detail=f"Файл не найден: {request.filename}")
audio.recognition_status = "failed"
audio.recognition_last_error = request.error
db.commit()
return AiConclusionResponse(
success=True,
message="Recognition marked as failed",
audio_id=str(audio.id),
filename=audio.filename
)
+256 -92
View File
@@ -1,3 +1,91 @@
"""
API endpoints для управления аудиофайлами (регистрация и пакетная обработка)
Используется Calls_WEB_Client_main для оркестрации процесса распознавания
"""
from fastapi import APIRouter, HTTPException, Depends, BackgroundTasks
from sqlalchemy.orm import Session
from pydantic import BaseModel
from typing import Optional, List
import os
import logging
from datetime import datetime
from apiApp.database import get_db
from apiApp.database.Audio import Audio
from apiApp.database.AiConclusion import AiConclusion
from apiApp.config import AUDIOFILES_PATH, MAX_RECOGNITION_ATTEMPTS
logger = logging.getLogger(__name__)
audio_management_router = APIRouter()
def query_audio_without_conclusion(db, limit=None):
"""
Возвращает запрос для поиска Audio без AiConclusion
Использует exists() подзапрос, так как AiConclusion - это relationship
"""
from sqlalchemy import exists, not_
# Используем более простой подзапрос
subquery = exists().where(
AiConclusion.audio_id == Audio.id
)
# Берём только те, которые еще можно/нужно распознавать
query = db.query(Audio).filter(
~subquery
).filter(
Audio.sourse == "internal"
).filter(
(Audio.recognition_status.in_(["pending", "processing"])) |
((Audio.recognition_status == "failed") & (Audio.recognition_attempts < MAX_RECOGNITION_ATTEMPTS))
).order_by(Audio.index_date.asc())
if limit:
query = query.limit(limit)
return query
class AudioRegisterRequest(BaseModel):
"""Запрос на регистрацию аудиофайла"""
filename: str
file_path: str # Полный путь к файлу в общей папке audiofiles
class AudioProcessAllRequest(BaseModel):
"""Запрос на пакетное распознавание"""
limit: int = 100
class AudioRegisterResponse(BaseModel):
"""Ответ на регистрацию аудиофайла"""
id: str
filename: str
file_size: int
created_at: datetime
@audio_management_router.post("/audio/register", response_model=AudioRegisterResponse, status_code=201)
async def register_audio_file(
request: AudioRegisterRequest,
db: Session = Depends(get_db)
):
"""
Регистрация аудиофайла в БД (без копирования файла)
Создаёт запись в таблице Audio для файла, который уже находится
в общей папке audiofiles. НЕ копирует файл, только создаёт запись в БД.
Args:
request: {filename: "in-xxx.wav", file_path: "/app/audiofiles/in-xxx.wav"}
Returns:
201 Created + информация о созданной записи
400 Bad Request если файл уже зарегистрирован
404 Not Found если файл не существует на диске
"""
try:
filename = request.filename
file_path = request.file_path
@@ -28,6 +116,10 @@
audio.filename = filename
audio.file_size = file_size
audio.index_date = datetime.utcnow()
audio.recognition_status = "pending"
audio.recognition_attempts = 0
audio.recognition_last_error = None
audio.recognition_last_attempt_at = None
db.add(audio)
db.commit()
@@ -66,23 +158,64 @@ def process_audio_file(audio_id: str, db: Session):
return
logger.info(f"🎵 Запуск распознавания для {audio.filename}")
audio.recognition_status = "processing"
audio.recognition_attempts = (audio.recognition_attempts or 0) + 1
audio.recognition_last_attempt_at = datetime.utcnow()
audio.recognition_last_error = None
db.commit()
# Формируем запрос в GigaAM API
# Проверяем что файл существует на диске
from apiApp.config import AUDIOFILES_PATH
import os
file_path = os.path.join(AUDIOFILES_PATH, audio.filename)
if not os.path.exists(file_path):
logger.error(f"❌ Файл не найден на диске в FileAudioAPI: {file_path}")
audio.recognition_status = "failed"
audio.recognition_last_error = f"File not found on disk: {file_path}"
db.commit()
return
file_size = os.path.getsize(file_path)
logger.info(f"📁 Файл найден: {file_path} (размер: {file_size} bytes)")
# Формируем запрос в GigaAM API - ТОЛЬКО асинхронный endpoint
from apiApp.config import GIGAAM_API_URL
api_url = f"{GIGAAM_API_URL}/api/call/process"
# Используем только асинхронный endpoint
api_url = f"{GIGAAM_API_URL}/api/call/process/async"
payload = {
"filename": audio.filename
}
# Отправляем запрос в GigaAM API
import requests
try:
# Отправляем запрос в асинхронный endpoint
response = requests.post(api_url, json=payload, timeout=10)
if response.status_code == 200 or response.status_code == 202:
logger.info(f"✅ Запущено распознавание для {audio.filename}")
result = response.json()
task_id = result.get('task_id')
logger.info(f"✅ Задача добавлена в очередь для {audio.filename} (task_id: {task_id})")
else:
logger.error(f"❌ Ошибка запуска распознавания для {audio.filename}: {response.status_code} - {response.text}")
error_detail = response.text
logger.error(f"❌ Ошибка запуска распознавания для {audio.filename}: {response.status_code}")
logger.error(f" Detail: {error_detail}")
audio.recognition_status = "failed"
audio.recognition_last_error = f"GigaAM start failed: {response.status_code} {error_detail}"
db.commit()
except requests.exceptions.Timeout:
logger.error(f"❌ Таймаут при отправке задачи для {audio.filename}")
audio.recognition_status = "failed"
audio.recognition_last_error = "Timeout when starting recognition in GigaAM"
db.commit()
except requests.exceptions.ConnectionError as e:
logger.error(f"❌ Ошибка подключения к GigaAM API для {audio.filename}: {e}")
audio.recognition_status = "failed"
audio.recognition_last_error = f"Connection error when starting recognition in GigaAM: {e}"
db.commit()
except Exception as e:
logger.error(f"❌ Ошибка при обработке {audio_id}: {e}")
@@ -107,28 +240,18 @@ async def process_all_pending_audio(
200 OK + {
"started_count": 15,
"pending_files": ["file1.wav", "file2.wav", ...],
"total_pending": 50
"total_pending": 50,
"skipped_duplicates": 2
}
"""
try:
from sqlalchemy import exists
limit = request.limit
logger.info(f"🚀 Поиск Audio без AiConclusion (limit={limit})")
# Находим все Audio без AiConclusion через подзапрос
subquery = db.query(AiConclusion.audio_id).filter(
AiConclusion.audio_id == Audio.id
)
pending_audio = db.query(Audio).filter(
~exists().where(subquery.exists())
).order_by(Audio.index_date.asc()).limit(limit).all()
total_pending = db.query(Audio).filter(
~exists().where(subquery.exists())
).count()
# Находим все Audio без AiConclusion используя вспомогательную функцию
pending_audio = query_audio_without_conclusion(db, limit).all()
total_pending = query_audio_without_conclusion(db).count()
if not pending_audio:
logger.info("ℹ️ Нет файлов для распознавания")
@@ -136,6 +259,7 @@ async def process_all_pending_audio(
"started_count": 0,
"pending_files": [],
"total_pending": 0,
"skipped_duplicates": 0,
"message": "Нет файлов без AiConclusion"
}
@@ -144,6 +268,7 @@ async def process_all_pending_audio(
# Добавляем задачи в фон
started_count = 0
pending_files = []
skipped_duplicates = 0 # Счётчик дубликатов
for audio in pending_audio:
# Проверяем, что файл существует
@@ -152,25 +277,46 @@ async def process_all_pending_audio(
logger.warning(f"⚠️ Файл не найден на диске: {audio.filename}")
continue
# ПРОВЕРКА НА ДУБЛИКАТЫ: Проверяем, не обрабатывается ли файл уже
import requests
try:
# Проверяем статус через GigaAM API
check_url = f"{os.getenv('GIGAAM_API_URL', 'http://gigaam_api:5001')}/api/call/check-status"
check_response = requests.post(
check_url,
json={"filename": audio.filename},
timeout=5
)
if check_response.status_code == 200:
check_data = check_response.json()
if check_data.get('in_queue', False):
logger.info(f"⏭️ Файл уже в очереди GigaAM: {audio.filename}")
skipped_duplicates += 1
continue
except Exception as e:
# Если проверка упала - всё равно продолжаем
logger.debug(f"Не удалось проверить статус {audio.filename}: {e}")
# Добавляем в фон (асинхронно)
# В FastAPI используем BackgroundTasks
# Но нужно создавать новую сессию для каждого таска
pending_files.append(audio.filename)
started_count += 1
# Запускаем обработку в фоне
# Используем lambda для захвата audio_id
background_tasks.add_task(
process_single_audio,
str(audio.id)
)
logger.info(f"✅ Запущено распознавание для {started_count} файлов")
if skipped_duplicates > 0:
logger.info(f"⏭️ Пропущено дубликатов: {skipped_duplicates}")
return {
"started_count": started_count,
"pending_files": pending_files,
"total_pending": total_pending
"total_pending": total_pending,
"skipped_duplicates": skipped_duplicates
}
except Exception as e:
@@ -211,15 +357,7 @@ async def get_pending_audio(
Список файлов, ожидающих распознавания
"""
try:
from sqlalchemy import exists
subquery = db.query(AiConclusion.audio_id).filter(
AiConclusion.audio_id == Audio.id
)
pending_audio = db.query(Audio).filter(
~exists().where(subquery.exists())
).order_by(Audio.index_date.asc()).limit(limit).all()
pending_audio = query_audio_without_conclusion(db, limit).all()
files_info = []
for audio in pending_audio:
@@ -234,9 +372,7 @@ async def get_pending_audio(
"exists_on_disk": exists
})
total_pending = db.query(Audio).filter(
~exists().where(subquery.exists())
).count()
total_pending = query_audio_without_conclusion(db).count()
return {
"total_pending": total_pending,
@@ -261,36 +397,55 @@ async def get_audio_stats(db: Session = Depends(get_db)):
Статистика по Audio записям
"""
try:
from sqlalchemy import exists
total_audio = db.query(Audio).count()
subquery = db.query(AiConclusion.audio_id).filter(
AiConclusion.audio_id == Audio.id
)
with_conclusion = db.query(Audio).filter(
exists().where(subquery.exists())
).count()
without_conclusion = total_audio - with_conclusion
with_conclusion = total_audio - query_audio_without_conclusion(db).count()
without_conclusion = query_audio_without_conclusion(db).count()
# Проверяем существование файлов на диске
all_audio = db.query(Audio).all()
existing_count = 0
missing_files = []
small_files = [] # Файлы меньше 1KB
for audio in all_audio:
file_path = os.path.join(AUDIOFILES_PATH, audio.filename)
if os.path.exists(file_path):
existing_count += 1
file_size = os.path.getsize(file_path)
if file_size < 1000:
small_files.append({
"audio_id": str(audio.id),
"filename": audio.filename,
"file_size": file_size,
"index_date": audio.index_date.isoformat() if audio.index_date else None
})
else:
missing_files.append({
"audio_id": str(audio.id),
"filename": audio.filename,
"index_date": audio.index_date.isoformat() if audio.index_date else None
})
return {
stats = {
"total_audio": total_audio,
"with_conclusion": with_conclusion,
"without_conclusion": without_conclusion,
"existing_on_disk": existing_count,
"missing_on_disk": total_audio - existing_count
"missing_on_disk": total_audio - existing_count,
"small_files_count": len(small_files)
}
# Добавляем списки проблемных файлов (первые 50 каждого типа)
if missing_files:
stats["missing_files_sample"] = missing_files[:50]
logger.warning(f"⚠️ Найдено {len(missing_files)} отсутствующих файлов")
if small_files:
stats["small_files_sample"] = small_files[:50]
logger.warning(f"⚠️ Найдено {len(small_files)} файлов меньше 1KB")
return stats
except Exception as e:
logger.error(f"❌ Ошибка при получении статистики: {e}")
raise HTTPException(
@@ -299,62 +454,71 @@ async def get_audio_stats(db: Session = Depends(get_db)):
)
def auto_restore_on_startup(db: Session, limit: int = 100):
@audio_management_router.delete("/audio/cleanup")
async def cleanup_invalid_audio_files(
delete_missing: bool = False,
delete_small: bool = True,
min_size_bytes: int = 1000,
db: Session = Depends(get_db)
):
"""
Автоматическое восстановление распознавания при старте FileAudioAPI
Удаляет записи Audio для проблемных файлов
Проверяет, есть ли файлы без AiConclusion, и запускает их распознавание
Query Parameters:
delete_missing: Удалять записи с отсутствующими файлами (default: False)
delete_small: Удалять записи с маленькими файлами (default: True)
min_size_bytes: Минимальный размер файла в bytes (default: 1000)
Args:
db: Сессия БД
limit: Максимум файлов для восстановления
Returns:
Статистику удаления
"""
try:
from sqlalchemy import or_
all_audio = db.session.query(Audio).all()
# Проверяем, есть ли файлы без AiConclusion
pending_audio = db.query(Audio).filter(
or_(
Audio.AiConclusion == None,
Audio.AiConclusion == ''
)
).limit(limit).all()
deleted_missing = 0
deleted_small = 0
if not pending_audio:
logger.info("️ Auto-restore: нет файлов для распознавания")
return
logger.info(f"🔄 Auto-restore: найдено {len(pending_audio)} файлов без AiConclusion")
# Запускаем распознавание
started_count = 0
for audio in pending_audio:
for audio in all_audio:
file_path = os.path.join(AUDIOFILES_PATH, audio.filename)
if not os.path.exists(file_path):
logger.warning(f"⚠️ Файл не найден: {audio.filename}")
# Проверяем отсутствие файла
if delete_missing and not os.path.exists(file_path):
logger.info(f"🗑️ Удаление записи с отсутствующим файлом: {audio.filename}")
db.session.delete(audio)
deleted_missing += 1
continue
# Отправляем в GigaAM API
from apiApp.config import GIGAAM_API_URL
api_url = f"{GIGAAM_API_URL}/api/call/process"
payload = {"filename": audio.filename}
# Проверяем размер файла
if delete_small and os.path.exists(file_path):
file_size = os.path.getsize(file_path)
if file_size < min_size_bytes:
logger.info(f"🗑️ Удаление записи с маленьким файлом ({file_size} bytes): {audio.filename}")
# Удаляем и файл тоже
try:
import requests
response = requests.post(api_url, json=payload, timeout=5)
os.remove(file_path)
logger.info(f" Файл удалён: {file_path}")
except Exception as e:
logger.warning(f" Не удалось удалить файл: {e}")
if response.status_code in [200, 202]:
logger.info(f"✅ Запущено распознавание: {audio.filename}")
started_count += 1
else:
logger.warning(f"⚠️ Ошибка запуска {audio.filename}: {response.status_code}")
db.session.delete(audio)
deleted_small += 1
db.session.commit()
return {
"success": True,
"deleted_missing": deleted_missing,
"deleted_small": deleted_small,
"total_deleted": deleted_missing + deleted_small,
"message": f"Удалено {deleted_missing + deleted_small} записей"
}
except Exception as e:
logger.error(f"❌ Ошибка при запуске {audio.filename}: {e}")
db.session.rollback()
logger.error(f"❌ Ошибка при очистке: {e}")
raise HTTPException(
status_code=500,
detail=str(e)
)
logger.info(f"🎉 Auto-restore завершено: запущено {started_count} файлов")
except Exception as e:
logger.error(f"❌ Ошибка при auto-restore: {e}")
@@ -1,393 +0,0 @@
"""
API endpoints для управления аудиофайлами (регистрация и пакетная обработка)
Используется Calls_WEB_Client_main для оркестрации процесса распознавания
"""
from fastapi import APIRouter, HTTPException, Depends, BackgroundTasks
from sqlalchemy.orm import Session
from pydantic import BaseModel
from typing import Optional, List
import os
import logging
from datetime import datetime
from apiApp.database import get_db
from apiApp.database.Audio import Audio
from apiApp.config import AUDIOFILES_PATH
logger = logging.getLogger(__name__)
audio_management_router = APIRouter()
class AudioRegisterRequest(BaseModel):
"""Запрос на регистрацию аудиофайла"""
filename: str
file_path: str # Полный путь к файлу в общей папке audiofiles
class AudioProcessAllRequest(BaseModel):
"""Запрос на пакетное распознавание"""
limit: int = 100
class AudioRegisterResponse(BaseModel):
"""Ответ на регистрацию аудиофайла"""
id: str
filename: str
file_size: int
created_at: datetime
@audio_management_router.post("/audio/register", response_model=AudioRegisterResponse, status_code=201)
async def register_audio_file(
request: AudioRegisterRequest,
db: Session = Depends(get_db)
):
"""
Регистрация аудиофайла в БД (без копирования файла)
Создаёт запись в таблице Audio для файла, который уже находится
в общей папке audiofiles. НЕ копирует файл, только создаёт запись в БД.
Args:
request: {filename: "in-xxx.wav", file_path: "/app/audiofiles/in-xxx.wav"}
Returns:
201 Created + информация о созданной записи
400 Bad Request если файл уже зарегистрирован
404 Not Found если файл не существует на диске
"""
try:
filename = request.filename
file_path = request.file_path
logger.info(f"📝 Регистрация файла: {filename}")
# Проверяем, что файл уже существует в общей папке
if not os.path.exists(file_path):
logger.error(f"❌ Файл не найден: {file_path}")
raise HTTPException(
status_code=404,
detail=f'Файл не найден на диске: {file_path}'
)
# Проверяем, что файл не был уже зарегистрирован
existing_audio = db.query(Audio).filter(Audio.filename == filename).first()
if existing_audio:
logger.warning(f"⚠️ Файл уже зарегистрирован: {filename}")
raise HTTPException(
status_code=400,
detail=f'Файл {filename} уже зарегистрирован в БД'
)
# Получаем размер файла
file_size = os.path.getsize(file_path)
# Создаём запись в БД
audio = Audio()
audio.filename = filename
audio.file_size = file_size
audio.index_date = datetime.utcnow()
db.add(audio)
db.commit()
db.refresh(audio)
logger.info(f"✅ Файл зарегистрирован: {filename} (audio_id={audio.id})")
return AudioRegisterResponse(
id=str(audio.id),
filename=audio.filename,
file_size=audio.file_size,
created_at=audio.index_date
)
except HTTPException:
raise
except Exception as e:
db.rollback()
logger.error(f"❌ Ошибка при регистрации файла: {e}")
raise HTTPException(
status_code=500,
detail=f'Ошибка при регистрации: {str(e)}'
)
def process_audio_file(audio_id: str, db: Session):
"""
Фоновая обработка одного аудиофайла
Отправляет запрос в GigaAM API для распознавания
"""
try:
audio = db.query(Audio).filter(Audio.id == audio_id).first()
if not audio:
logger.error(f"❌ Audio {audio_id} не найден")
return
logger.info(f"🎵 Запуск распознавания для {audio.filename}")
# Формируем запрос в GigaAM API
from apiApp.config import GIGAAM_API_URL
api_url = f"{GIGAAM_API_URL}/api/call/process"
payload = {
"filename": audio.filename
}
# Отправляем запрос в GigaAM API
import requests
response = requests.post(api_url, json=payload, timeout=10)
if response.status_code == 200 or response.status_code == 202:
logger.info(f"✅ Запущено распознавание для {audio.filename}")
else:
logger.error(f"❌ Ошибка запуска распознавания для {audio.filename}: {response.status_code} - {response.text}")
except Exception as e:
logger.error(f"❌ Ошибка при обработке {audio_id}: {e}")
@audio_management_router.post("/audio/process-all")
async def process_all_pending_audio(
request: AudioProcessAllRequest,
background_tasks: BackgroundTasks,
db: Session = Depends(get_db)
):
"""
Запуск распознавания для всех Audio без AiConclusion
Находит все записи Audio, у которых нет AiConclusion, и запускает
распознавание для них (до указанного лимита).
Args:
request: {limit: 100} - максимум файлов для обработки
Returns:
200 OK + {
"started_count": 15,
"pending_files": ["file1.wav", "file2.wav", ...],
"total_pending": 50
}
"""
try:
limit = request.limit
logger.info(f"🚀 Поиск Audio без AiConclusion (limit={limit})")
# Находим все Audio без AiConclusion
pending_audio = db.query(Audio).filter(
Audio.AiConclusion == None
).order_by(Audio.index_date.asc()).limit(limit).all()
total_pending = db.query(Audio).filter(Audio.AiConclusion == None).count()
if not pending_audio:
logger.info("️ Нет файлов для распознавания")
return {
"started_count": 0,
"pending_files": [],
"total_pending": 0,
"message": "Нет файлов без AiConclusion"
}
logger.info(f"📋 Найдено файлов для обработки: {len(pending_audio)} из {total_pending}")
# Добавляем задачи в фон
started_count = 0
pending_files = []
for audio in pending_audio:
# Проверяем, что файл существует
file_path = os.path.join(AUDIOFILES_PATH, audio.filename)
if not os.path.exists(file_path):
logger.warning(f"⚠️ Файл не найден на диске: {audio.filename}")
continue
# Добавляем в фон (асинхронно)
# В FastAPI используем BackgroundTasks
# Но нужно создавать новую сессию для каждого таска
pending_files.append(audio.filename)
started_count += 1
# Запускаем обработку в фоне
# Используем lambda для захвата audio_id
background_tasks.add_task(
process_single_audio,
str(audio.id)
)
logger.info(f"✅ Запущено распознавание для {started_count} файлов")
return {
"started_count": started_count,
"pending_files": pending_files,
"total_pending": total_pending
}
except Exception as e:
logger.error(f"❌ Ошибка при запуске пакетного распознавания: {e}")
raise HTTPException(
status_code=500,
detail=f'Ошибка: {str(e)}'
)
def process_single_audio(audio_id: str):
"""
Обработка одного аудиофайла в фоне
Создаёт новую DB сессию для обработки
"""
from apiApp.database import SessionLocal
db = SessionLocal()
try:
process_audio_file(audio_id, db)
finally:
db.close()
@audio_management_router.get("/audio/pending")
async def get_pending_audio(
db: Session = Depends(get_db),
limit: int = 100
):
"""
Получить список Audio без AiConclusion
Query Parameters:
limit: Максимум файлов (default: 100)
Returns:
Список файлов, ожидающих распознавания
"""
try:
pending_audio = db.query(Audio).filter(
Audio.AiConclusion == None
).order_by(Audio.index_date.asc()).limit(limit).all()
files_info = []
for audio in pending_audio:
file_path = os.path.join(AUDIOFILES_PATH, audio.filename)
exists = os.path.exists(file_path)
files_info.append({
"audio_id": str(audio.id),
"filename": audio.filename,
"file_size": audio.file_size,
"created_at": audio.index_date.isoformat() if audio.index_date else None,
"exists_on_disk": exists
})
total_pending = db.query(Audio).filter(Audio.AiConclusion == None).count()
return {
"total_pending": total_pending,
"count": len(files_info),
"files": files_info
}
except Exception as e:
logger.error(f"❌ Ошибка при получении списка: {e}")
raise HTTPException(
status_code=500,
detail=str(e)
)
@audio_management_router.get("/audio/stats")
async def get_audio_stats(db: Session = Depends(get_db)):
"""
Получить статистику по аудиофайлам
Returns:
Статистика по Audio записям
"""
try:
total_audio = db.query(Audio).count()
with_conclusion = db.query(Audio).filter(Audio.AiConclusion != None).count()
without_conclusion = db.query(Audio).filter(Audio.AiConclusion == None).count()
# Проверяем существование файлов на диске
all_audio = db.query(Audio).all()
existing_count = 0
for audio in all_audio:
file_path = os.path.join(AUDIOFILES_PATH, audio.filename)
if os.path.exists(file_path):
existing_count += 1
return {
"total_audio": total_audio,
"with_conclusion": with_conclusion,
"without_conclusion": without_conclusion,
"existing_on_disk": existing_count,
"missing_on_disk": total_audio - existing_count
}
except Exception as e:
logger.error(f"❌ Ошибка при получении статистики: {e}")
raise HTTPException(
status_code=500,
detail=str(e)
)
def auto_restore_on_startup(db: Session, limit: int = 100):
"""
Автоматическое восстановление распознавания при старте FileAudioAPI
Проверяет, есть ли файлы без AiConclusion, и запускает их распознавание
Args:
db: Сессия БД
limit: Максимум файлов для восстановления
"""
try:
from sqlalchemy import or_
# Проверяем, есть ли файлы без AiConclusion
pending_audio = db.query(Audio).filter(
or_(
Audio.AiConclusion == None,
Audio.AiConclusion == ''
)
).limit(limit).all()
if not pending_audio:
logger.info("️ Auto-restore: нет файлов для распознавания")
return
logger.info(f"🔄 Auto-restore: найдено {len(pending_audio)} файлов без AiConclusion")
# Запускаем распознавание
started_count = 0
for audio in pending_audio:
file_path = os.path.join(AUDIOFILES_PATH, audio.filename)
if not os.path.exists(file_path):
logger.warning(f"⚠️ Файл не найден: {audio.filename}")
continue
# Отправляем в GigaAM API
from apiApp.config import GIGAAM_API_URL
api_url = f"{GIGAAM_API_URL}/api/call/process"
payload = {"filename": audio.filename}
try:
import requests
response = requests.post(api_url, json=payload, timeout=5)
if response.status_code in [200, 202]:
logger.info(f"✅ Запущено распознавание: {audio.filename}")
started_count += 1
else:
logger.warning(f"⚠️ Ошибка запуска {audio.filename}: {response.status_code}")
except Exception as e:
logger.error(f"❌ Ошибка при запуске {audio.filename}: {e}")
logger.info(f"🎉 Auto-restore завершено: запущено {started_count} файлов")
except Exception as e:
logger.error(f"❌ Ошибка при auto-restore: {e}")
+130
View File
@@ -0,0 +1,130 @@
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File as FastAPIFile, Form, status
from apiApp.database import get_db
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
import os, uuid
import logging
from pathlib import Path
import requests
from apiApp.config import ALLOWED_AUDIO_EXTENSIONS, MAX_UPLOAD_SIZE, AUDIOFILES_PATH, GIGAAM_API_URL
import aiofiles
from apiApp.schemas import AudioCreate
from apiApp.services import AudioCRUD
logger = logging.getLogger(__name__)
router = APIRouter(
prefix="/external_audio",
tags=["Внешние аудиофайлы"]
)
@router.post("/upload")
async def upload_external_audio(
file: UploadFile = FastAPIFile(...),
callback_url: str = Form(None, description="URL для отправки результата распознавания (опционально). Если передан — FileAudioAPI вызовет GigaAM и затем отправит результат на этот URL."),
db: Session = Depends(get_db)
):
"""
Загрузка внешнего аудиофайла. Файл сохраняется в общей папке.
Если передан callback_url автоматически отправляется на распознавание в GigaAM,
а результат придёт на callback_url.
"""
# Проверка расширения файла
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"
)
# Сохранение в общей папке под именем uuid+ext; это же имя передаём в GigaAM
safe_name = f"{uuid.uuid4()}{file_ext}"
upload_dir = Path(AUDIOFILES_PATH) if isinstance(AUDIOFILES_PATH, str) else AUDIOFILES_PATH
upload_dir.mkdir(parents=True, exist_ok=True)
file_path = upload_dir / safe_name
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)}"
)
# Создание записи в БД (filename = имя файла на диске, его же передаём в GigaAM)
try:
audio_data = AudioCreate(filename=safe_name)
audio = AudioCRUD.create(
db=db,
audio_data=audio_data,
file_path=str(file_path),
file_size=len(content),
sourse="external"
)
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)}"
)
# Всегда запускаем распознавание в GigaAM; callback_url передаём только если передан
cb = callback_url.strip() if (callback_url and callback_url.strip()) else None
task_id, recognition_error = send_to_recognition(filename=safe_name, callback_url=cb)
# Ответ: запись аудио + данные по постановке в очередь распознавания
result = {
"id": str(audio.id),
"filename": audio.filename,
"index_date": audio.index_date.isoformat() if audio.index_date else None,
"file_path": audio.file_path,
"file_size": audio.file_size,
"sourse": audio.sourse,
}
if task_id:
result["recognition_task_id"] = task_id
result["recognition_status_url"] = f"{GIGAAM_API_URL.rstrip('/')}/api/call/task/{task_id}"
if recognition_error:
result["recognition_error"] = recognition_error
return result
def send_to_recognition(filename: str, callback_url: str | None = None) -> tuple:
"""
Отправка аудиофайла на распознавание в GigaAM.
Файл должен уже лежать в общей папке под именем filename.
Returns:
(task_id или None, recognition_error или None)
"""
try:
gigaam_url = f"{GIGAAM_API_URL.rstrip('/')}/api/call/external/process"
payload = {"filename": filename}
if callback_url:
payload["callback_url"] = callback_url
resp = requests.post(
gigaam_url,
json=payload,
timeout=15,
)
if resp.status_code == 202:
data = resp.json()
task_id = data.get("task_id")
logger.info(f"✅ Внешний файл {filename} поставлен в очередь GigaAM, task_id={task_id}")
return (task_id, None)
recognition_error = resp.text or f"HTTP {resp.status_code}"
logger.warning(f"⚠️ GigaAM не принял задачу {filename}: {recognition_error}")
return (None, recognition_error)
except requests.exceptions.RequestException as e:
logger.warning(f"⚠️ Ошибка вызова GigaAM для {filename}: {e}")
return (None, str(e))
+9 -2
View File
@@ -26,13 +26,20 @@ class AudioCRUD:
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:
def create(
db: Session,
audio_data: AudioCreate,
file_path: str,
file_size: int = None,
sourse: str = "internal"
) -> Audio:
"""Создать новую запись аудиофайла"""
db_audio = Audio(
filename=audio_data.filename,
file_path=file_path,
index_date=datetime.datetime.utcnow(),
file_size=file_size
file_size=file_size,
sourse=sourse
)
db.add(db_audio)
db.commit()
+3
View File
@@ -11,3 +11,6 @@ DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./speech_analytics.db")
# GigaAM API
GIGAAM_API_URL = os.getenv("GIGAAM_API_URL", "http://localhost:5001")
#App Setting
PORT = 5008
+143
View File
@@ -0,0 +1,143 @@
#!/usr/bin/env python3
"""
Пример использования параллельной отправки файлов на распознавание
"""
from autoLoader.loader import RecognitionChecker
from autoLoader.database import get_db_session, Audio
from autoLoader.config import GIGAAM_API_URL
import logging
# Настройка логирования
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
def example_parallel_send():
"""Пример параллельной отправки"""
print("🚀 Пример параллельной отправки файлов\n")
# Создаём checker с настройкой количества потоков
checker = RecognitionChecker(
api_url=GIGAAM_API_URL,
max_workers=10 # До 10 параллельных запросов
)
# Проверяем доступность API
if not checker.check_api_availability():
print("❌ GigaAM API недоступен")
return
# Проверяем БД
if not checker.check_database():
print("❌ База данных не готова")
return
# Получаем файлы без заключения
print("🔍 Поиск файлов без заключения...")
files_without_conclusion = checker.get_files_without_conclusion(limit=20)
if not files_without_conclusion:
print("✅ Все файлы уже обработаны")
return
print(f"📊 Найдено файлов: {len(files_without_conclusion)}\n")
# Отправляем параллельно
print("🚀 Начинаем параллельную отправку...\n")
results = checker.send_to_recognition_parallel(files_without_conclusion)
# Выводим результаты
print(f"\n{'='*60}")
print(f"📊 РЕЗУЛЬТАТЫ:")
print(f"{'='*60}")
print(f"Всего файлов: {results['total']}")
print(f"✅ Отправлено: {results['sent']}")
print(f"❌ Ошибок: {results['failed']}")
# Детали по каждому файлу
if results.get('files'):
print(f"\n📋 Детали:")
for file_result in results['files']:
status = "" if file_result['success'] else ""
print(f" {status} {file_result['filename']}")
print(f"{'='*60}\n")
def example_sequential_send():
"""Пример последовательной отправки (для сравнения)"""
print("📤 Пример последовательной отправки файлов\n")
checker = RecognitionChecker(api_url=GIGAAM_API_URL)
if not checker.check_api_availability():
print("❌ GigaAM API недоступен")
return
# Получаем файлы
files_without_conclusion = checker.get_files_without_conclusion(limit=5)
if not files_without_conclusion:
print("✅ Все файлы уже обработаны")
return
print(f"📊 Найдено файлов: {len(files_without_conclusion)}\n")
# Отправляем последовательно (parallel=False)
results = checker.process_all_pending(limit=5, parallel=False)
print(f"\n{'='*60}")
print(f"📊 РЕЗУЛЬТАТЫ:")
print(f"{'='*60}")
print(f"Всего файлов: {results['total']}")
print(f"✅ Отправлено: {results['sent']}")
print(f"❌ Ошибок: {results['failed']}")
print(f"{'='*60}\n")
def example_send_specific_files():
"""Пример отправки конкретных файлов"""
print("🎯 Пример отправки конкретных файлов\n")
checker = RecognitionChecker(max_workers=5)
# Получаем конкретные файлы из БД
with get_db_session() as db:
# Например, последние 10 файлов
audio_files = db.query(Audio).order_by(Audio.index_date.desc()).limit(10).all()
if not audio_files:
print("⏭️ Файлы не найдены")
return
print(f"📊 Выбрано файлов: {len(audio_files)}\n")
# Отправляем параллельно
results = checker.send_to_recognition_parallel(audio_files)
print(f"\n✅ Отправлено: {results['sent']} из {results['total']}\n")
if __name__ == "__main__":
import sys
# Выбор примера
if len(sys.argv) > 1:
example_type = sys.argv[1]
else:
print("Выберите пример:")
print(" python example_parallel_send.py parallel - Параллельная отправка")
print(" python example_parallel_send.py sequential - Последовательная отправка")
print(" python example_parallel_send.py specific - Отправка конкретных файлов")
print("\nИспользование по умолчанию: parallel\n")
example_type = "parallel"
if example_type == "parallel":
example_parallel_send()
elif example_type == "sequential":
example_sequential_send()
elif example_type == "specific":
example_send_specific_files()
else:
print(f"❌ Неизвестный пример: {example_type}")
+5 -3
View File
@@ -11,6 +11,7 @@ from apiApp.routers import audio_router, recognition_router
from apiApp.routers.ai_conclusion_router import ai_conclusion_router
from apiApp.routers.audio_files_router import audio_files_router
from apiApp.routers.audio_management_router import audio_management_router
from apiApp.routers.external_audio import router as external_audio_router
print("✅ audio_management_router imported successfully")
@@ -63,10 +64,11 @@ app.include_router(audio_router, prefix=API_V1_PREFIX, tags=["audio"])
app.include_router(recognition_router, prefix=API_V1_PREFIX, tags=["recognition"])
app.include_router(ai_conclusion_router, prefix=API_V1_PREFIX, tags=["ai_conclusion"])
app.include_router(audio_files_router, prefix=API_V1_PREFIX, tags=["audio_files"])
# audio_management_router без префикса для совместимости с вызовами из Calls_WEB_Client_main
app.include_router(external_audio_router, prefix=API_V1_PREFIX)
# audio_management_router с префиксом /audio для логической структуры
print("📝 Registering audio_management_router...")
app.include_router(audio_management_router, tags=["audio_management"])
print("✅ audio_management_router registered at /api/audio/*")
app.include_router(audio_management_router, prefix="/api", tags=["audio_management"])
print("✅ audio_management_router registered at /audio/*")
# Статические файлы (для загрузки аудио)
app.mount("/uploads", StaticFiles(directory=str(UPLOAD_FOLDER)), name="uploads")
+1
View File
@@ -6,3 +6,4 @@ python-multipart==0.0.12
aiofiles==24.1.0
psycopg2-binary
paramiko
requests
+3 -2
View File
@@ -5,6 +5,7 @@
import uvicorn
import sys
from pathlib import Path
from apiApp.config import PORT
# >102;O5< :>@=52CN 48@5:B>@8N 2 Python path
sys.path.insert(0, str(Path(__file__).resolve().parent))
@@ -13,7 +14,7 @@ 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
port=PORT,
reload=False, # 2B><0B8G5A:0O ?5@5703@C7:0 ?@8 87<5=5=88 :>40
log_level="info"
)