add transcription history logging

This commit is contained in:
Serge Zaigraeff
2025-03-04 09:55:47 +03:00
parent 450750c47b
commit 8dab730d4a
8 changed files with 887 additions and 167 deletions
+85
View File
@@ -0,0 +1,85 @@
"""
Модуль history_logger.py содержит класс HistoryLogger для журналирования результатов
транскрибации.
"""
import os
import json
import datetime
import random
import string
from typing import Dict, Any, Optional
from .utils import logger
class HistoryLogger:
"""Класс для сохранения истории транскрибации."""
def __init__(self, config: Dict):
"""
Инициализация логгера истории.
Args:
config: Словарь с конфигурацией.
"""
self.config = config
self.history_enabled = config.get("enable_history", False)
self.history_root = os.path.join(os.path.dirname(os.path.dirname(__file__)), "history")
# Создаем корневую директорию истории, если она не существует
if self.history_enabled and not os.path.exists(self.history_root):
os.makedirs(self.history_root)
logger.info(f"Создана директория для истории транскрибации: {self.history_root}")
def save(self, result: Dict[str, Any], original_filename: str) -> Optional[str]:
"""
Сохраняет результат транскрибации в файл истории.
Args:
result: Результат транскрибации.
original_filename: Исходное имя аудиофайла.
Returns:
Путь к сохраненному файлу истории или None, если сохранение отключено.
"""
if not self.history_enabled:
logger.debug("История транскрибации отключена в конфигурации")
return None
try:
# Получаем текущую дату и время
now = datetime.datetime.now()
date_str = now.strftime("%Y-%m-%d")
# Получаем текущий таймстамп в миллисекундах
timestamp_ms = int(datetime.datetime.now().timestamp() * 1000)
# Генерируем 4-символьную случайную метку
random_tag = ''.join(random.choices(string.ascii_lowercase + string.digits, k=4))
# Получаем только имя файла без пути
base_filename = os.path.basename(original_filename)
# Создаем имя файла истории
history_filename = f"{timestamp_ms}_{base_filename}_{random_tag}.json"
# Путь к директории для текущей даты
date_dir = os.path.join(self.history_root, date_str)
# Создаем директорию для текущей даты, если она не существует
if not os.path.exists(date_dir):
os.makedirs(date_dir)
# Полный путь к файлу истории
history_path = os.path.join(date_dir, history_filename)
# Сохраняем результат в JSON файл
with open(history_path, 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False, indent=2)
logger.info(f"Результат транскрибации сохранен в историю: {history_path}")
return history_path
except Exception as e:
logger.error(f"Ошибка при сохранении истории транскрибации: {e}")
return None
+39 -165
View File
@@ -1,158 +1,28 @@
"""
Модуль routes.py содержит классы для обработки транскрибации аудиофайлов
и регистрации маршрутов API для сервиса распознавания речи.
Модуль routes.py содержит классы для регистрации маршрутов API
для сервиса распознавания речи.
"""
import os
import uuid
import tempfile
import time
import librosa
from flask import request, jsonify
from typing import Dict, Tuple
from typing import Dict
from .utils import logger
from .transcriber_service import TranscriptionService
from .audio_sources import (
AudioSource,
UploadedFileSource,
URLSource,
Base64Source,
UploadedFileSource,
URLSource,
Base64Source,
LocalFileSource
)
class TranscriptionService:
"""Сервис для обработки и транскрибации аудиофайлов."""
def __init__(self, transcriber, config: Dict):
"""
Инициализация сервиса транскрибации.
Args:
transcriber: Экземпляр транскрайбера.
config: Словарь с конфигурацией.
"""
self.transcriber = transcriber
self.config = config
self.max_file_size_mb = self.config.get("max_file_size", 100) # Default 100MB
def get_audio_duration(self, file_path: str) -> float:
"""
Определяет длительность аудиофайла в секундах.
Args:
file_path: Путь к аудиофайлу.
Returns:
Длительность в секундах.
"""
try:
y, sr = librosa.load(file_path, sr=None)
duration = librosa.get_duration(y=y, sr=sr)
return duration
except Exception as e:
logger.error(f"Ошибка при определении длительности файла: {e}")
return 0.0
def transcribe_from_source(self, source: AudioSource, params: Dict = None) -> Tuple[Dict, int]:
"""
Транскрибирует аудиофайл из указанного источника.
Args:
source: Источник аудиофайла.
params: Дополнительные параметры для транскрибации.
Returns:
Кортеж (JSON-ответ, HTTP-код).
"""
# Получаем файл из источника
file, filename, error = source.get_audio_file()
# Обрабатываем ошибки получения файла
if error:
return jsonify({"error": error}), 400
if not file:
return jsonify({"error": "Failed to get audio file"}), 400
# Извлекаем параметры из запроса, если они есть
params = params or {}
language = params.get('language', self.config.get('language', 'en'))
temperature = float(params.get('temperature', 0.0))
prompt = params.get('prompt', '')
# Проверяем, запрошены ли временные метки
return_timestamps = params.get('return_timestamps', self.config.get('return_timestamps', False))
# Преобразуем строковое значение в булево, если необходимо
if isinstance(return_timestamps, str):
return_timestamps = return_timestamps.lower() in ('true', 't', 'yes', 'y', '1')
# Временно изменяем настройку return_timestamps в транскрайбере
original_return_timestamps = self.transcriber.return_timestamps
self.transcriber.return_timestamps = return_timestamps
# Сохраняем файл во временный файл
temp_dir = tempfile.mkdtemp()
temp_file_path = os.path.join(temp_dir, str(uuid.uuid4()) + "_" + os.path.basename(filename))
file.save(temp_file_path)
# Определяем длительность аудиофайла
duration = self.get_audio_duration(temp_file_path)
# Для файлов из внешних источников (URL, base64), закрываем их и выполняем очистку
if hasattr(source, 'cleanup'):
file.file.close() # Закрываем файловый объект
source.cleanup() # Очищаем временные файлы источника
try:
start_time = time.time()
result = self.transcriber.process_file(temp_file_path)
processing_time = time.time() - start_time
# Формируем ответ в зависимости от return_timestamps
if return_timestamps:
response = {
"segments": result.get("segments", []),
"text": result.get("text", ""),
"processing_time": processing_time,
"response_size_bytes": len(str(result).encode('utf-8')),
"duration_seconds": duration,
"model": os.path.basename(self.config["model_path"])
}
else:
# Если не запрашивались временные метки, result - это строка
response = {
"text": result,
"processing_time": processing_time,
"response_size_bytes": len(str(result).encode('utf-8')),
"duration_seconds": duration,
"model": os.path.basename(self.config["model_path"])
}
return jsonify(response), 200
except Exception as e:
logger.error(f"Ошибка при транскрибации: {e}")
return jsonify({"error": str(e)}), 500
finally:
# Восстанавливаем оригинальное значение return_timestamps
self.transcriber.return_timestamps = original_return_timestamps
# Очистка временных файлов
if os.path.exists(temp_file_path):
os.remove(temp_file_path)
if os.path.exists(temp_dir):
os.rmdir(temp_dir)
class Routes:
"""Класс для регистрации всех эндпоинтов API."""
def __init__(self, app, transcriber, config: Dict):
"""
Инициализация маршрутов.
Args:
app: Flask-приложение.
transcriber: Экземпляр транскрайбера.
@@ -161,13 +31,13 @@ class Routes:
self.app = app
self.config = config
self.transcription_service = TranscriptionService(transcriber, config)
# Регистрация маршрутов
self._register_routes()
def _register_routes(self):
"""Регистрация всех эндпоинтов."""
@self.app.route('/health', methods=['GET'])
def health_check():
"""Эндпоинт для проверки статуса сервиса."""
@@ -175,25 +45,25 @@ class Routes:
"status": "ok",
"version": self.config.get("version", "1.0.0")
}), 200
@self.app.route('/config', methods=['GET'])
def get_config():
"""Эндпоинт для получения конфигурации сервиса."""
"""Эндпоинт для получения конфигурации сервиса."""
return jsonify(self.config), 200
@self.app.route('/local/transcriptions', methods=['POST'])
def local_transcribe():
"""Эндпоинт для локальной транскрибации файла по пути на сервере."""
data = request.json
if not data or "file_path" not in data:
return jsonify({"error": "No file_path provided"}), 400
file_path = data["file_path"]
source = LocalFileSource(file_path, self.config.get("max_file_size", 100))
return self.transcription_service.transcribe_from_source(source, data)
response, status_code = self.transcription_service.transcribe_from_source(source, data)
return jsonify(response), status_code
@self.app.route('/v1/models', methods=['GET'])
def list_models():
"""Эндпоинт для получения списка доступных моделей."""
@@ -208,7 +78,7 @@ class Routes:
],
"object": "list"
}), 200
@self.app.route('/v1/models/<model_id>', methods=['GET'])
def retrieve_model(model_id):
"""Эндпоинт для получения информации о конкретной модели."""
@@ -224,51 +94,55 @@ class Routes:
"error": "Model not found",
"details": f"Model '{model_id}' does not exist"
}), 404
@self.app.route('/v1/audio/transcriptions', methods=['POST'])
def openai_transcribe_endpoint():
"""Эндпоинт для транскрибации аудиофайла (multipart-форма)."""
source = UploadedFileSource(request.files, self.config.get("max_file_size", 100))
return self.transcription_service.transcribe_from_source(source, request.form)
response, status_code = self.transcription_service.transcribe_from_source(source, request.form)
return jsonify(response), status_code
@self.app.route('/v1/audio/transcriptions/url', methods=['POST'])
def transcribe_from_url():
"""Эндпоинт для транскрибации аудиофайла по URL."""
data = request.json
if not data or "url" not in data:
return jsonify({
"error": "No URL provided",
"details": "Please provide 'url' in the JSON request"
}), 400
url = data["url"]
# Извлекаем параметры транскрибации, если они есть
params = {k: v for k, v in data.items() if k != "url"}
source = URLSource(url, self.config.get("max_file_size", 100))
return self.transcription_service.transcribe_from_source(source, params)
response, status_code = self.transcription_service.transcribe_from_source(source, params)
return jsonify(response), status_code
@self.app.route('/v1/audio/transcriptions/base64', methods=['POST'])
def transcribe_from_base64():
"""Эндпоинт для транскрибации аудио, закодированного в base64."""
data = request.json
if not data or "file" not in data:
return jsonify({
"error": "No base64 file provided",
"details": "Please provide 'file' in the JSON request"
}), 400
base64_data = data["file"]
# Извлекаем параметры транскрибации, если они есть
params = {k: v for k, v in data.items() if k != "file"}
source = Base64Source(base64_data, self.config.get("max_file_size", 100))
return self.transcription_service.transcribe_from_source(source, params)
response, status_code = self.transcription_service.transcribe_from_source(source, params)
return jsonify(response), status_code
@self.app.route('/v1/audio/transcriptions/multipart', methods=['POST'])
def transcribe_multipart():
"""Эндпоинт для транскрибации аудиофайла, загруженного через форму."""
source = UploadedFileSource(request.files, self.config.get("max_file_size", 100))
return self.transcription_service.transcribe_from_source(source, request.form)
response, status_code = self.transcription_service.transcribe_from_source(source, request.form)
return jsonify(response), status_code
+1 -1
View File
@@ -235,4 +235,4 @@ class WhisperTranscriber:
finally:
# Очистка временных файлов
self.audio_processor.cleanup_temp_files(temp_files)
self.audio_processor.cleanup_temp_files(temp_files)
+146
View File
@@ -0,0 +1,146 @@
"""
Модуль transcriber_service.py содержит класс TranscriptionService,
который отвечает за обработку и транскрибацию аудиофайлов.
"""
import os
import uuid
import tempfile
import time
import librosa
from typing import Dict, Tuple
from .utils import logger
from .history_logger import HistoryLogger
from .audio_sources import AudioSource
class TranscriptionService:
"""Сервис для обработки и транскрибации аудиофайлов."""
def __init__(self, transcriber, config: Dict):
"""
Инициализация сервиса транскрибации.
Args:
transcriber: Экземпляр транскрайбера.
config: Словарь с конфигурацией.
"""
self.transcriber = transcriber
self.config = config
self.max_file_size_mb = self.config.get("max_file_size", 100) # Default 100MB
# Объект журналирования
self.history = HistoryLogger(config)
def get_audio_duration(self, file_path: str) -> float:
"""
Определяет длительность аудиофайла в секундах.
Args:
file_path: Путь к аудиофайлу.
Returns:
Длительность в секундах.
"""
try:
y, sr = librosa.load(file_path, sr=None)
duration = librosa.get_duration(y=y, sr=sr)
return duration
except Exception as e:
logger.error(f"Ошибка при определении длительности файла: {e}")
return 0.0
def transcribe_from_source(self, source: AudioSource, params: Dict = None) -> Tuple[Dict, int]:
"""
Транскрибирует аудиофайл из указанного источника.
Args:
source: Источник аудиофайла.
params: Дополнительные параметры для транскрибации.
Returns:
Кортеж (JSON-ответ, HTTP-код).
"""
# Получаем файл из источника
file, filename, error = source.get_audio_file()
# Обрабатываем ошибки получения файла
if error:
return {"error": error}, 400
if not file:
return {"error": "Failed to get audio file"}, 400
# Извлекаем параметры из запроса, если они есть
params = params or {}
language = params.get('language', self.config.get('language', 'en'))
temperature = float(params.get('temperature', 0.0))
prompt = params.get('prompt', '')
# Проверяем, запрошены ли временные метки
return_timestamps = params.get('return_timestamps', self.config.get('return_timestamps', False))
# Преобразуем строковое значение в булево, если необходимо
if isinstance(return_timestamps, str):
return_timestamps = return_timestamps.lower() in ('true', 't', 'yes', 'y', '1')
# Временно изменяем настройку return_timestamps в транскрайбере
original_return_timestamps = self.transcriber.return_timestamps
self.transcriber.return_timestamps = return_timestamps
# Сохраняем файл во временный файл
temp_dir = tempfile.mkdtemp()
temp_file_path = os.path.join(temp_dir, str(uuid.uuid4()) + "_" + os.path.basename(filename))
file.save(temp_file_path)
# Определяем длительность аудиофайла
duration = self.get_audio_duration(temp_file_path)
# Для файлов из внешних источников (URL, base64), закрываем их и выполняем очистку
if hasattr(source, 'cleanup'):
file.file.close() # Закрываем файловый объект
source.cleanup() # Очищаем временные файлы источника
try:
start_time = time.time()
result = self.transcriber.process_file(temp_file_path)
processing_time = time.time() - start_time
# Формируем ответ в зависимости от return_timestamps
if return_timestamps:
response = {
"segments": result.get("segments", []),
"text": result.get("text", ""),
"processing_time": processing_time,
"response_size_bytes": len(str(result).encode('utf-8')),
"duration_seconds": duration,
"model": os.path.basename(self.config["model_path"])
}
else:
# Если не запрашивались временные метки, result - это строка
response = {
"text": result,
"processing_time": processing_time,
"response_size_bytes": len(str(result).encode('utf-8')),
"duration_seconds": duration,
"model": os.path.basename(self.config["model_path"])
}
# Журналирование результата
self.history.save(response, filename)
return response, 200
except Exception as e:
logger.error(f"Ошибка при транскрибации: {e}")
return {"error": str(e)}, 500
finally:
# Восстанавливаем оригинальное значение return_timestamps
self.transcriber.return_timestamps = original_return_timestamps
# Очистка временных файлов
if os.path.exists(temp_file_path):
os.remove(temp_file_path)
if os.path.exists(temp_dir):
os.rmdir(temp_dir)