Compare commits
32 Commits
de2e66a37c
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 4dca198d3c | |||
| fe0bed7f58 | |||
| 94633288c3 | |||
| 15925c0217 | |||
| e161681bc9 | |||
| 5360ea7f9c | |||
| d460728b2e | |||
| 8e1175c9bf | |||
| 014271e929 | |||
| ae4cfa0119 | |||
| db62f9ef74 | |||
| bf507b5363 | |||
| 9fc60e8d19 | |||
| f3e3da1696 | |||
| b8da32f44a | |||
| 688417990f | |||
| e31440f91e | |||
| 163b019b31 | |||
| cf25f364d1 | |||
| 34a31cc4d6 | |||
| ebef51aa48 | |||
| 013dd9a7d4 | |||
| c1a9ef27f1 | |||
| ac8e881ee3 | |||
| 21dafb5681 | |||
| 9f65a9398a | |||
| b8af963e9f | |||
| c4dec7644d | |||
| 2df1761d34 | |||
| 59b5676c84 | |||
| ddbebea001 | |||
| 86245ce1fb |
+3
-2
@@ -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"]
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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"]
|
||||
|
||||
@@ -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,31 +159,41 @@ 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:
|
||||
logger.info(f"📤 Отправка webhook в Calls_WEB_Client_main для {request.filename}")
|
||||
if (audio.sourse or "").lower() != "external":
|
||||
logger.info(f"📤 Отправка webhook в Calls_WEB_Client_main для {request.filename}")
|
||||
|
||||
webhook_payload = {
|
||||
"audio_id": str(audio.id),
|
||||
"filename": request.filename,
|
||||
"transcription": request.transcription,
|
||||
"corrected_transcription": request.corrected_transcription,
|
||||
"segments": request.segments,
|
||||
"processing_time_seconds": request.processing_time_seconds
|
||||
}
|
||||
webhook_payload = {
|
||||
"audio_id": str(audio.id),
|
||||
"filename": request.filename,
|
||||
"transcription": request.transcription,
|
||||
"corrected_transcription": request.corrected_transcription,
|
||||
"segments": request.segments,
|
||||
"processing_time_seconds": request.processing_time_seconds
|
||||
}
|
||||
|
||||
webhook_response = requests.post(
|
||||
WEBHOOK_ENDPOINT,
|
||||
json=webhook_payload,
|
||||
headers={"X-Webhook-Key": WEBHOOK_API_KEY},
|
||||
timeout=30
|
||||
)
|
||||
webhook_response = requests.post(
|
||||
WEBHOOK_ENDPOINT,
|
||||
json=webhook_payload,
|
||||
headers={"X-Webhook-Key": WEBHOOK_API_KEY},
|
||||
timeout=30
|
||||
)
|
||||
|
||||
if webhook_response.status_code == 200:
|
||||
logger.info(f"✅ Webhook успешно отправлен для {request.filename}")
|
||||
else:
|
||||
logger.warning(f"⚠️ Webhook вернул статус {webhook_response.status_code}")
|
||||
logger.warning(f"Response: {webhook_response.text}")
|
||||
if webhook_response.status_code == 200:
|
||||
logger.info(f"✅ Webhook успешно отправлен для {request.filename}")
|
||||
else:
|
||||
logger.warning(f"⚠️ Webhook вернул статус {webhook_response.status_code}")
|
||||
logger.warning(f"Response: {webhook_response.text}")
|
||||
|
||||
except requests.exceptions.ConnectionError:
|
||||
logger.error(f"❌ Не удалось подключиться к Calls_WEB_Client_main webhook: {WEBHOOK_ENDPOINT}")
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
response = requests.post(api_url, json=payload, timeout=10)
|
||||
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}")
|
||||
else:
|
||||
logger.error(f"❌ Ошибка запуска распознавания для {audio.filename}: {response.status_code} - {response.text}")
|
||||
if response.status_code == 200 or response.status_code == 202:
|
||||
result = response.json()
|
||||
task_id = result.get('task_id')
|
||||
logger.info(f"✅ Задача добавлена в очередь для {audio.filename} (task_id: {task_id})")
|
||||
else:
|
||||
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"
|
||||
# Проверяем размер файла
|
||||
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:
|
||||
os.remove(file_path)
|
||||
logger.info(f" Файл удалён: {file_path}")
|
||||
except Exception as e:
|
||||
logger.warning(f" Не удалось удалить файл: {e}")
|
||||
|
||||
payload = {"filename": audio.filename}
|
||||
db.session.delete(audio)
|
||||
deleted_small += 1
|
||||
|
||||
try:
|
||||
import requests
|
||||
response = requests.post(api_url, json=payload, timeout=5)
|
||||
db.session.commit()
|
||||
|
||||
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} файлов")
|
||||
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"❌ Ошибка при auto-restore: {e}")
|
||||
db.session.rollback()
|
||||
logger.error(f"❌ Ошибка при очистке: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=str(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}")
|
||||
@@ -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))
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
Executable
+143
@@ -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}")
|
||||
@@ -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")
|
||||
|
||||
@@ -6,3 +6,4 @@ python-multipart==0.0.12
|
||||
aiofiles==24.1.0
|
||||
psycopg2-binary
|
||||
paramiko
|
||||
requests
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user