Compare commits

..

8 Commits

6 changed files with 96 additions and 38 deletions
+4 -2
View File
@@ -13,7 +13,7 @@ APP_VERSION=1.0.0
# Server
HOST=0.0.0.0
PORT=5056
PORT=5008
RELOAD=True
#SFTP
@@ -23,4 +23,6 @@ SFPT_PASSWORD = Audio4analy6!6
FILESAPTH = audiofiles
GIGAAM_API_URL = http://localhost:5001
GIGAAM_API_URL = http://host.docker.internal:5002
CALLS_WEB_CLIENT_URL=http://web_client_test:8642
AUDIO_FILES_PATH_TEST=D:\SpeechAnalitics\audiofiles
+16 -2
View File
@@ -1,12 +1,26 @@
# 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
# Клонировать и перейти в директорию
cd /Users/petr/SpeechAnalytics/FileAudioAPI
# Перейти в директорию сервиса (от корня репозитория SpeechAnalytics)
cd FileAudioAPI
# Запуск с Docker (рекомендуется)
docker-compose up -d
+2 -2
View File
@@ -25,14 +25,14 @@ PORT = int(os.getenv("PORT", "5008"))
HOST = os.getenv("HOST", "localhost")
# GigaAM API Configuration
GIGAAM_API_URL = os.getenv("GIGAAM_API_URL", "http://192.168.1.73:5002")
GIGAAM_API_URL = os.getenv("GIGAAM_API_URL", "http://host.docker.internal:5002")
AUDIOFILES_PATH = os.path.join(os.getcwd(), os.getenv("FILESAPTH", "audiofiles"))
# Calls_WEB_Client_main Webhook Configuration
CALLS_WEB_CLIENT_URL = os.getenv(
"CALLS_WEB_CLIENT_URL",
"http://192.168.1.73:8642"
"http://host.docker.internal:8642"
)
WEBHOOK_ENDPOINT = f"{CALLS_WEB_CLIENT_URL}/api/transcription/webhook"
WEBHOOK_API_KEY = os.getenv("WEBHOOK_API_KEY", "webhook_secret_key")
+64 -30
View File
@@ -9,9 +9,10 @@ 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
from apiApp.config import WEBHOOK_ENDPOINT, WEBHOOK_API_KEY, CALLS_WEB_CLIENT_URL
logger = logging.getLogger(__name__)
ai_conclusion_router = APIRouter()
@@ -168,39 +169,48 @@ async def save_ai_conclusion(request: AiConclusionRequest, db: Session = Depends
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 в 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
}
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
)
max_retries = 5
delays = [2, 4, 8, 15, 30]
delivered = False
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}")
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}")
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}")
if attempt < max_retries:
time.sleep(delays[attempt - 1])
if not delivered:
logger.error(f"❌ Не удалось доставить webhook для {request.filename} после {max_retries} попыток")
return AiConclusionResponse(
success=True,
@@ -228,15 +238,39 @@ async def mark_recognition_failed(request: RecognitionFailedRequest, db: Session
"""
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
)
+1 -1
View File
@@ -250,7 +250,7 @@ async def process_all_pending_audio(
logger.info(f"🚀 Поиск Audio без AiConclusion (limit={limit})")
# Находим все Audio без AiConclusion используя вспомогательную функцию
pending_audio = query_audio_without_conclusion(db, limit).all()
pending_audio: list[Audio] = query_audio_without_conclusion(db, limit).all()
total_pending = query_audio_without_conclusion(db).count()
if not pending_audio:
+9 -1
View File
@@ -11,7 +11,15 @@ class Loader():
self.call_types = ['in']
self.loaded_files = [] # Список загруженных файлов
def filter_call(self, filename: str):
def filter_call(self, filename: str) -> bool:
if not filename or not filename.endswith('.wav'):
return False
# Пропускаем раздельные каналы звонков (_r = receive/клиент, _t = transmit/оператор)
name_without_ext = filename[:-4]
if name_without_ext.endswith(('_r', '_t', '_in', '_out', '-r', '-t')):
return False
if filename.split("-")[0] in self.call_types:
return True
return False