""" 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 logging import requests 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 заключения""" 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 для анализа try: 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_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}") except requests.exceptions.ConnectionError: logger.error(f"❌ Не удалось подключиться к Calls_WEB_Client_main webhook: {WEBHOOK_ENDPOINT}") except requests.exceptions.Timeout: logger.warning(f"⚠️ Таймаут при отправке webhook для {request.filename}") except Exception as e: logger.error(f"❌ Ошибка при отправке webhook: {e}") 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: 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 )