Files
FileAudioAPI/apiApp/routers/ai_conclusion_router.py
T
2026-09-02 17:38:24 +05:00

277 lines
12 KiB
Python

"""
API endpoints для получения AI заключений от GigaAM API
"""
from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel
from typing import Optional, List, Dict, Any
from sqlalchemy.orm import Session
from apiApp.database import get_db
from apiApp.database.Audio import Audio
from apiApp.database.AiConclusion import AiConclusion
from datetime import datetime
import time
import logging
import requests
from apiApp.config import WEBHOOK_ENDPOINT, WEBHOOK_API_KEY, CALLS_WEB_CLIENT_URL
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 заключения"""
filename: str
transcription: str
corrected_transcription: str
analysis: Dict[str, Any]
segments: Optional[List[Dict[str, Any]]] = []
processing_time_seconds: Optional[float] = 0
callback_url: Optional[str] = None
class AiConclusionResponse(BaseModel):
"""Модель ответа"""
success: bool
message: Optional[str] = None
audio_id: Optional[str] = None
filename: 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)
async def save_ai_conclusion(request: AiConclusionRequest, db: Session = Depends(get_db)):
"""
Сохраняет AI заключение от GigaAM API
**Параметры:**
- **filename**: Имя файла (например, "in-xxx.wav")
- **transcription**: Исходная транскрибация
- **corrected_transcription**: Исправленная транскрибация
- **analysis**: Результат анализа (sentiment, topics, summary)
- **segments**: Сегменты диалога (опционально)
- **processing_time_seconds**: Время обработки (опционально)
"""
try:
logger.info(f"📥 Получено AI заключение для файла: {request.filename}")
# Находим аудио файл по имени
audio = db.query(Audio).filter(Audio.filename == request.filename).first()
if not audio:
logger.warning(f"⚠️ Файл {request.filename} не найден в БД")
raise HTTPException(
status_code=404,
detail=f'Файл {request.filename} не найден'
)
# Проверяем, есть ли уже заключение
existing_conclusion = db.query(AiConclusion).filter(
AiConclusion.audio_id == audio.id
).first()
conclusion_data = {
"transcription": request.transcription,
"ai_transcription": request.corrected_transcription,
"conclusion": request.analysis,
"segments": request.segments,
"processing_time_seconds": request.processing_time_seconds
}
if existing_conclusion:
# Обновляем существующее заключение
logger.info(f"🔄 Обновление существующего заключения для {request.filename}")
existing_conclusion.conclusion = conclusion_data
existing_conclusion.end_date = datetime.utcnow()
else:
# Создаём новое заключение
logger.info(f"✅ Создание нового заключения для {request.filename}")
new_conclusion = AiConclusion(
audio_id=audio.id,
conclusion=conclusion_data,
index_date=datetime.utcnow()
)
db.add(new_conclusion)
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 для анализа (с retry при перезагрузке контейнера)
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
}
max_retries = 5
delays = [2, 4, 8, 15, 30]
delivered = False
for attempt in range(1, max_retries + 1):
try:
webhook_response = requests.post(
WEBHOOK_ENDPOINT,
json=webhook_payload,
headers={"X-Webhook-Key": WEBHOOK_API_KEY},
timeout=30
)
if webhook_response.status_code in (200, 201):
logger.info(f"✅ Webhook успешно отправлен для {request.filename} (попытка {attempt}/{max_retries})")
delivered = True
break
else:
logger.warning(f"⚠️ Webhook вернул статус {webhook_response.status_code} (попытка {attempt}/{max_retries})")
logger.warning(f"Response: {webhook_response.text[:200]}")
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
logger.warning(f"⏳ Calls_WEB_Client_main временно недоступен ({type(e).__name__}), попытка {attempt}/{max_retries}...")
except Exception as e:
logger.error(f"❌ Непредвиденная ошибка при отправке webhook (попытка {attempt}): {e}")
if attempt < max_retries:
time.sleep(delays[attempt - 1])
if not delivered:
logger.error(f"❌ Не удалось доставить webhook для {request.filename} после {max_retries} попыток")
return AiConclusionResponse(
success=True,
message='Заключение сохранено и отправлено на анализ',
audio_id=str(audio.id),
filename=request.filename
)
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Ошибка при сохранении заключения: {e}")
db.rollback()
raise HTTPException(
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:
logger.warning(f"⚠️ Попытка пометить failed для несуществующего аудио: {request.filename}, error={request.error}")
raise HTTPException(status_code=404, detail=f"Файл не найден: {request.filename}")
audio.recognition_status = "failed"
audio.recognition_last_error = request.error
db.commit()
logger.warning(f"❌ Распознавание помечено failed для {request.filename}. Причина: {request.error}")
# Отправляем вебхук об ошибке в Calls_WEB_Client_main, чтобы синхронизировать pipeline_status
try:
if (audio.sourse or "").lower() != "external":
failed_webhook_payload = {
"audio_id": str(audio.id),
"filename": request.filename,
"status": "failed",
"error": request.error
}
webhook_url = f"{CALLS_WEB_CLIENT_URL}/api/transcription/failed"
logger.info(f"📤 Отправка failed-webhook в Calls_WEB_Client_main: {webhook_url}")
requests.post(
webhook_url,
json=failed_webhook_payload,
headers={"X-Webhook-Key": WEBHOOK_API_KEY},
timeout=10
)
except Exception as e:
logger.error(f"❌ Ошибка отправки failed-webhook в Calls_WEB: {e}")
return AiConclusionResponse(
success=True,
message="Recognition marked as failed",
audio_id=str(audio.id),
filename=audio.filename
)