add external service
This commit is contained in:
@@ -1,393 +0,0 @@
|
|||||||
"""
|
|
||||||
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.config import AUDIOFILES_PATH
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
audio_management_router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
logger.info(f"📝 Регистрация файла: {filename}")
|
|
||||||
|
|
||||||
# Проверяем, что файл уже существует в общей папке
|
|
||||||
if not os.path.exists(file_path):
|
|
||||||
logger.error(f"❌ Файл не найден: {file_path}")
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=404,
|
|
||||||
detail=f'Файл не найден на диске: {file_path}'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Проверяем, что файл не был уже зарегистрирован
|
|
||||||
existing_audio = db.query(Audio).filter(Audio.filename == filename).first()
|
|
||||||
if existing_audio:
|
|
||||||
logger.warning(f"⚠️ Файл уже зарегистрирован: {filename}")
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=400,
|
|
||||||
detail=f'Файл {filename} уже зарегистрирован в БД'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Получаем размер файла
|
|
||||||
file_size = os.path.getsize(file_path)
|
|
||||||
|
|
||||||
# Создаём запись в БД
|
|
||||||
audio = Audio()
|
|
||||||
audio.filename = filename
|
|
||||||
audio.file_size = file_size
|
|
||||||
audio.index_date = datetime.utcnow()
|
|
||||||
|
|
||||||
db.add(audio)
|
|
||||||
db.commit()
|
|
||||||
db.refresh(audio)
|
|
||||||
|
|
||||||
logger.info(f"✅ Файл зарегистрирован: {filename} (audio_id={audio.id})")
|
|
||||||
|
|
||||||
return AudioRegisterResponse(
|
|
||||||
id=str(audio.id),
|
|
||||||
filename=audio.filename,
|
|
||||||
file_size=audio.file_size,
|
|
||||||
created_at=audio.index_date
|
|
||||||
)
|
|
||||||
|
|
||||||
except HTTPException:
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
db.rollback()
|
|
||||||
logger.error(f"❌ Ошибка при регистрации файла: {e}")
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=500,
|
|
||||||
detail=f'Ошибка при регистрации: {str(e)}'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def process_audio_file(audio_id: str, db: Session):
|
|
||||||
"""
|
|
||||||
Фоновая обработка одного аудиофайла
|
|
||||||
|
|
||||||
Отправляет запрос в GigaAM API для распознавания
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
audio = db.query(Audio).filter(Audio.id == audio_id).first()
|
|
||||||
if not audio:
|
|
||||||
logger.error(f"❌ Audio {audio_id} не найден")
|
|
||||||
return
|
|
||||||
|
|
||||||
logger.info(f"🎵 Запуск распознавания для {audio.filename}")
|
|
||||||
|
|
||||||
# Формируем запрос в GigaAM API
|
|
||||||
from apiApp.config import GIGAAM_API_URL
|
|
||||||
api_url = f"{GIGAAM_API_URL}/api/call/process"
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"filename": audio.filename
|
|
||||||
}
|
|
||||||
|
|
||||||
# Отправляем запрос в GigaAM API
|
|
||||||
import requests
|
|
||||||
response = requests.post(api_url, json=payload, timeout=10)
|
|
||||||
|
|
||||||
if response.status_code == 200 or response.status_code == 202:
|
|
||||||
logger.info(f"✅ Запущено распознавание для {audio.filename}")
|
|
||||||
else:
|
|
||||||
logger.error(f"❌ Ошибка запуска распознавания для {audio.filename}: {response.status_code} - {response.text}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"❌ Ошибка при обработке {audio_id}: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
@audio_management_router.post("/audio/process-all")
|
|
||||||
async def process_all_pending_audio(
|
|
||||||
request: AudioProcessAllRequest,
|
|
||||||
background_tasks: BackgroundTasks,
|
|
||||||
db: Session = Depends(get_db)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Запуск распознавания для всех Audio без AiConclusion
|
|
||||||
|
|
||||||
Находит все записи Audio, у которых нет AiConclusion, и запускает
|
|
||||||
распознавание для них (до указанного лимита).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request: {limit: 100} - максимум файлов для обработки
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
200 OK + {
|
|
||||||
"started_count": 15,
|
|
||||||
"pending_files": ["file1.wav", "file2.wav", ...],
|
|
||||||
"total_pending": 50
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
limit = request.limit
|
|
||||||
|
|
||||||
logger.info(f"🚀 Поиск Audio без AiConclusion (limit={limit})")
|
|
||||||
|
|
||||||
# Находим все Audio без AiConclusion
|
|
||||||
pending_audio = db.query(Audio).filter(
|
|
||||||
Audio.AiConclusion == None
|
|
||||||
).order_by(Audio.index_date.asc()).limit(limit).all()
|
|
||||||
|
|
||||||
total_pending = db.query(Audio).filter(Audio.AiConclusion == None).count()
|
|
||||||
|
|
||||||
if not pending_audio:
|
|
||||||
logger.info("ℹ️ Нет файлов для распознавания")
|
|
||||||
return {
|
|
||||||
"started_count": 0,
|
|
||||||
"pending_files": [],
|
|
||||||
"total_pending": 0,
|
|
||||||
"message": "Нет файлов без AiConclusion"
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(f"📋 Найдено файлов для обработки: {len(pending_audio)} из {total_pending}")
|
|
||||||
|
|
||||||
# Добавляем задачи в фон
|
|
||||||
started_count = 0
|
|
||||||
pending_files = []
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# Добавляем в фон (асинхронно)
|
|
||||||
# В FastAPI используем BackgroundTasks
|
|
||||||
# Но нужно создавать новую сессию для каждого таска
|
|
||||||
pending_files.append(audio.filename)
|
|
||||||
started_count += 1
|
|
||||||
|
|
||||||
# Запускаем обработку в фоне
|
|
||||||
# Используем lambda для захвата audio_id
|
|
||||||
background_tasks.add_task(
|
|
||||||
process_single_audio,
|
|
||||||
str(audio.id)
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info(f"✅ Запущено распознавание для {started_count} файлов")
|
|
||||||
|
|
||||||
return {
|
|
||||||
"started_count": started_count,
|
|
||||||
"pending_files": pending_files,
|
|
||||||
"total_pending": total_pending
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"❌ Ошибка при запуске пакетного распознавания: {e}")
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=500,
|
|
||||||
detail=f'Ошибка: {str(e)}'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def process_single_audio(audio_id: str):
|
|
||||||
"""
|
|
||||||
Обработка одного аудиофайла в фоне
|
|
||||||
|
|
||||||
Создаёт новую DB сессию для обработки
|
|
||||||
"""
|
|
||||||
from apiApp.database import SessionLocal
|
|
||||||
|
|
||||||
db = SessionLocal()
|
|
||||||
try:
|
|
||||||
process_audio_file(audio_id, db)
|
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
|
|
||||||
@audio_management_router.get("/audio/pending")
|
|
||||||
async def get_pending_audio(
|
|
||||||
db: Session = Depends(get_db),
|
|
||||||
limit: int = 100
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Получить список Audio без AiConclusion
|
|
||||||
|
|
||||||
Query Parameters:
|
|
||||||
limit: Максимум файлов (default: 100)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Список файлов, ожидающих распознавания
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
pending_audio = db.query(Audio).filter(
|
|
||||||
Audio.AiConclusion == None
|
|
||||||
).order_by(Audio.index_date.asc()).limit(limit).all()
|
|
||||||
|
|
||||||
files_info = []
|
|
||||||
for audio in pending_audio:
|
|
||||||
file_path = os.path.join(AUDIOFILES_PATH, audio.filename)
|
|
||||||
exists = os.path.exists(file_path)
|
|
||||||
|
|
||||||
files_info.append({
|
|
||||||
"audio_id": str(audio.id),
|
|
||||||
"filename": audio.filename,
|
|
||||||
"file_size": audio.file_size,
|
|
||||||
"created_at": audio.index_date.isoformat() if audio.index_date else None,
|
|
||||||
"exists_on_disk": exists
|
|
||||||
})
|
|
||||||
|
|
||||||
total_pending = db.query(Audio).filter(Audio.AiConclusion == None).count()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"total_pending": total_pending,
|
|
||||||
"count": len(files_info),
|
|
||||||
"files": files_info
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"❌ Ошибка при получении списка: {e}")
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=500,
|
|
||||||
detail=str(e)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@audio_management_router.get("/audio/stats")
|
|
||||||
async def get_audio_stats(db: Session = Depends(get_db)):
|
|
||||||
"""
|
|
||||||
Получить статистику по аудиофайлам
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Статистика по Audio записям
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
total_audio = db.query(Audio).count()
|
|
||||||
with_conclusion = db.query(Audio).filter(Audio.AiConclusion != None).count()
|
|
||||||
without_conclusion = db.query(Audio).filter(Audio.AiConclusion == None).count()
|
|
||||||
|
|
||||||
# Проверяем существование файлов на диске
|
|
||||||
all_audio = db.query(Audio).all()
|
|
||||||
existing_count = 0
|
|
||||||
for audio in all_audio:
|
|
||||||
file_path = os.path.join(AUDIOFILES_PATH, audio.filename)
|
|
||||||
if os.path.exists(file_path):
|
|
||||||
existing_count += 1
|
|
||||||
|
|
||||||
return {
|
|
||||||
"total_audio": total_audio,
|
|
||||||
"with_conclusion": with_conclusion,
|
|
||||||
"without_conclusion": without_conclusion,
|
|
||||||
"existing_on_disk": existing_count,
|
|
||||||
"missing_on_disk": total_audio - existing_count
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"❌ Ошибка при получении статистики: {e}")
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=500,
|
|
||||||
detail=str(e)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
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}")
|
|
||||||
@@ -11,6 +11,7 @@ from apiApp.routers import audio_router, recognition_router
|
|||||||
from apiApp.routers.ai_conclusion_router import ai_conclusion_router
|
from apiApp.routers.ai_conclusion_router import ai_conclusion_router
|
||||||
from apiApp.routers.audio_files_router import audio_files_router
|
from apiApp.routers.audio_files_router import audio_files_router
|
||||||
from apiApp.routers.audio_management_router import audio_management_router
|
from apiApp.routers.audio_management_router import audio_management_router
|
||||||
|
from apiApp.routers.external_audio import external_audio_router
|
||||||
|
|
||||||
print("✅ audio_management_router imported successfully")
|
print("✅ audio_management_router imported successfully")
|
||||||
|
|
||||||
@@ -63,6 +64,7 @@ app.include_router(audio_router, prefix=API_V1_PREFIX, tags=["audio"])
|
|||||||
app.include_router(recognition_router, prefix=API_V1_PREFIX, tags=["recognition"])
|
app.include_router(recognition_router, prefix=API_V1_PREFIX, tags=["recognition"])
|
||||||
app.include_router(ai_conclusion_router, prefix=API_V1_PREFIX, tags=["ai_conclusion"])
|
app.include_router(ai_conclusion_router, prefix=API_V1_PREFIX, tags=["ai_conclusion"])
|
||||||
app.include_router(audio_files_router, prefix=API_V1_PREFIX, tags=["audio_files"])
|
app.include_router(audio_files_router, prefix=API_V1_PREFIX, tags=["audio_files"])
|
||||||
|
app.include_router(external_audio_router, prefix=API_V1_PREFIX, tags=["external_audio"])
|
||||||
# audio_management_router с префиксом /audio для логической структуры
|
# audio_management_router с префиксом /audio для логической структуры
|
||||||
print("📝 Registering audio_management_router...")
|
print("📝 Registering audio_management_router...")
|
||||||
app.include_router(audio_management_router, prefix="/api", tags=["audio_management"])
|
app.include_router(audio_management_router, prefix="/api", tags=["audio_management"])
|
||||||
|
|||||||
Reference in New Issue
Block a user