267 lines
11 KiB
Python
267 lines
11 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 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:
|
|
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_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:
|
|
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
|
|
)
|
|
|