Compare commits

...

39 Commits
master ... dev

Author SHA1 Message Date
poturaevpetr 34a31cc4d6 fix db connection 2 weeks ago
PoturaevPetr ebef51aa48 fix cnnect bp 2 weeks ago
poturaevpetr 013dd9a7d4 fix 2 weeks ago
PoturaevPetr c1a9ef27f1 fix settings 2 weeks ago
poturaevpetr ac8e881ee3 fix 2 weeks ago
poturaevpetr 21dafb5681 fix 2 weeks ago
poturaevpetr 9f65a9398a fix 2 weeks ago
poturaevpetr b8af963e9f fix 2 weeks ago
poturaevpetr c4dec7644d fix 2 weeks ago
poturaevpetr 2df1761d34 fix 2 weeks ago
poturaevpetr 59b5676c84 fix 2 weeks ago
poturaevpetr ddbebea001 add autorestart rec 2 weeks ago
poturaevpetr 86245ce1fb add autorestart rec 2 weeks ago
poturaevpetr de2e66a37c add autorestart rec 2 weeks ago
poturaevpetr ba93ac1970 add autorestart rec 2 weeks ago
poturaevpetr e5081e71a3 add autorestart rec 2 weeks ago
poturaevpetr 51c361f99d fix add router 2 weeks ago
poturaevpetr a361078ef9 fix add router 2 weeks ago
poturaevpetr 6efd32f368 перенос SFTP мониторинг в сервис Calls_WEB_Client_main 2 weeks ago
poturaevpetr 3930ea7021 webhook to site 2 weeks ago
poturaevpetr 76b5b26c75 взаимодействие сайта с FileAudioAPI 2 weeks ago
poturaevpetr 168151fe7f fix get_db 2 weeks ago
poturaevpetr 094b17ee93 рутер для полуения ответа от gigaam 2 weeks ago
poturaevpetr cceaf65576 fix checker 2 weeks ago
poturaevpetr 0f8e6be08c fix circle imports 2 weeks ago
poturaevpetr 5a1e296d41 send files parallel mode 2 weeks ago
poturaevpetr 7bb64b3c42 отправление файлов в gigaam сразу после скачивания 2 weeks ago
poturaevpetr 1a573362c9 add GIGAAM_API_URL to .env 2 weeks ago
poturaevpetr 2a61e8422d Merge remote-tracking branch 'origin/dev' into dev 2 weeks ago
poturaevpetr f58c95708f add post file to gigaam service 2 weeks ago
PoturaevPetr efa4d775e4 fix type field audio 2 weeks ago
poturaevpetr ebfb48a2b8 create tables 2 weeks ago
poturaevpetr 577dc277e0 fix model import 2 weeks ago
poturaevpetr c2289bfc45 fix model 2 weeks ago
poturaevpetr fa4c46173f add write audio to db 2 weeks ago
PoturaevPetr 711d7b6b1e fix field table 2 weeks ago
PoturaevPetr 8b471c771a add autoLoader 2 weeks ago
PoturaevPetr d4db29f710 update requirements.txt 2 weeks ago
poturaevpetr cfcc84eac9 add env.test 2 weeks ago
  1. 26
      .env.test
  2. 5
      Dockerfile
  3. 24
      apiApp/config.py
  4. 6
      apiApp/database.py
  5. 3
      apiApp/database/AiConclusion.py
  6. 6
      apiApp/database/Audio.py
  7. 7
      apiApp/database/ConclusionVersion.py
  8. 4
      apiApp/database/Operator.py
  9. 5
      apiApp/database/__init__.py
  10. 144
      apiApp/routers/ai_conclusion_router.py
  11. 236
      apiApp/routers/audio_files_router.py
  12. 32
      apiApp/routers/audio_management_helper.py
  13. 474
      apiApp/routers/audio_management_router.py
  14. 393
      apiApp/routers/audio_management_router.py.backup
  15. 3
      autoLoader/__init__.py
  16. 16
      autoLoader/config.py
  17. 41
      autoLoader/database/__init__.py
  18. 5
      autoLoader/loader/__init__.py
  19. 53
      autoLoader/loader/connector.py
  20. 172
      autoLoader/loader/loader.py
  21. 388
      autoLoader/loader/recognition_checker.py
  22. 389
      autoLoader/loader/recognition_checker_old.py
  23. 39
      init_db.py
  24. 31
      main.py
  25. 3
      requirements.txt
  26. 3
      run.py
  27. 65
      run_checker.py
  28. 13
      run_loader.py

26
.env.test

@ -0,0 +1,26 @@
# Database
# DATABASE_URL=sqlite:///./speech_analytics.db
# Для PostgreSQL:
DATABASE_URL=postgresql://postgres_test:test_user@postgres_test:5432/audiofiles_db
# API Settings
API_V1_PREFIX=/api/v1
MAX_UPLOAD_SIZE=104857600 # 100MB in bytes
# Application
APP_TITLE=Speech Analytics API
APP_VERSION=1.0.0
# Server
HOST=0.0.0.0
PORT=5056
RELOAD=True
#SFTP
SFPT_HOSTNAME = 192.168.1.150
SFPT_USERNAME = monitor
SFPT_PASSWORD = Audio4analy6!6
FILESAPTH = audiofiles
GIGAAM_API_URL = http://localhost:5001

5
Dockerfile

@ -32,7 +32,8 @@ RUN mkdir -p /app/uploads
RUN mkdir -p /app/data
# Открытие порта
EXPOSE 8000
EXPOSE 5008
# Команда запуска приложения
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
CMD ["python", "run.py"]

24
apiApp/config.py

@ -6,6 +6,9 @@ BASE_DIR = Path(__file__).resolve().parent.parent
UPLOAD_FOLDER = BASE_DIR / "uploads"
UPLOAD_FOLDER.mkdir(exist_ok=True)
# Audio files path (shared with Calls_WEB_Client_main)
AUDIOFILES_PATH = os.getenv("AUDIOFILES_PATH", "/app/audiofiles")
# Database
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./speech_analytics.db")
@ -17,3 +20,24 @@ ALLOWED_AUDIO_EXTENSIONS = {".mp3", ".wav", ".ogg", ".flac", ".m4a", ".aac"}
# Application
APP_TITLE = "Speech Analytics API"
APP_VERSION = "1.0.0"
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")
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:5059"
)
WEBHOOK_ENDPOINT = f"{CALLS_WEB_CLIENT_URL}/api/transcription/webhook"
WEBHOOK_API_KEY = os.getenv("WEBHOOK_API_KEY", "webhook_secret_key")
# Auto-restore recognition on startup
ENABLE_AUTO_RESTORE = os.getenv("ENABLE_AUTO_RESTORE", "true").lower() == "true"
AUTO_RESTORE_LIMIT = int(os.getenv("AUTO_RESTORE_LIMIT", "100")) # Максимум файлов для восстановления
AUTO_RESTORE_DELAY = int(os.getenv("AUTO_RESTORE_DELAY", "5")) # Задержка перед запуском (секунды)

6
apiApp/database.py

