|
|
|
|
@ -1,3 +1,84 @@
|
|
|
|
|
""" |
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
subquery = db.query(AiConclusion.audio_id).filter( |
|
|
|
|
AiConclusion.audio_id == Audio.id |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
query = db.query(Audio).filter( |
|
|
|
|
~exists().where(subquery.exists()) |
|
|
|
|
).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 |
|
|
|
|
|
|
|
|
|
@ -111,24 +192,13 @@ async def process_all_pending_audio(
|
|
|
|
|
} |
|
|
|
|
""" |
|
|
|
|
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("ℹ️ Нет файлов для распознавания") |
|
|
|
|
@ -211,15 +281,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 +296,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,19 +321,9 @@ 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() |
|
|
|
|
@ -299,62 +349,3 @@ async def get_audio_stats(db: Session = Depends(get_db)):
|
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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}") |
|
|
|
|
|