Compare commits

...

34 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
poturaevpetr de2e66a37c add autorestart rec 2025-12-30 00:25:37 +05:00
poturaevpetr ba93ac1970 add autorestart rec 2025-12-30 00:25:29 +05:00
16 changed files with 901 additions and 110 deletions
+3 -2
View File
@@ -32,7 +32,8 @@ RUN mkdir -p /app/uploads
RUN mkdir -p /app/data 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
Сервис для хранения файлов аудио, индексации файлов, записи и выдачи результатов распознавания ## Назначение и роль в экосистеме
**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_TITLE = "Speech Analytics API"
APP_VERSION = "1.0.0" APP_VERSION = "1.0.0"
PORT = int(os.getenv("PORT", "8000")) PORT = int(os.getenv("PORT", "5008"))
HOST = os.getenv("HOST", "localhost") HOST = os.getenv("HOST", "localhost")
# GigaAM API Configuration # 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_main Webhook Configuration
CALLS_WEB_CLIENT_URL = os.getenv( CALLS_WEB_CLIENT_URL = os.getenv(
"CALLS_WEB_CLIENT_URL", "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_ENDPOINT = f"{CALLS_WEB_CLIENT_URL}/api/transcription/webhook"
WEBHOOK_API_KEY = os.getenv("WEBHOOK_API_KEY", "webhook_secret_key") 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" ENABLE_AUTO_RESTORE = os.getenv("ENABLE_AUTO_RESTORE", "true").lower() == "true"
AUTO_RESTORE_LIMIT = int(os.getenv("AUTO_RESTORE_LIMIT", "100")) # Максимум файлов для восстановления AUTO_RESTORE_LIMIT = int(os.getenv("AUTO_RESTORE_LIMIT", "100")) # Максимум файлов для восстановления
AUTO_RESTORE_DELAY = int(os.getenv("AUTO_RESTORE_DELAY", "5")) # Задержка перед запуском (секунды) 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
engine = create_engine( engine = create_engine(
DATABASE_URL, 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 # SessionLocal
+11 -1
View File
@@ -14,6 +14,11 @@ class Audio(Base):
file_path = Column(Text) file_path = Column(Text)
duration = Column(Float) duration = Column(Float)
file_size = Column(Integer) 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") 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, "index_date": self.index_date.isoformat() if self.index_date else None,
"file_path": self.file_path, "file_path": self.file_path,
"duration": self.duration, "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.audio import router as audio_router
from apiApp.routers.recognition import router as recognition_router from apiApp.routers.recognition import router as recognition_router
from apiApp.routers.external_audio import router as external_audio_router
__all__ = ["audio_router", "recognition_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__) logger = logging.getLogger(__name__)
ai_conclusion_router = APIRouter() 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): class AiConclusionRequest(BaseModel):
"""Модель запроса для сохранения AI заключения""" """Модель запроса для сохранения AI заключения"""
@@ -25,6 +50,7 @@ class AiConclusionRequest(BaseModel):
analysis: Dict[str, Any] analysis: Dict[str, Any]
segments: Optional[List[Dict[str, Any]]] = [] segments: Optional[List[Dict[str, Any]]] = []
processing_time_seconds: Optional[float] = 0 processing_time_seconds: Optional[float] = 0
callback_url: Optional[str] = None
class AiConclusionResponse(BaseModel): class AiConclusionResponse(BaseModel):
@@ -36,6 +62,46 @@ class AiConclusionResponse(BaseModel):
error: Optional[str] = None 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) @ai_conclusion_router.post("/conclusion/save", response_model=AiConclusionResponse)
async def save_ai_conclusion(request: AiConclusionRequest, db: Session = Depends(get_db)): 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() db.commit()
logger.info(f"✅ Заключение сохранено для {request.filename}") 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 для анализа # Отправляем webhook в Calls_WEB_Client_main для анализа
try: try:
if (audio.sourse or "").lower() != "external":
logger.info(f"📤 Отправка webhook в Calls_WEB_Client_main для {request.filename}") logger.info(f"📤 Отправка webhook в Calls_WEB_Client_main для {request.filename}")
webhook_payload = { webhook_payload = {
@@ -142,3 +218,25 @@ async def save_ai_conclusion(request: AiConclusionRequest, db: Session = Depends
status_code=500, status_code=500,
detail=str(e) 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
)
+32
View File
@@ -0,0 +1,32 @@
"""
Исправленная версия для проверки Audio без AiConclusion
"""
from sqlalchemy import exists
def get_audio_without_conclusion(db, limit=100):
"""
Находит все Audio, у которых нет AiConclusion
Использует подзапрос через exists, так как AiConclusion - это relationship
"""
# Импортируем модели
from apiApp.database.Audio import Audio
from apiApp.database.AiConclusion import AiConclusion
# Создаём подзапрос для проверки наличия AiConclusion
subquery = db.query(AiConclusion.audio_id).filter(
AiConclusion.audio_id == Audio.id
)
# Находим Audio без AiConclusion
pending_audio = db.query(Audio).filter(
~exists().where(subquery.exists())
).order_by(Audio.index_date.asc()).limit(limit).all()
# Считаем total
total_pending = db.query(Audio).filter(
~exists().where(subquery.exists())
).count()
return pending_audio, total_pending
+198 -67
View File
@@ -12,12 +12,41 @@ from datetime import datetime
from apiApp.database import get_db from apiApp.database import get_db
from apiApp.database.Audio import Audio from apiApp.database.Audio import Audio
from apiApp.config import AUDIOFILES_PATH from apiApp.database.AiConclusion import AiConclusion
from apiApp.config import AUDIOFILES_PATH, MAX_RECOGNITION_ATTEMPTS
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
audio_management_router = APIRouter() 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): class AudioRegisterRequest(BaseModel):
"""Запрос на регистрацию аудиофайла""" """Запрос на регистрацию аудиофайла"""
filename: str filename: str
@@ -87,6 +116,10 @@ async def register_audio_file(
audio.filename = filename audio.filename = filename
audio.file_size = file_size audio.file_size = file_size
audio.index_date = datetime.utcnow() 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.add(audio)
db.commit() db.commit()
@@ -125,23 +158,64 @@ def process_audio_file(audio_id: str, db: Session):
return return
logger.info(f"🎵 Запуск распознавания для {audio.filename}") 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 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 = { payload = {
"filename": audio.filename "filename": audio.filename
} }
# Отправляем запрос в GigaAM API
import requests import requests
try:
# Отправляем запрос в асинхронный endpoint
response = requests.post(api_url, json=payload, timeout=10) response = requests.post(api_url, json=payload, timeout=10)
if response.status_code == 200 or response.status_code == 202: 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: 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: except Exception as e:
logger.error(f"❌ Ошибка при обработке {audio_id}: {e}") logger.error(f"❌ Ошибка при обработке {audio_id}: {e}")
@@ -166,7 +240,8 @@ async def process_all_pending_audio(
200 OK + { 200 OK + {
"started_count": 15, "started_count": 15,
"pending_files": ["file1.wav", "file2.wav", ...], "pending_files": ["file1.wav", "file2.wav", ...],
"total_pending": 50 "total_pending": 50,
"skipped_duplicates": 2
} }
""" """
try: try:
@@ -174,12 +249,9 @@ async def process_all_pending_audio(
logger.info(f"🚀 Поиск Audio без AiConclusion (limit={limit})") logger.info(f"🚀 Поиск Audio без AiConclusion (limit={limit})")
# Находим все Audio без AiConclusion # Находим все Audio без AiConclusion используя вспомогательную функцию
pending_audio = db.query(Audio).filter( pending_audio = query_audio_without_conclusion(db, limit).all()
Audio.AiConclusion == None total_pending = query_audio_without_conclusion(db).count()
).order_by(Audio.index_date.asc()).limit(limit).all()
total_pending = db.query(Audio).filter(Audio.AiConclusion == None).count()
if not pending_audio: if not pending_audio:
logger.info("ℹ️ Нет файлов для распознавания") logger.info("ℹ️ Нет файлов для распознавания")
@@ -187,6 +259,7 @@ async def process_all_pending_audio(
"started_count": 0, "started_count": 0,
"pending_files": [], "pending_files": [],
"total_pending": 0, "total_pending": 0,
"skipped_duplicates": 0,
"message": "Нет файлов без AiConclusion" "message": "Нет файлов без AiConclusion"
} }
@@ -195,6 +268,7 @@ async def process_all_pending_audio(
# Добавляем задачи в фон # Добавляем задачи в фон
started_count = 0 started_count = 0
pending_files = [] pending_files = []
skipped_duplicates = 0 # Счётчик дубликатов
for audio in pending_audio: for audio in pending_audio:
# Проверяем, что файл существует # Проверяем, что файл существует
@@ -203,25 +277,46 @@ async def process_all_pending_audio(
logger.warning(f"⚠️ Файл не найден на диске: {audio.filename}") logger.warning(f"⚠️ Файл не найден на диске: {audio.filename}")
continue 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) pending_files.append(audio.filename)
started_count += 1 started_count += 1
# Запускаем обработку в фоне # Запускаем обработку в фоне
# Используем lambda для захвата audio_id
background_tasks.add_task( background_tasks.add_task(
process_single_audio, process_single_audio,
str(audio.id) str(audio.id)
) )
logger.info(f"✅ Запущено распознавание для {started_count} файлов") logger.info(f"✅ Запущено распознавание для {started_count} файлов")
if skipped_duplicates > 0:
logger.info(f"⏭️ Пропущено дубликатов: {skipped_duplicates}")
return { return {
"started_count": started_count, "started_count": started_count,
"pending_files": pending_files, "pending_files": pending_files,
"total_pending": total_pending "total_pending": total_pending,
"skipped_duplicates": skipped_duplicates
} }
except Exception as e: except Exception as e:
@@ -262,9 +357,7 @@ async def get_pending_audio(
Список файлов, ожидающих распознавания Список файлов, ожидающих распознавания
""" """
try: try:
pending_audio = db.query(Audio).filter( pending_audio = query_audio_without_conclusion(db, limit).all()
Audio.AiConclusion == None
).order_by(Audio.index_date.asc()).limit(limit).all()
files_info = [] files_info = []
for audio in pending_audio: for audio in pending_audio:
@@ -279,7 +372,7 @@ async def get_pending_audio(
"exists_on_disk": exists "exists_on_disk": exists
}) })
total_pending = db.query(Audio).filter(Audio.AiConclusion == None).count() total_pending = query_audio_without_conclusion(db).count()
return { return {
"total_pending": total_pending, "total_pending": total_pending,
@@ -305,25 +398,54 @@ async def get_audio_stats(db: Session = Depends(get_db)):
""" """
try: try:
total_audio = db.query(Audio).count() total_audio = db.query(Audio).count()
with_conclusion = db.query(Audio).filter(Audio.AiConclusion != None).count() with_conclusion = total_audio - query_audio_without_conclusion(db).count()
without_conclusion = db.query(Audio).filter(Audio.AiConclusion == None).count() without_conclusion = query_audio_without_conclusion(db).count()
# Проверяем существование файлов на диске # Проверяем существование файлов на диске
all_audio = db.query(Audio).all() all_audio = db.query(Audio).all()
existing_count = 0 existing_count = 0
missing_files = []
small_files = [] # Файлы меньше 1KB
for audio in all_audio: for audio in all_audio:
file_path = os.path.join(AUDIOFILES_PATH, audio.filename) file_path = os.path.join(AUDIOFILES_PATH, audio.filename)
if os.path.exists(file_path): if os.path.exists(file_path):
existing_count += 1 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, "total_audio": total_audio,
"with_conclusion": with_conclusion, "with_conclusion": with_conclusion,
"without_conclusion": without_conclusion, "without_conclusion": without_conclusion,
"existing_on_disk": existing_count, "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: except Exception as e:
logger.error(f"❌ Ошибка при получении статистики: {e}") logger.error(f"❌ Ошибка при получении статистики: {e}")
raise HTTPException( raise HTTPException(
@@ -332,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: Returns:
db: Сессия БД Статистику удаления
limit: Максимум файлов для восстановления
""" """
try: try:
from sqlalchemy import or_ all_audio = db.session.query(Audio).all()
# Проверяем, есть ли файлы без AiConclusion deleted_missing = 0
pending_audio = db.query(Audio).filter( deleted_small = 0
or_(
Audio.AiConclusion == None,
Audio.AiConclusion == ''
)
).limit(limit).all()
if not pending_audio: for audio in all_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) 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 continue
# Отправляем в GigaAM API # Проверяем размер файла
from apiApp.config import GIGAAM_API_URL if delete_small and os.path.exists(file_path):
api_url = f"{GIGAAM_API_URL}/api/call/process" file_size = os.path.getsize(file_path)
if file_size < min_size_bytes:
payload = {"filename": audio.filename} logger.info(f"🗑️ Удаление записи с маленьким файлом ({file_size} bytes): {audio.filename}")
# Удаляем и файл тоже
try: try:
import requests os.remove(file_path)
response = requests.post(api_url, json=payload, timeout=5) logger.info(f" Файл удалён: {file_path}")
except Exception as e:
logger.warning(f" Не удалось удалить файл: {e}")
if response.status_code in [200, 202]: db.session.delete(audio)
logger.info(f"✅ Запущено распознавание: {audio.filename}") deleted_small += 1
started_count += 1
else: db.session.commit()
logger.warning(f"⚠️ Ошибка запуска {audio.filename}: {response.status_code}")
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: 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}")
+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() return db.query(Audio).filter(Audio.filename == filename).first()
@staticmethod @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( db_audio = Audio(
filename=audio_data.filename, filename=audio_data.filename,
file_path=file_path, file_path=file_path,
index_date=datetime.datetime.utcnow(), index_date=datetime.datetime.utcnow(),
file_size=file_size file_size=file_size,
sourse=sourse
) )
db.add(db_audio) db.add(db_audio)
db.commit() db.commit()
+3
View File
@@ -11,3 +11,6 @@ DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./speech_analytics.db")
# GigaAM API # GigaAM API
GIGAAM_API_URL = os.getenv("GIGAAM_API_URL", "http://localhost:5001") 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.ai_conclusion_router import ai_conclusion_router
from apiApp.routers.audio_files_router import audio_files_router from apiApp.routers.audio_files_router import audio_files_router
from apiApp.routers.audio_management_router import audio_management_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") 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(recognition_router, prefix=API_V1_PREFIX, tags=["recognition"])
app.include_router(ai_conclusion_router, prefix=API_V1_PREFIX, tags=["ai_conclusion"]) 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"]) 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...") print("📝 Registering audio_management_router...")
app.include_router(audio_management_router, tags=["audio_management"]) app.include_router(audio_management_router, prefix="/api", tags=["audio_management"])
print("✅ audio_management_router registered at /api/audio/*") print("✅ audio_management_router registered at /audio/*")
# Статические файлы (для загрузки аудио) # Статические файлы (для загрузки аудио)
app.mount("/uploads", StaticFiles(directory=str(UPLOAD_FOLDER)), name="uploads") 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 aiofiles==24.1.0
psycopg2-binary psycopg2-binary
paramiko paramiko
requests
+3 -2
View File
@@ -5,6 +5,7 @@
import uvicorn import uvicorn
import sys import sys
from pathlib import Path from pathlib import Path
from apiApp.config import PORT
# >102;O5< :>@=52CN 48@5:B>@8N 2 Python path # >102;O5< :>@=52CN 48@5:B>@8N 2 Python path
sys.path.insert(0, str(Path(__file__).resolve().parent)) sys.path.insert(0, str(Path(__file__).resolve().parent))
@@ -13,7 +14,7 @@ if __name__ == "__main__":
uvicorn.run( uvicorn.run(
"main:app", "main:app",
host="0.0.0.0", host="0.0.0.0",
port=8000, port=PORT,
reload=True, # 2B><0B8G5A:0O ?5@5703@C7:0 ?@8 87<5=5=88 :>40 reload=False, # 2B><0B8G5A:0O ?5@5703@C7:0 ?@8 87<5=5=88 :>40
log_level="info" log_level="info"
) )