@ -6,7 +6,11 @@ from apiApp.config import DATABASE_URL
# Создание engine
engine = create_engine(
DATABASE_URL,
connect_args={"check_same_thread": False} if "sqlite" in DATABASE_URL else {}
connect_args={"check_same_thread": False} if "sqlite" in DATABASE_URL else {},
pool_pre_ping=True, # Проверять соединение перед использованием
pool_recycle=600, # Пересоздавать соединения каждые 10 минут
pool_size=10, # Размер пула соединений
max_overflow=20 # Дополнительные соединения при пиковой нагрузке
)
# SessionLocal

3
apiApp/database/AiConclusion.py

@ -18,4 +18,5 @@ class AiConclusion(Base):
index_date = Column(DateTime, default=datetime.utcnow)
end_date = Column(DateTime)
audio = relationship("Audio", back_populates="ai_conclusion")
audio = relationship("Audio", back_populates="ai_conclusion")
versions = relationship("ConclusionVersion", back_populates="ai_conclusion")

6
apiApp/database/Audio.py

@ -1,4 +1,4 @@
from sqlalchemy import Column, String, DateTime, UUID, ForeignKey, Float, Integer
from sqlalchemy import Column, String, DateTime, Text, UUID, ForeignKey, Float, Integer
from sqlalchemy.orm import relationship
from apiApp.database import Base
import uuid
@ -9,9 +9,9 @@ class Audio(Base):
__tablename__ = "audio"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
filename = Column(String(255), nullable=False)
filename = Column(Text, nullable=False)
index_date = Column(DateTime, default=datetime.utcnow)
file_path = Column(String(500))
file_path = Column(Text)
duration = Column(Float)
file_size = Column(Integer)

7
apiApp/database/ConclusionVersion.py

@ -1,4 +1,5 @@
from sqlalchemy import Column, UUID, ForeignKey, Integer, Text
from sqlalchemy.orm import relationship
from apiApp.database import Base
import uuid
@ -7,6 +8,8 @@ class ConclusionVersion(Base):
__tablename__ = "conclusion_version"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
conclusion_id = Column(UUID(as_uuid=True), ForeignKey("conclusion.id"))
conclusion_id = Column(UUID(as_uuid=True), ForeignKey("ai_conclusion.id"))
version = Column(Integer)
content = Column(Text)
content = Column(Text)
ai_conclusion = relationship("AiConclusion", back_populates="versions")

4
apiApp/database/Operator.py

@ -11,6 +11,6 @@ class Operator(Base):
fio = Column(String(100))
num = Column(Integer)
calls = relationship("Call", back_populates="operator")
# TODO: Добавить relationship когда будет создана модель Call
# calls = relationship("Call", back_populates="operator")

5
apiApp/database/__init__.py

@ -28,4 +28,7 @@ def get_db():
from apiApp.database.Operator import Operator
from apiApp.database.Audio import Audio
from apiApp.database.AiConclusion import AiConclusion
from apiApp.database.ConclusionVersion import ConclusionVersion
from apiApp.database.ConclusionVersion import ConclusionVersion
# Все модели должны быть импортированы здесь для правильной работы SQLAlchemy metadata
# Это гарантирует, что все relationship будут работать корректно

144
apiApp/routers/ai_conclusion_router.py

