structute optimization

This commit is contained in:
Serge Zaigraeff
2025-03-02 10:14:15 +03:00
parent e6b6ed52b7
commit de81eba532
5 changed files with 413 additions and 245 deletions
+99 -242
View File
@@ -1,76 +1,32 @@
"""
Модуль routes.py содержит классы для обработки транскрибации аудиофайлов
и регистрации маршрутов API для сервиса распознавания речи.
"""
import os
import uuid
import tempfile
import base64
import requests
import time
from flask import request, jsonify
from typing import Dict
from typing import Dict, Tuple
from .logger import logger
from .audio_sources import (
AudioSource,
UploadedFileSource,
URLSource,
Base64Source,
LocalFileSource
)
class FakeFile:
"""Имитирует файловый объект для унификации обработки из разных источников.
class TranscriptionService:
"""Сервис для обработки и транскрибации аудиофайлов."""
Позволяет обрабатывать файлы из различных источников (локальный путь, URL, base64)
как стандартные файловые объекты Flask, обеспечивая совместимость с существующей
логикой обработки файлов.
Атрибуты:
- file: Исходный файловый объект или поток
- filename: Имя файла для метаданных
Методы эмулируют поведение стандартного файлового объекта:
- read(): Чтение содержимого файла
- seek(): Перемещение позиции чтения
- tell(): Текущая позиция чтения
- name (property): Возвращает имя файла
Пример использования:
>>> with open('audio.wav', 'rb') as f:
>>> fake = FakeFile(f, 'audio.wav')
>>> fake.save('/tmp/copy.wav')
>>> processor.handle_file(fake)
"""
def __init__(self, file, filename):
self.file = file
self.filename = filename
def read(self):
return self.file.read()
def seek(self, offset, whence=0):
self.file.seek(offset, whence)
def tell(self):
return self.file.tell()
def save(self, destination):
"""Сохраняет содержимое файла в указанное место назначения.
Args:
destination (str): Путь для сохранения файла
Реализует совместимость с Flask FileStorage API. После записи
сбрасывает позицию чтения в начало файла для последующих операций.
"""
with open(destination, 'wb') as f:
content = self.file.read()
f.write(content)
self.file.seek(0) # Reset pointer after reading
@property
def name(self):
return self.filename
class AudioFileProcessor:
"""
Класс для обработки аудиофайлов, включая проверку размера, сохранение во временный файл и транскрибацию.
"""
def __init__(self, transcriber, config: Dict):
"""
Инициализация AudioFileProcessor.
Инициализация сервиса транскрибации.
Args:
transcriber: Экземпляр транскрайбера.
config: Словарь с конфигурацией.
@@ -78,47 +34,49 @@ class AudioFileProcessor:
self.transcriber = transcriber
self.config = config
self.max_file_size_mb = self.config.get("max_file_size", 100) # Default 100MB
def process_audio_file(self, file, request_form=None):
def transcribe_from_source(self, source: AudioSource, params: Dict = None) -> Tuple[Dict, int]:
"""
Обрабатывает аудиофайл: проверяет размер, сохраняет во временный файл и транскрибирует.
Транскрибирует аудиофайл из указанного источника.
Args:
file: Объект файла, полученный из запроса.
request_form: Объект request.form, если есть параметры из формы.
source: Источник аудиофайла.
params: Дополнительные параметры для транскрибации.
Returns:
jsonify: JSON-ответ с результатом транскрибации.
Кортеж (JSON-ответ, HTTP-код).
"""
# Получаем файл из источника
file, filename, error = source.get_audio_file()
# Обрабатываем ошибки получения файла
if error:
return jsonify({"error": error}), 400
if not file:
return jsonify({"error": "No file part"}), 400
if file.filename == '':
return jsonify({"error": "No selected file"}), 400
# Проверка размера файла
file.seek(0, os.SEEK_END)
file_length = file.tell()
file.seek(0) # Reset file pointer after reading for size check
if file_length > self.max_file_size_mb * 1024 * 1024:
return jsonify({"error": f"File exceeds maximum size of {self.max_file_size_mb}MB"}), 413
return jsonify({"error": "Failed to get audio file"}), 400
# Извлекаем параметры из запроса, если они есть
language = request_form.get('language', self.config.get('language', 'en')) if request_form else self.config.get('language', 'en') # Default language
temperature = float(request_form.get('temperature', 0.0)) if request_form else 0.0 # Default temperature
prompt = request_form.get('prompt', '') if request_form else '' # Default prompt
params = params or {}
language = params.get('language', self.config.get('language', 'en'))
temperature = float(params.get('temperature', 0.0))
prompt = params.get('prompt', '')
# Сохраняем файл во временный файл
temp_dir = tempfile.mkdtemp()
temp_file_path = os.path.join(temp_dir, str(uuid.uuid4()) + "_" + os.path.basename(file.filename))
temp_file_path = os.path.join(temp_dir, str(uuid.uuid4()) + "_" + os.path.basename(filename))
file.save(temp_file_path)
# Для файлов из внешних источников (URL, base64), закрываем их и выполняем очистку
if hasattr(source, 'cleanup'):
file.file.close() # Закрываем файловый объект
source.cleanup() # Очищаем временные файлы источника
try:
start_time = time.time()
text = self.transcriber.process_file(temp_file_path)
processing_time = time.time() - start_time
# Форматируем ответ в стиле OpenAI
return jsonify({
"text": text,
@@ -126,57 +84,26 @@ class AudioFileProcessor:
"response_size_bytes": len(text.encode('utf-8')),
"model": os.path.basename(self.config["model_path"])
}), 200
except Exception as e:
logger.error(f"Ошибка при транскрибации: {e}")
return jsonify({"error": str(e)}), 500
finally:
# Очистка временных файлов
os.remove(temp_file_path)
os.rmdir(temp_dir)
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.
Этот класс содержит все маршруты (endpoints) для взаимодействия с сервером транскрибации.
Он предоставляет функциональность для получения списка доступных моделей, информации о конкретной модели,
а также для транскрибации аудиофайлов, загруженных различными способами.
Эндпоинты:
- GET /v1/models:
Возвращает JSON-список доступных моделей для транскрибации. Каждая модель содержит информацию об ID,
типе объекта, владельце и разрешениях.
- GET /v1/models/<model_id>:
Возвращает JSON-объект с информацией о конкретной модели, идентифицированной по <model_id>.
Если модель не найдена, возвращает ошибку 404.
- POST /v1/audio/transcriptions:
Транскрибирует аудиофайл, загруженный через форму. Ожидает, что файл будет передан в поле 'file'
multipart формы. Возвращает JSON с транскрибированным текстом и временем обработки.
Поддерживает параметры language, temperature и prompt, передаваемые также через форму.
- POST /v1/audio/transcriptions/url:
Транскрибирует аудиофайл, доступный по указанному URL. Ожидает JSON-запрос с полем 'url',
содержащим URL аудиофайла. Возвращает JSON с транскрибированным текстом и временем обработки.
- POST /v1/audio/transcriptions/base64:
Транскрибирует аудиофайл, закодированный в base64. Ожидает JSON-запрос с полем 'file',
содержащим base64-encoded представление аудиофайла. Возвращает JSON с транскрибированным текстом
и временем обработки.
- POST /v1/audio/transcriptions/multipart:
Аналогичен /v1/audio/transcriptions, но явно указывает на то, что файл ожидается в multipart форме.
Используется для транскрибации аудиофайла, загруженного через multipart-форму.
Возвращает JSON с транскрибированным текстом и временем обработки.
Поддерживает параметры language, temperature и prompt, передаваемые также через форму.
"""
"""Класс для регистрации всех эндпоинтов API."""
def __init__(self, app, transcriber, config: Dict):
"""
Инициализация маршрутов.
Args:
app: Flask-приложение.
transcriber: Экземпляр транскрайбера.
@@ -184,22 +111,27 @@ class Routes:
"""
self.app = app
self.config = config
self.audio_processor = AudioFileProcessor(transcriber, config)
self.transcription_service = TranscriptionService(transcriber, config)
# Регистрация маршрутов
self._register_routes()
def _register_routes(self):
"""Регистрация всех эндпоинтов."""
@self.app.route('/health', methods=['GET'])
def health_check():
"""Эндпоинт для проверки статуса сервиса и получения конфигурации."""
"""Эндпоинт для проверки статуса сервиса."""
return jsonify({
"status": "ok",
"config": self.config
"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():
"""Эндпоинт для локальной транскрибации файла по пути на сервере."""
@@ -207,32 +139,19 @@ class Routes:
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)
if not os.path.exists(file_path):
return jsonify({"error": "File not found"}), 400
try:
with open(file_path, 'rb') as f:
# Создаем объект файла, совместимый с обработчиком
fake_file = FakeFile(f, os.path.basename(file_path))
return self.audio_processor.process_audio_file(fake_file)
except Exception as e:
logger.error(f"Ошибка локальной транскрибации: {e}")
return jsonify({
"error": "Processing error",
"details": str(e)
}), 500
@self.app.route('/v1/models', methods=['GET'])
def list_models():
"""Эндпоинт для получения списка доступных моделей."""
return jsonify({
"data": [
{
"id": os.path.basename(self.config["model_path"]), # Имя модели из конфига
"id": os.path.basename(self.config["model_path"]),
"object": "model",
"owned_by": "openai",
"permissions": []
@@ -240,7 +159,7 @@ class Routes:
],
"object": "list"
}), 200
@self.app.route('/v1/models/<model_id>', methods=['GET'])
def retrieve_model(model_id):
"""Эндпоинт для получения информации о конкретной модели."""
@@ -256,113 +175,51 @@ 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():
"""Эндпоинт для транскрибации аудиофайла."""
if 'file' not in request.files:
return jsonify({"error": "No file part"}), 400
file = request.files['file']
return self.audio_processor.process_audio_file(file, request.form)
"""Эндпоинт для транскрибации аудиофайла (multipart-форма)."""
source = UploadedFileSource(request.files, self.config.get("max_file_size", 100))
return self.transcription_service.transcribe_from_source(source, request.form)
@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"]
try:
# Скачиваем файл по URL
response = requests.get(url, stream=True)
response.raise_for_status()
# Сохраняем файл во временный файл
temp_dir = tempfile.mkdtemp()
temp_file_path = os.path.join(temp_dir, str(uuid.uuid4()) + ".wav")
with open(temp_file_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
# Открываем файл для обработки
with open(temp_file_path, 'rb') as file:
# Создаем объект файла, как будто он пришел из request.files
fake_file = FakeFile(file, os.path.basename(temp_file_path))
result = self.audio_processor.process_audio_file(fake_file)
return result
except Exception as e:
logger.error(f"Ошибка при транскрибации файла по URL {url}: {e}")
return jsonify({
"error": "Transcription error",
"details": str(e)
}), 500
finally:
# Очистка временных файлов
if os.path.exists(temp_file_path):
os.remove(temp_file_path)
if os.path.exists(temp_dir):
os.rmdir(temp_dir)
# Извлекаем параметры транскрибации, если они есть
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)
@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"]
try:
# Декодируем base64
audio_data = base64.b64decode(base64_data)
# Сохраняем файл во временный файл
temp_dir = tempfile.mkdtemp()
temp_file_path = os.path.join(temp_dir, str(uuid.uuid4()) + ".wav")
with open(temp_file_path, 'wb') as f:
f.write(audio_data)
# Открываем файл для обработки
with open(temp_file_path, 'rb') as file:
# Создаем объект файла, как будто он пришел из request.files
fake_file = FakeFile(file, os.path.basename(temp_file_path))
result = self.audio_processor.process_audio_file(fake_file)
return result
except Exception as e:
logger.error(f"Ошибка при транскрибации файла из base64: {e}")
return jsonify({
"error": "Transcription error",
"details": str(e)
}), 500
finally:
# Очистка временных файлов
if os.path.exists(temp_file_path):
os.remove(temp_file_path)
if os.path.exists(temp_dir):
os.rmdir(temp_dir)
# Извлекаем параметры транскрибации, если они есть
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)
@self.app.route('/v1/audio/transcriptions/multipart', methods=['POST'])
def transcribe_multipart():
"""Эндпоинт для транскрибации аудиофайла, загруженного через форму."""
if 'file' not in request.files:
return jsonify({"error": "No file part"}), 400
file = request.files['file']
return self.audio_processor.process_audio_file(file, request.form)
source = UploadedFileSource(request.files, self.config.get("max_file_size", 100))
return self.transcription_service.transcribe_from_source(source, request.form)