@ -0,0 +1,144 @@
"""
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()
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
class AiConclusionResponse(BaseModel):
"""Модель ответа"""
success: bool
message: Optional[str] = None
audio_id: Optional[str] = None
filename: Optional[str] = None
error: Optional[str] = None
@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}")
# Отправляем 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)
)

236
apiApp/routers/audio_files_router.py

@ -0,0 +1,236 @@
"""
API endpoints для обслуживания аудиофайлов
"""
from fastapi import APIRouter, HTTPException, Depends
from fastapi.responses import FileResponse, StreamingResponse
from sqlalchemy.orm import Session
from typing import Optional
import os
import logging
from apiApp.database import get_db
from apiApp.database.Audio import Audio
from apiApp.config import AUDIOFILES_PATH
logger = logging.getLogger(__name__)
audio_files_router = APIRouter()
@audio_files_router.get("/audio/{filename}")
async def get_audio_file(filename: str):
"""
Возвращает аудиофайл для стриминга/воспроизведения
Args:
filename: Имя аудиофайла (например, "in-xxx.wav")
Returns:
StreamingResponse с аудиофайлом
"""
try:
# Проверяем расширение файла
allowed_extensions = ['.wav', '.mp3', '.ogg', '.m4a', '.flac']
if not any(filename.lower().endswith(ext) for ext in allowed_extensions):
raise HTTPException(
status_code=400,
detail=f'Неподдерживаемый формат файла. Разрешены: {", ".join(allowed_extensions)}'
)
# Формируем путь к файлу
file_path = os.path.join(AUDIOFILES_PATH, filename)
# Проверяем существование файла
if not os.path.exists(file_path):
logger.warning(f" Файл не найден: {file_path}")
raise HTTPException(
status_code=404,
detail=f'Файл {filename} не найден'
)
logger.info(f"🎵 Отдача аудио: {filename}")
# Определяем MIME тип
import mimetypes
mime_type, _ = mimetypes.guess_type(file_path)
if mime_type is None:
mime_type = 'audio/wav'
# Возвращаем файл как поток
def iterfile():
with open(file_path, 'rb') as f:
data = f.read(8192)
while data:
yield data
data = f.read(8192)
return StreamingResponse(
iterfile(),
media_type=mime_type,
headers={
'Content-Disposition': f'inline; filename="{filename}"',
'Accept-Ranges': 'bytes'
}
)
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Ошибка при отдаче аудио: {e}")
raise HTTPException(
status_code=500,
detail=f'Ошибка при отдаче файла: {str(e)}'
)
@audio_files_router.get("/audio/{filename}/info")
async def get_audio_info(filename: str, db: Session = Depends(get_db)):
"""
Возвращает информацию об аудиофайле
Args:
filename: Имя аудиофайла
Returns:
JSON с информацией о файле
"""
try:
# Ищем информацию в БД
audio = db.query(Audio).filter(Audio.filename == filename).first()
if not audio:
raise HTTPException(
status_code=404,
detail=f'Файл {filename} не найден в БД'
)
# Проверяем существование файла
file_path = os.path.join(AUDIOFILES_PATH, filename)
if not os.path.exists(file_path):
raise HTTPException(
status_code=404,
detail=f'Файл {filename} не найден на диске'
)
# Получаем размер файла
file_size = os.path.getsize(file_path)
# Определяем длительность (приблизительно)
# В идеале использовать библиотеку типа mutagen или soundfile
duration = None
try:
import wave
with wave.open(file_path, 'r') as wav_file:
frames = wav_file.getnframes()
rate = wav_file.getframerate()
duration = frames / float(rate) if rate > 0 else 0
except:
pass
return {
"success": True,
"filename": filename,
"file_size_bytes": file_size,
"duration_seconds": duration,
"created_at": audio.index_date.isoformat() if audio.index_date else None,
"audio_id": str(audio.id)
}
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Ошибка при получении информации: {e}")
raise HTTPException(
status_code=500,
detail=str(e)
)
@audio_files_router.get("/audio/list")
async def list_audio_files(
db: Session = Depends(get_db),
limit: int = 100,
offset: int = 0
):
"""
Возвращает список аудиофайлов
Query Parameters:
limit: Максимальное количество файлов (default: 100)
offset: Смещение для пагинации (default: 0)
Returns:
JSON со списком файлов
"""
try:
# Получаем список файлов из БД
query = db.query(Audio).order_by(Audio.index_date.desc())
total = query.count()
audio_files = query.offset(offset).limit(limit).all()
files_info = []
for audio in audio_files:
file_path = os.path.join(AUDIOFILES_PATH, audio.filename)
exists = os.path.exists(file_path)
files_info.append({
"filename": audio.filename,
"file_size": audio.file_size,
"created_at": audio.index_date.isoformat() if audio.index_date else None,
"exists": exists,
"audio_id": str(audio.id)
})
return {
"success": True,
"total": total,
"count": len(files_info),
"files": files_info
}
except Exception as e:
logger.error(f"❌ Ошибка при получении списка файлов: {e}")
raise HTTPException(
status_code=500,
detail=str(e)
)
@audio_files_router.get("/audio/{filename}/download")
async def download_audio_file(filename: str):
"""
Возвращает аудиофайл для скачивания
Args:
filename: Имя аудиофайла
Returns:
FileResponse для скачивания
"""
try:
# Формируем путь к файлу
file_path = os.path.join(AUDIOFILES_PATH, filename)
# Проверяем существование файла
if not os.path.exists(file_path):
raise HTTPException(
status_code=404,
detail=f'Файл {filename} не найден'
)
logger.info(f"📥 Скачивание аудио: {filename}")
return FileResponse(
path=file_path,
media_type='audio/wav',
filename=filename
)
except HTTPException:
raise
except Exception as e:
logger.error(f"❌ Ошибка при скачивании: {e}")
raise HTTPException(
status_code=500,
detail=str(e)
)

32
apiApp/routers/audio_management_helper.py

@ -0,0 +1,32 @@
"""
Исправленная версия для проверки Audio без AiConclusion
"""
from sqlalchemy import exists
def get_audio_without_conclusion(db, limit=100):
"""
Находит все Audio, у которых нет AiConclusion
Использует подзапрос через exists, так как AiConclusion - это relationship
"""
# Импортируем модели
from apiApp.database.Audio import Audio
from apiApp.database.AiConclusion import AiConclusion
# Создаём подзапрос для проверки наличия AiConclusion
subquery = db.query(AiConclusion.audio_id).filter(
AiConclusion.audio_id == Audio.id
)
# Находим Audio без AiConclusion
pending_audio = db.query(Audio).filter(
~exists().where(subquery.exists())
).order_by(Audio.index_date.asc()).limit(limit).all()
# Считаем total
total_pending = db.query(Audio).filter(
~exists().where(subquery.exists())
).count()
return pending_audio, total_pending

474
apiApp/routers/audio_management_router.py

@ -0,0 +1,474 @@
"""
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.database.AiConclusion import AiConclusion
from apiApp.config import AUDIOFILES_PATH
logger = logging.getLogger(__name__)
audio_management_router = APIRouter()
def query_audio_without_conclusion(db, limit=None):
"""
Возвращает запрос для поиска Audio без AiConclusion
Использует exists() подзапрос, так как AiConclusion - это relationship
"""
from sqlalchemy import exists, not_
# Используем более простой подзапрос
subquery = exists().where(
AiConclusion.audio_id == Audio.id
)
query = db.query(Audio).filter(
~subquery
).order_by(Audio.index_date.asc())
if limit:
query = query.limit(limit)
return query
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}")
# Проверяем что файл существует на диске
from apiApp.config import AUDIOFILES_PATH
import os
file_path = os.path.join(AUDIOFILES_PATH, audio.filename)
if not os.path.exists(file_path):
logger.error(f"❌ Файл не найден на диске в FileAudioAPI: {file_path}")
# Помечаем audio как проблемный
return
file_size = os.path.getsize(file_path)
logger.info(f"📁 Файл найден: {file_path} (размер: {file_size} bytes)")
# Формируем запрос в GigaAM API - ТОЛЬКО асинхронный endpoint
from apiApp.config import GIGAAM_API_URL
# Используем только асинхронный endpoint
api_url = f"{GIGAAM_API_URL}/api/call/process/async"
payload = {
"filename": audio.filename
}
import requests
try:
# Отправляем запрос в асинхронный endpoint
response = requests.post(api_url, json=payload, timeout=10)
if response.status_code == 200 or response.status_code == 202:
result = response.json()
task_id = result.get('task_id')
logger.info(f"✅ Задача добавлена в очередь для {audio.filename} (task_id: {task_id})")
else:
error_detail = response.text
logger.error(f"❌ Ошибка запуска распознавания для {audio.filename}: {response.status_code}")
logger.error(f" Detail: {error_detail}")
except requests.exceptions.Timeout:
logger.error(f"❌ Таймаут при отправке задачи для {audio.filename}")
except requests.exceptions.ConnectionError as e:
logger.error(f"❌ Ошибка подключения к GigaAM API для {audio.filename}: {e}")
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 = query_audio_without_conclusion(db, limit).all()
total_pending = query_audio_without_conclusion(db).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 = query_audio_without_conclusion(db, 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 = query_audio_without_conclusion(db).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 = total_audio - query_audio_without_conclusion(db).count()
without_conclusion = query_audio_without_conclusion(db).count()
# Проверяем существование файлов на диске
all_audio = db.query(Audio).all()
existing_count = 0
missing_files = []
small_files = [] # Файлы меньше 1KB
for audio in all_audio:
file_path = os.path.join(AUDIOFILES_PATH, audio.filename)
if os.path.exists(file_path):
existing_count += 1
file_size = os.path.getsize(file_path)
if file_size < 1000:
small_files.append({
"audio_id": str(audio.id),
"filename": audio.filename,
"file_size": file_size,
"index_date": audio.index_date.isoformat() if audio.index_date else None
})
else:
missing_files.append({
"audio_id": str(audio.id),
"filename": audio.filename,
"index_date": audio.index_date.isoformat() if audio.index_date else None
})
stats = {
"total_audio": total_audio,
"with_conclusion": with_conclusion,
"without_conclusion": without_conclusion,
"existing_on_disk": existing_count,
"missing_on_disk": total_audio - existing_count,
"small_files_count": len(small_files)
}
# Добавляем списки проблемных файлов (первые 50 каждого типа)
if missing_files:
stats["missing_files_sample"] = missing_files[:50]
logger.warning(f" Найдено {len(missing_files)} отсутствующих файлов")
if small_files:
stats["small_files_sample"] = small_files[:50]
logger.warning(f" Найдено {len(small_files)} файлов меньше 1KB")
return stats
except Exception as e:
logger.error(f"❌ Ошибка при получении статистики: {e}")
raise HTTPException(
status_code=500,
detail=str(e)
)
@audio_management_router.delete("/audio/cleanup")
async def cleanup_invalid_audio_files(
delete_missing: bool = False,
delete_small: bool = True,
min_size_bytes: int = 1000,
db: Session = Depends(get_db)
):
"""
Удаляет записи Audio для проблемных файлов
Query Parameters:
delete_missing: Удалять записи с отсутствующими файлами (default: False)
delete_small: Удалять записи с маленькими файлами (default: True)
min_size_bytes: Минимальный размер файла в bytes (default: 1000)
Returns:
Статистику удаления
"""
try:
all_audio = db.session.query(Audio).all()
deleted_missing = 0
deleted_small = 0
for audio in all_audio:
file_path = os.path.join(AUDIOFILES_PATH, audio.filename)
# Проверяем отсутствие файла
if delete_missing and not os.path.exists(file_path):
logger.info(f"🗑 Удаление записи с отсутствующим файлом: {audio.filename}")
db.session.delete(audio)
deleted_missing += 1
continue
# Проверяем размер файла
if delete_small and os.path.exists(file_path):
file_size = os.path.getsize(file_path)
if file_size < min_size_bytes:
logger.info(f"🗑 Удаление записи с маленьким файлом ({file_size} bytes): {audio.filename}")
# Удаляем и файл тоже
try:
os.remove(file_path)
logger.info(f" Файл удалён: {file_path}")
except Exception as e:
logger.warning(f" Не удалось удалить файл: {e}")
db.session.delete(audio)
deleted_small += 1
db.session.commit()
return {
"success": True,
"deleted_missing": deleted_missing,
"deleted_small": deleted_small,
"total_deleted": deleted_missing + deleted_small,
"message": f"Удалено {deleted_missing + deleted_small} записей"
}
except Exception as e:
db.session.rollback()
logger.error(f"❌ Ошибка при очистке: {e}")
raise HTTPException(
status_code=500,
detail=str(e)
)

393
apiApp/routers/audio_management_router.py.backup

@ -0,0 +1,393 @@
"""
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}")

3
autoLoader/__init__.py

@ -0,0 +1,3 @@
from autoLoader.database import *
from autoLoader.loader import Loader
loader = Loader()

16
autoLoader/config.py

@ -0,0 +1,16 @@
import os
# SFTP конфигурация
SFTP_HOSTNAME = os.getenv("SFTP_HOSTNAME", "192.168.1.150")
SFTP_USERNAME = os.getenv("SFTP_USERNAME", "monitor")
SFTP_PASSWORD = os.getenv("SFTP_PASSWORD", "Audio4analy6!6")
FILESAPTH = os.getenv("FILESAPTH", "audiofiles")
# База данных
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./speech_analytics.db")
# GigaAM API
GIGAAM_API_URL = os.getenv("GIGAAM_API_URL", "http://localhost:5001")
#App Setting
PORT = 5008

41
autoLoader/database/__init__.py

@ -0,0 +1,41 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, Session
from autoLoader.config import DATABASE_URL
from contextlib import contextmanager
# Создаём engine, но используем Base из apiApp.database
from apiApp.database import Base, engine
# SessionLocal (используем тот же engine)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Зависимость для получения сессии БД (для FastAPI)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# Контекстный менеджер для использования в loader
@contextmanager
def get_db_session():
"""Контекстный менеджер для работы с БД в loader"""
db = SessionLocal()
try:
yield db
db.commit()
except Exception as e:
db.rollback()
raise e
finally:
db.close()
# Импортируем ТОЛЬКО нужные модели из apiApp.database
# НЕ импортируем из autoLoader.database, чтобы избежать дублирования таблиц
from apiApp.database.Audio import Audio
from apiApp.database.AiConclusion import AiConclusion

5
autoLoader/loader/__init__.py

@ -0,0 +1,5 @@
from .loader import Loader
from .connector import ConnectorSFTP
from .recognition_checker import RecognitionChecker, process_pending_files
__all__ = ['Loader', 'ConnectorSFTP', 'RecognitionChecker', 'process_pending_files']

53
autoLoader/loader/connector.py

@ -0,0 +1,53 @@
from autoLoader.config import SFTP_HOSTNAME, SFTP_USERNAME, SFTP_PASSWORD
import socket
def check_connection():
"""Проверка доступности сервера"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(10)
try:
result = sock.connect_ex((SFTP_HOSTNAME, 22))
if result == 0:
print("Порт 22 доступен")
else:
print(f"Порт 22 недоступен. Код ошибки: {result}")
except Exception as e:
print(f"Ошибка проверки соединения: {e}")
finally:
sock.close()
# Перед подключением вызовите проверку
# check_connection()
import paramiko
class ConnectorSFTP():
def __init__(self):
self.sftp = None
self.ssh = paramiko.SSHClient()
def connect(self, remote_path: str):
try:
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.ssh.connect(
hostname=SFTP_HOSTNAME,
username=SFTP_USERNAME,
password=SFTP_PASSWORD,
)
self.sftp = self.ssh.open_sftp()
remote_path = remote_path.lstrip('/') # Удаляем начальный слэш, если есть
self.sftp.chdir(remote_path) # Переходим в директори
except paramiko.AuthenticationException:
print("Ошибка аутентификации. Проверьте имя пользователя и пароль.")
return None, None
except paramiko.SSHException as e:
print(f"Ошибка SSH: {e}")
return None, None
except FileNotFoundError:
print(f"Директория {remote_path} не найдена на сервере.")
return None, None
except IOError as e:
print(f"Ошибка ввода-вывода: {e}")
return None, None

172
autoLoader/loader/loader.py

@ -0,0 +1,172 @@
from autoLoader.config import FILESAPTH
from autoLoader.loader.connector import ConnectorSFTP
import datetime, os
from autoLoader.database import Audio, get_db_session
from sqlalchemy import inspect
local_path = os.path.join(os.getcwd(), FILESAPTH)
class Loader():
def __init__(self):
self.call_types = ['in']
self.loaded_files = [] # Список загруженных файлов
def filter_call(self, filename: str):
if filename.split("-")[0] in self.call_types:
return True
return False
def check_database(self):
"""Проверяет существование таблиц в БД"""
from autoLoader.database import engine
inspector = inspect(engine)
existing_tables = inspector.get_table_names()
if 'audio' not in existing_tables:
print("❌ Таблица 'audio' не существует в базе данных!")
print("💡 Запустите 'python init_db.py' для создания таблиц")
return False
return True
def send_to_recognition(self, limit: int = None):
"""
Отправляет загруженные файлы на распознавание
Args:
limit: Максимальное количество файлов для отправки
"""
if not self.loaded_files:
print(" Нет файлов для отправки на распознавание")
return
from autoLoader.loader import RecognitionChecker
print(f"\n🎤 Отправка {len(self.loaded_files)} файлов на распознавание...")
checker = RecognitionChecker()
# Проверяем доступность API
if not checker.check_api_availability():
print("❌ GigaAM API недоступен. Пропускаем распознавание.")
return
# Проверяем БД
if not checker.check_database():
print("❌ База данных не готова. Пропускаем распознавание.")
return
# Фильтруем только что загруженные файлы без заключения
files_to_send = []
for audio in self.loaded_files:
# Проверяем, есть ли у файла заключение
with get_db_session() as db:
from autoLoader.database import AiConclusion
existing_conclusion = db.query(AiConclusion).filter(
AiConclusion.audio_id == audio.id
).first()
if existing_conclusion:
print(f" Файл {audio.filename} уже имеет заключение")
continue
else:
# Конвертируем SQLAlchemy объект в словарь
files_to_send.append({
'id': str(audio.id),
'filename': audio.filename,
'file_size': audio.file_size,
'index_date': audio.index_date.isoformat() if audio.index_date else None
})
if not files_to_send:
print("✅ Все файлы уже имеют заключения")
return
# Отправляем файлы параллельно
print(f"🚀 Отправка {len(files_to_send)} файлов параллельно...")
results = checker.send_to_recognition_parallel(files_to_send)
print(f"✅ Отправлено {results['sent']} из {results['total']} файлов на распознавание")
def load(self):
# Проверяем БД перед началом работы
if not self.check_database():
exit(1)
date_now = datetime.datetime.now()# - datetime.timedelta(days=1)
remote_path = f"/{date_now.strftime('%Y/%m/%d')}"
connector = ConnectorSFTP()
connector.connect(remote_path=remote_path)
sftp_client = connector.sftp
ssh_client = connector.ssh
if sftp_client is None or ssh_client is None:
print("Не удалось подключиться к SFTP. Завершение работы.")
exit(1)
try:
listdir = sftp_client.listdir()
os.makedirs(local_path, exist_ok = True)
for file in listdir:
if self.filter_call(filename=file):
remote_file = f"{file}".lstrip('/')
filepath = os.path.join(local_path, file)
# Проверяем, существует ли файл локально
if os.path.exists(filepath):
print(f"Файл уже существует локально: {file}")
continue
try:
# Скачиваем файл
sftp_client.get(remote_file, filepath)
print(f"📥 Скачан файл: {remote_file}")
# Получаем размер файла
file_size = os.path.getsize(filepath)
# Сохраняем в БД через контекстный менеджер
with get_db_session() as db:
# Проверяем, есть ли уже такой файл в БД
existing_audio = db.query(Audio).filter(Audio.filename == file).first()
if existing_audio:
print(f" Файл {file} уже есть в БД, пропускаем")
continue
# Создаём новую запись
audio = Audio()
audio.index_date = datetime.datetime.now()
audio.filename = file
audio.file_size = file_size
db.add(audio)
db.flush() # Чтобы получить audio.id
# Сохраняем в список для последующей отправки
self.loaded_files.append(audio)
print(f"✅ Файл {file} сохранён в БД")
except Exception as e:
print(f"❌ Ошибка при обработке файла {remote_file}: {e}")
# Если файл скачался, но ошибка в БД - удаляем файл
if os.path.exists(filepath):
try:
os.remove(filepath)
print(f"🗑 Файл {file} удалён из-за ошибки")
except:
pass
finally:
# Закрываем соединения
sftp_client.close()
ssh_client.close()
# После загрузки всех файлов отправляем на распознавание
print(f"\n📊 Загружено файлов: {len(self.loaded_files)}")
self.send_to_recognition()

388
autoLoader/loader/recognition_checker.py

@ -0,0 +1,388 @@
"""
Исправленная версия recognition_checker.py
Избегает detached instance error путём использования словарей вместо SQLAlchemy объектов
"""
import requests
from sqlalchemy import inspect
from typing import List, Optional, Dict, Any
import logging
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
from autoLoader.database import get_db_session, Audio, AiConclusion
from autoLoader.config import GIGAAM_API_URL
# Настройка логирования
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class RecognitionChecker:
"""Класс для проверки и отправки файлов на распознавание"""
def __init__(self, api_url: Optional[str] = None, max_workers: int = 5):
"""
Инициализация checker
Args:
api_url: URL API GigaAM для распознавания (если None, берётся из config)
max_workers: Максимальное количество параллельных запросов
"""
# Если api_url не передан, берём из config.py
if api_url is None:
api_url = GIGAAM_API_URL
self.api_url = f"{api_url}/api/call/process"
self.timeout = 10 # таймаут запроса в секундах
self.max_workers = max_workers # количество параллельных потоков
# Thread-safe счётчики для статистики
self._lock = threading.Lock()
self._sent_count = 0
self._failed_count = 0
logger.info(f"✅ RecognitionChecker инициализирован с URL: {self.api_url}")
logger.info(f"📊 Параллельная отправка: до {max_workers} запросов одновременно")
def check_database(self) -> bool:
"""
Проверяет существование необходимых таблиц в БД
Returns:
True если таблицы существуют, иначе False
"""
from autoLoader.database import engine
inspector = inspect(engine)
existing_tables = inspector.get_table_names()
required_tables = ['audio', 'ai_conclusion']
missing_tables = [t for t in required_tables if t not in existing_tables]
if missing_tables:
logger.error(f"❌ Отсутствуют таблицы: {missing_tables}")
return False
return True
def get_files_without_conclusion(self, limit: Optional[int] = None) -> List[Dict[str, Any]]:
"""
Находит все файлы, у которых нет AI заключения
Args:
limit: Ограничение количества файлов (None = все)
Returns:
Список словарей с информацией о файлах (избегаем detached instance)
"""
if not self.check_database():
logger.error("❌ База данных не готова")
return []
try:
with get_db_session() as db:
# Подзапрос: находим все audio_id, у которых есть заключение
from sqlalchemy import distinct
audio_with_conclusion = db.query(
distinct(AiConclusion.audio_id)
).filter(
AiConclusion.audio_id.isnot(None)
).all()
# Извлекаем ID из кортежей
conclusion_ids = [row[0] for row in audio_with_conclusion]
# Находим все audio, у которых нет заключения
if conclusion_ids:
audio_objects = db.query(Audio).filter(
~Audio.id.in_(conclusion_ids)
).all()
else:
# Если заключений нет вообще - все файлы без заключения
audio_objects = db.query(Audio).all()
# Конвертируем в словари внутри сессии БД
files_data = []
for audio in audio_objects:
files_data.append({
'id': str(audio.id),
'filename': audio.filename,
'file_size': audio.file_size,
'index_date': audio.index_date.isoformat() if audio.index_date else None
})
logger.info(f"📊 Найдено файлов без заключения: {len(files_data)}")
if limit:
return files_data[:limit]
return files_data
except Exception as e:
logger.error(f"❌ Ошибка при поиске файлов: {e}")
return []
def send_to_recognition(self, audio_data: Dict[str, Any]) -> bool:
"""
Отправляет файл на распознавание в GigaAM API
Args:
audio_data: Словарь с данными об аудио файле
Returns:
True если успешно отправлен, иначе False
"""
filename = audio_data.get('filename')
payload = {
"filename": filename
}
try:
logger.info(f"📤 [Thread-{threading.current_thread().name}] Отправка файла {filename} на распознавание...")
response = requests.post(
self.api_url,
json=payload,
timeout=self.timeout
)
if response.status_code == 200 or response.status_code == 202:
logger.info(f"✅ [Thread-{threading.current_thread().name}] Файл {filename} успешно отправлен")
# Thread-safe обновление счётчиков
with self._lock:
self._sent_count += 1
return True
else:
logger.error(f"❌ [Thread-{threading.current_thread().name}] Ошибка API {response.status_code}: {response.text}")
# Thread-safe обновление счётчиков
with self._lock:
self._failed_count += 1
return False
except requests.exceptions.Timeout:
logger.error(f"❌ [Thread-{threading.current_thread().name}] Таймаут при отправке файла {filename}")
with self._lock:
self._failed_count += 1
return False
except requests.exceptions.ConnectionError:
logger.error(f"❌ [Thread-{threading.current_thread().name}] Не удалось подключиться к API {self.api_url}")
with self._lock:
self._failed_count += 1
return False
except Exception as e:
logger.error(f"❌ [Thread-{threading.current_thread().name}] Ошибка при отправке {filename}: {e}")
with self._lock:
self._failed_count += 1
return False
def send_to_recognition_parallel(self, audio_list: List[Dict[str, Any]]) -> Dict[str, Any]:
"""
Отправляет несколько файлов на распознавание параллельно
Args:
audio_list: Список словарей с данными об аудио файлах
Returns:
Словарь с результатами отправки
"""
if not audio_list:
logger.info(" Список файлов пуст, нечего отправлять")
return {
"total": 0,
"sent": 0,
"failed": 0,
"files": []
}
# Сбрасываем счётчики
self._sent_count = 0
self._failed_count = 0
logger.info(f"🚀 Начинаем параллельную отправку {len(audio_list)} файлов")
logger.info(f"📊 Количество потоков: {self.max_workers}")
results = {
"total": len(audio_list),
"sent": 0,
"failed": 0,
"files": []
}
# Используем ThreadPoolExecutor для параллельной отправки
with ThreadPoolExecutor(max_workers=self.max_workers, thread_name_prefix="SendReq") as executor:
# Запускаем все задачи
future_to_audio = {
executor.submit(self.send_to_recognition, audio): audio
for audio in audio_list
}
# Обрабатываем результаты по мере завершения
for future in as_completed(future_to_audio):
audio = future_to_audio[future]
try:
success = future.result()
result = {
"filename": audio.get('filename'),
"audio_id": audio.get('id'),
"success": success
}
results["files"].append(result)
except Exception as exc:
logger.error(f"❌ Файл {audio.get('filename')} сгенерировал исключение: {exc}")
result = {
"filename": audio.get('filename'),
"audio_id": audio.get('id'),
"success": False,
"error": str(exc)
}
results["files"].append(result)
# Получаем итоговую статистику из счётчиков
results["sent"] = self._sent_count
results["failed"] = self._failed_count
# Логирование итогов
logger.info(f"📊 Итого параллельной отправки:")
logger.info(f" - Всего: {results['total']}")
logger.info(f" - Отправлено: {results['sent']}")
logger.info(f" - Ошибок: {results['failed']}")
return results
def process_all_pending(self, limit: Optional[int] = None, parallel: bool = True) -> Dict[str, Any]:
"""
Находит и отправляет все файлы без заключения на распознавание
Args:
limit: Максимальное количество файлов для обработки
parallel: Использовать параллельную отправку (по умолчанию True)
Returns:
Словарь с результатами обработки
"""
logger.info("🔍 Поиск файлов без AI заключения...")
files_without_conclusion = self.get_files_without_conclusion(limit)
if not files_without_conclusion:
logger.info("✅ Все файлы обработаны")
return {
"total": 0,
"sent": 0,
"failed": 0,
"files": []
}
# Выбираем метод отправки
if parallel:
logger.info("🚀 Используем параллельную отправку")
return self.send_to_recognition_parallel(files_without_conclusion)
else:
logger.info("📤 Используем последовательную отправку")
results = {
"total": len(files_without_conclusion),
"sent": 0,
"failed": 0,
"files": []
}
for audio in files_without_conclusion:
success = self.send_to_recognition(audio)
result = {
"filename": audio.get('filename'),
"audio_id": audio.get('id'),
"success": success
}
results["files"].append(result)
if success:
results["sent"] += 1
else:
results["failed"] += 1
# Логирование итогов
logger.info(f"📊 Итого:")
logger.info(f" - Всего: {results['total']}")
logger.info(f" - Отправлено: {results['sent']}")
logger.info(f" - Ошибок: {results['failed']}")
return results
def check_api_availability(self) -> bool:
"""
Проверяет доступность GigaAM API
Returns:
True если API доступен, иначе False
"""
try:
# Проверяем health endpoint или просто подключение
response = requests.get(
self.api_url.replace("/process", "/status"), # Пробуем /status
timeout=5
)
if response.status_code in [200, 404]: # 404 тоже ок - API работает
logger.info("✅ GigaAM API доступен")
return True
except requests.exceptions.ConnectionError:
logger.warning(" GigaAM API недоступен")
return False
except Exception as e:
logger.warning(f" Ошибка проверки API: {e}")
return False
return True
# Удобная функция для запуска из командной строки
def process_pending_files(api_url: Optional[str] = None, limit: Optional[int] = None) -> Dict[str, Any]:
"""
Обрабатывает все файлы без заключения
Args:
api_url: URL GigaAM API (если None, берётся из config.py)
limit: Максимальное количество файлов для обработки
Returns:
Результаты обработки
"""
checker = RecognitionChecker(api_url)
# Проверяем доступность API
if not checker.check_api_availability():
logger.error("❌ GigaAM API недоступен. Проверьте, запущен ли сервис.")
return {
"total": 0,
"sent": 0,
"failed": 0,
"error": "API unavailable"
}
# Обрабатываем файлы
return checker.process_all_pending(limit)

389
autoLoader/loader/recognition_checker_old.py

@ -0,0 +1,389 @@
"""
Класс для проверки файлов без AI заключения и отправки на распознавание
"""
import requests
from sqlalchemy import inspect
from typing import List, Optional
import logging
from concurrent.futures import ThreadPoolExecutor, as_completed
import threading
from autoLoader.database import get_db_session, Audio, AiConclusion
from autoLoader.config import GIGAAM_API_URL
# Настройка логирования
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class RecognitionChecker:
"""Класс для проверки и отправки файлов на распознавание"""
def __init__(self, api_url: Optional[str] = None, max_workers: int = 5):
"""
Инициализация checker
Args:
api_url: URL API GigaAM для распознавания (если None, берётся из config)
max_workers: Максимальное количество параллельных запросов
"""
# Если api_url не передан, берём из config.py
if api_url is None:
api_url = GIGAAM_API_URL
self.api_url = f"{api_url}/api/call/process"
self.timeout = 10 # таймаут запроса в секундах
self.max_workers = max_workers # количество параллельных потоков
# Thread-safe счётчики для статистики
self._lock = threading.Lock()
self._sent_count = 0
self._failed_count = 0
logger.info(f"✅ RecognitionChecker инициализирован с URL: {self.api_url}")
logger.info(f"📊 Параллельная отправка: до {max_workers} запросов одновременно")
def check_database(self) -> bool:
"""
Проверяет существование необходимых таблиц в БД
Returns:
True если таблицы существуют, иначе False
"""
from autoLoader.database import engine
inspector = inspect(engine)
existing_tables = inspector.get_table_names()
required_tables = ['audio', 'ai_conclusion']
missing_tables = [t for t in required_tables if t not in existing_tables]
if missing_tables:
logger.error(f"❌ Отсутствуют таблицы: {missing_tables}")
return False
return True
def get_files_without_conclusion(self, limit: Optional[int] = None) -> List[Audio]:
"""
Находит все файлы, у которых нет AI заключения
Args:
limit: Ограничение количества файлов (None = все)
Returns:
Список объектов Audio без заключения
"""
if not self.check_database():
logger.error("❌ База данных не готова")
return []
try:
with get_db_session() as db:
# Подзапрос: находим все audio_id, у которых есть заключение
from sqlalchemy import distinct
audio_with_conclusion = db.query(
distinct(AiConclusion.audio_id)
).filter(
AiConclusion.audio_id.isnot(None)
).all()
# Извлекаем ID из кортежей
conclusion_ids = [row[0] for row in audio_with_conclusion]
# Находим все audio, у которых нет заключения
if conclusion_ids:
files_without_conclusion = db.query(Audio).filter(
~Audio.id.in_(conclusion_ids)
).all()
else:
# Если заключений нет вообще - все файлы без заключения
files_without_conclusion = db.query(Audio).all()
logger.info(f"📊 Найдено файлов без заключения: {len(files_without_conclusion)}")
if limit:
return files_without_conclusion[:limit]
return files_without_conclusion
except Exception as e:
logger.error(f"❌ Ошибка при поиске файлов: {e}")
return []
def send_to_recognition(self, audio: Audio) -> bool:
"""
Отправляет файл на распознавание в GigaAM API
Args:
audio: Объект Audio для распознавания
Returns:
True если успешно отправлен, иначе False
"""
payload = {
"filename": audio.filename
}
try:
logger.info(f"📤 [Thread-{threading.current_thread().name}] Отправка файла {audio.filename} на распознавание...")
response = requests.post(
self.api_url,
json=payload,
timeout=self.timeout
)
if response.status_code == 200 or response.status_code == 202:
logger.info(f"✅ [Thread-{threading.current_thread().name}] Файл {audio.filename} успешно отправлен")
# Thread-safe обновление счётчиков
with self._lock:
self._sent_count += 1
return True
else:
logger.error(f"❌ [Thread-{threading.current_thread().name}] Ошибка API {response.status_code}: {response.text}")
# Thread-safe обновление счётчиков
with self._lock:
self._failed_count += 1
return False
except requests.exceptions.Timeout:
logger.error(f"❌ [Thread-{threading.current_thread().name}] Таймаут при отправке файла {audio.filename}")
with self._lock:
self._failed_count += 1
return False
except requests.exceptions.ConnectionError:
logger.error(f"❌ [Thread-{threading.current_thread().name}] Не удалось подключиться к API {self.api_url}")
with self._lock:
self._failed_count += 1
return False
except Exception as e:
logger.error(f"❌ [Thread-{threading.current_thread().name}] Ошибка при отправке {audio.filename}: {e}")
with self._lock:
self._failed_count += 1
return False
def send_to_recognition_parallel(self, audio_list: List[Audio]) -> dict:
"""
Отправляет несколько файлов на распознавание параллельно
Args:
audio_list: Список объектов Audio для распознавания
Returns:
Словарь с результатами отправки
"""
if not audio_list:
logger.info(" Список файлов пуст, нечего отправлять")
return {
"total": 0,
"sent": 0,
"failed": 0,
"files": []
}
# Сбрасываем счётчики
self._sent_count = 0
self._failed_count = 0
logger.info(f"🚀 Начинаем параллельную отправку {len(audio_list)} файлов")
logger.info(f"📊 Количество потоков: {self.max_workers}")
results = {
"total": len(audio_list),
"sent": 0,
"failed": 0,
"files": []
}
# Используем ThreadPoolExecutor для параллельной отправки
with ThreadPoolExecutor(max_workers=self.max_workers, thread_name_prefix="SendReq") as executor:
# Запускаем все задачи
future_to_audio = {
executor.submit(self.send_to_recognition, audio): audio
for audio in audio_list
}
# Обрабатываем результаты по мере завершения
for future in as_completed(future_to_audio):
audio = future_to_audio[future]
try:
success = future.result()
result = {
"filename": audio.filename,
"audio_id": str(audio.id),
"success": success
}
results["files"].append(result)
except Exception as exc:
logger.error(f"❌ Файл {audio.filename} сгенерировал исключение: {exc}")
result = {
"filename": audio.filename,
"audio_id": str(audio.id),
"success": False,
"error": str(exc)
}
results["files"].append(result)
# Получаем итоговую статистику из счётчиков
results["sent"] = self._sent_count
results["failed"] = self._failed_count
# Логирование итогов
logger.info(f"📊 Итого параллельной отправки:")
logger.info(f" - Всего: {results['total']}")
logger.info(f" - Отправлено: {results['sent']}")
logger.info(f" - Ошибок: {results['failed']}")
return results
def process_all_pending(self, limit: Optional[int] = None, parallel: bool = True) -> dict:
"""
Находит и отправляет все файлы без заключения на распознавание
Args:
limit: Максимальное количество файлов для обработки
parallel: Использовать параллельную отправку (по умолчанию True)
Returns:
Словарь с результатами обработки
"""
logger.info("🔍 Поиск файлов без AI заключения...")
files_without_conclusion = self.get_files_without_conclusion(limit)
if not files_without_conclusion:
logger.info("✅ Все файлы обработаны")
return {
"total": 0,
"sent": 0,
"failed": 0,
"files": []
}
# Выбираем метод отправки
if parallel:
logger.info("🚀 Используем параллельную отправку")
return self.send_to_recognition_parallel(files_without_conclusion)
else:
logger.info("📤 Используем последовательную отправку")
results = {
"total": len(files_without_conclusion),
"sent": 0,
"failed": 0,
"files": []
}
for audio in files_without_conclusion:
success = self.send_to_recognition(audio)
result = {
"filename": audio.filename,
"audio_id": str(audio.id),
"success": success
}
results["files"].append(result)
if success:
results["sent"] += 1
else:
results["failed"] += 1
# Логирование итогов
logger.info(f"📊 Итого:")
logger.info(f" - Всего: {results['total']}")
logger.info(f" - Отправлено: {results['sent']}")
logger.info(f" - Ошибок: {results['failed']}")
return results
def check_api_availability(self) -> bool:
"""
Проверяет доступность GigaAM API
Returns:
True если API доступен, иначе False
"""
try:
# Проверяем health endpoint или просто подключение
response = requests.get(
self.api_url.replace("/process", "/status"), # Пробуем /status
timeout=5
)
if response.status_code in [200, 404]: # 404 тоже ок - API работает
logger.info("✅ GigaAM API доступен")
return True
except requests.exceptions.ConnectionError:
logger.warning(" GigaAM API недоступен")
return False
except Exception as e:
logger.warning(f" Ошибка проверки API: {e}")
return False
return True
# Удобная функция для запуска из командной строки
def process_pending_files(api_url: Optional[str] = None, limit: Optional[int] = None):
"""
Обрабатывает все файлы без заключения
Args:
api_url: URL GigaAM API (если None, берётся из config.py)
limit: Максимальное количество файлов для обработки
Returns:
Результаты обработки
"""
checker = RecognitionChecker(api_url)
# Проверяем доступность API
if not checker.check_api_availability():
logger.error("❌ GigaAM API недоступен. Проверьте, запущен ли сервис.")
return {
"total": 0,
"sent": 0,
"failed": 0,
"error": "API unavailable"
}
# Обрабатываем файлы
return checker.process_all_pending(limit)
if __name__ == "__main__":
# Пример использования
import sys
api_url = sys.argv[1] if len(sys.argv) > 1 else "http://localhost:5001/api/call/process"
limit = int(sys.argv[2]) if len(sys.argv) > 2 else None
results = process_pending_files(api_url, limit)
print(f"\n📊 Результаты:")
print(f"Всего: {results['total']}")
print(f"Отправлено: {results['sent']}")
print(f"Ошибок: {results['failed']}")

39
init_db.py

@ -0,0 +1,39 @@
#!/usr/bin/env python3
"""
Скрипт для инициализации базы данных
Создаёт все необходимые таблицы
"""
from apiApp.database import Base, engine
from sqlalchemy import inspect
def init_database():
"""Создаёт все таблицы в базе данных"""
print("🔧 Инициализация базы данных...")
# Проверяем существующие таблицы
inspector = inspect(engine)
existing_tables = inspector.get_table_names()
if existing_tables:
print(f"📋 Существующие таблицы: {', '.join(existing_tables)}")
# Создаём все таблицы
Base.metadata.create_all(bind=engine)
# Проверяем результат
inspector = inspect(engine)
all_tables = inspector.get_table_names()
print(f"✅ Создано таблиц: {len(all_tables)}")
for table in all_tables:
print(f" - {table}")
return all_tables
if __name__ == "__main__":
try:
tables = init_database()
print(f"\n🎉 База данных готова! Создано таблиц: {len(tables)}")
except Exception as e:
print(f"\n❌ Ошибка при создании таблиц: {e}")
exit(1)

31
main.py

@ -5,9 +5,14 @@ from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.exc import SQLAlchemyError
import logging
from apiApp.config import APP_TITLE, APP_VERSION, API_V1_PREFIX, UPLOAD_FOLDER, DATABASE_URL
from apiApp.config import APP_TITLE, APP_VERSION, API_V1_PREFIX, UPLOAD_FOLDER, DATABASE_URL, PORT, HOST
from apiApp.database import engine, Base
from apiApp.routers import audio_router, recognition_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_management_router import audio_management_router
print("✅ audio_management_router imported successfully")
# Настройка логирования
logging.basicConfig(level=logging.INFO)
@ -48,7 +53,7 @@ async def startup_event():
"""Создание таблиц при запуске приложения"""
try:
Base.metadata.create_all(bind=engine)
logger.info("Database tables created successfully")
logger.info("Database tables created successfully")
except Exception as e:
logger.error(f"Error creating database tables: {e}")
@ -56,6 +61,12 @@ async def startup_event():
# Подключение routers
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(ai_conclusion_router, prefix=API_V1_PREFIX, tags=["ai_conclusion"])
app.include_router(audio_files_router, prefix=API_V1_PREFIX, tags=["audio_files"])
# audio_management_router с префиксом /audio для логической структуры
print("📝 Registering audio_management_router...")
app.include_router(audio_management_router, prefix="/api", tags=["audio_management"])
print("✅ audio_management_router registered at /audio/*")
# Статические файлы (для загрузки аудио)
app.mount("/uploads", StaticFiles(directory=str(UPLOAD_FOLDER)), name="uploads")
@ -74,7 +85,21 @@ async def root():
async def health_check():
return {"status": "healthy"}
@app.get("/routes")
async def list_routes():
"""Отладка: список всех роутов"""
from fastapi.routing import APIRoute
routes = []
for route in app.routes:
if isinstance(route, APIRoute):
routes.append({
"path": route.path,
"methods": list(route.methods),
"name": route.name
})
return {"routes": routes}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
uvicorn.run(app, host=HOST, port=PORT)

3
requirements.txt

@ -4,3 +4,6 @@ sqlalchemy==2.0.35
pydantic==2.9.2
python-multipart==0.0.12
aiofiles==24.1.0
psycopg2-binary
paramiko
requests

3
run.py

@ -5,6 +5,7 @@
import uvicorn
import sys
from pathlib import Path
from apiApp.config import PORT
# >102;O5< :>@=52CN 48@5:B>@8N 2 Python path
sys.path.insert(0, str(Path(__file__).resolve().parent))
@ -13,7 +14,7 @@ if __name__ == "__main__":
uvicorn.run(
"main:app",
host="0.0.0.0",
port=8000,
port=PORT,
reload=True, # 2B><0B8G5A:0O ?5@5703@C7:0 ?@8 87<5=5=88 :>40
log_level="info"
)

65
run_checker.py

@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""
Скрипт для проверки и отправки файлов на распознавание
"""
from autoLoader.loader.recognition_checker import process_pending_files
from autoLoader.config import GIGAAM_API_URL
import sys
import logging
from dotenv import load_dotenv
# Загружаем .env файл
load_dotenv()
# Настройка логирования
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
def main():
"""Главная функция"""
# Получаем параметры из командной строки
api_url = sys.argv[1] if len(sys.argv) > 1 else GIGAAM_API_URL
limit = int(sys.argv[2]) if len(sys.argv) > 2 else None
print(f"🚀 Запуск проверки файлов на распознавание")
print(f"📡 API URL: {api_url}")
print(f"📂 Config URL: {GIGAAM_API_URL}")
if limit:
print(f"📊 Лимит: {limit} файлов")
print()
# Запускаем проверку
results = process_pending_files(api_url=api_url, limit=limit)
# Итоги
print(f"\n{'='*60}")
print(f"📊 ИТОГИ ОБРАБОТКИ:")
print(f"{'='*60}")
print(f"Всего файлов без заключения: {results['total']}")
print(f"✅ Отправлено на распознавание: {results['sent']}")
print(f"❌ Ошибок при отправке: {results['failed']}")
if 'error' in results:
print(f"\n {results['error']}")
# Детали по каждому файлу
if results.get('files'):
print(f"\n📋 Детали:")
for file_result in results['files']:
status = "" if file_result['success'] else ""
print(f" {status} {file_result['filename']}")
print(f"{'='*60}\n")
# Код выхода
if results['failed'] > 0:
sys.exit(1)
else:
sys.exit(0)
if __name__ == "__main__":
main()

13
run_loader.py

@ -0,0 +1,13 @@
from autoLoader.loader import Loader
from apiApp.database import Base, engine
# Создаём таблицы, если они не существуют
print("🔧 Создание таблиц базы данных...")
Base.metadata.create_all(bind=engine)
print("✅ Таблицы созданы")
# Запускаем загрузчик
print("\n🚀 Запуск AutoLoader...")
loader_instance = Loader()
loader_instance.load()
print("✅ AutoLoader завершил работу")
Loading…
Cancel
Save