Fix code review issues: security, race conditions, resource leaks
- Fix race condition: pass return_timestamps as parameter instead of mutating shared state - Remove /local/transcriptions endpoint (path traversal vulnerability) - Add timeout=30 and URL scheme validation to prevent SSRF - Add temp file cleanup via try/finally in all route handlers - AsyncTaskManager: add threading.Lock and TTL cleanup for completed tasks - Change audio_rate to 16000 (matches Whisper's expected sample rate) - Extract filename from URL via Content-Disposition/path - Detect base64 audio format via python-magic - Fix response_size_bytes to use json.dumps instead of str() - Move scipy import to module level - Clamp temperature to [0.0, 1.0] - Deduplicate _load_model() code - Fix docstrings, README structure, typo, log level Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
ff4bd2ba3d
commit
2138651474
@@ -224,13 +224,20 @@ The project consists of the following components:
|
||||
- `config.json`: Service configuration file
|
||||
- `app/`: Main application module
|
||||
- `__init__.py`: Contains the `WhisperServiceAPI` class for service initialization
|
||||
- `utils.py`: Logging configuration
|
||||
- `transcriber.py`: Contains the `WhisperTranscriber` class for speech recognition
|
||||
- `audio_processor.py`: Contains the `AudioProcessor` class for audio preprocessing
|
||||
- `audio_sources.py`: Contains different audio source handlers (upload, URL, base64, local)
|
||||
- `transcriber_service.py`: Manages the transcription workflow
|
||||
- `history_logger.py`: Handles saving transcription history
|
||||
- `routes.py`: Contains the API route definitions
|
||||
- `routes.py`: API route definitions
|
||||
- `history.py`: Saving transcription history
|
||||
- `core/`: Core logic
|
||||
- `transcriber.py`: `WhisperTranscriber` class for speech recognition
|
||||
- `transcription_service.py`: Manages the transcription workflow
|
||||
- `audio/`: Audio processing
|
||||
- `processor.py`: `AudioProcessor` class for audio preprocessing
|
||||
- `sources.py`: Audio source handlers (upload, URL, base64)
|
||||
- `utils.py`: Audio utilities (loading, duration)
|
||||
- `infrastructure/`: Supporting modules
|
||||
- `log.py`: Logging configuration
|
||||
- `validation.py`: File validation
|
||||
- `storage.py`: Temp file management
|
||||
- `async_tasks.py`: Async task manager
|
||||
- `static/`: Web interface files
|
||||
|
||||
## Advanced usage
|
||||
|
||||
@@ -77,7 +77,7 @@ class AudioProcessor:
|
||||
output_path
|
||||
]
|
||||
|
||||
logger.info(f"Конвертация в WAV: {' '.join(cmd)}")
|
||||
logger.debug(f"Конвертация в WAV: {' '.join(cmd)}")
|
||||
|
||||
try:
|
||||
subprocess.run(cmd, check=True, capture_output=True)
|
||||
|
||||
+33
-27
@@ -1,12 +1,14 @@
|
||||
"""
|
||||
Модуль sources.py содержит функции для получения аудиофайлов
|
||||
из различных источников (загруженные файлы, URL, base64, локальные файлы).
|
||||
из различных источников (загруженные файлы, URL, base64).
|
||||
"""
|
||||
|
||||
import os
|
||||
import uuid
|
||||
import tempfile
|
||||
import base64
|
||||
from urllib.parse import urlparse
|
||||
import magic
|
||||
import requests
|
||||
from typing import Tuple, Optional
|
||||
import logging
|
||||
@@ -66,7 +68,11 @@ def get_url_file(url: str, max_file_size_mb: int = 100) -> Tuple[Optional[str],
|
||||
Кортеж (путь к temp-файлу, имя файла, сообщение об ошибке).
|
||||
"""
|
||||
try:
|
||||
response = requests.get(url, stream=True)
|
||||
parsed = urlparse(url)
|
||||
if parsed.scheme not in ('http', 'https'):
|
||||
return None, None, f"Unsupported URL scheme: {parsed.scheme}. Only http/https allowed"
|
||||
|
||||
response = requests.get(url, stream=True, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
# Проверка размера по Content-Length
|
||||
@@ -76,12 +82,22 @@ def get_url_file(url: str, max_file_size_mb: int = 100) -> Tuple[Optional[str],
|
||||
if error:
|
||||
return None, None, error
|
||||
|
||||
# Извлекаем имя файла из Content-Disposition или URL
|
||||
original_name = None
|
||||
cd = response.headers.get('Content-Disposition', '')
|
||||
if 'filename=' in cd:
|
||||
original_name = cd.split('filename=')[-1].strip('" ')
|
||||
if not original_name:
|
||||
url_path = parsed.path.rstrip('/')
|
||||
if url_path:
|
||||
original_name = os.path.basename(url_path)
|
||||
|
||||
temp_path = _make_temp_path()
|
||||
with open(temp_path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
f.write(chunk)
|
||||
|
||||
return temp_path, os.path.basename(temp_path), None
|
||||
return temp_path, original_name or os.path.basename(temp_path), None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении файла по URL {url}: {e}")
|
||||
@@ -102,7 +118,20 @@ def get_base64_file(base64_data: str, max_file_size_mb: int = 100) -> Tuple[Opti
|
||||
if error:
|
||||
return None, None, error
|
||||
|
||||
temp_path = _make_temp_path()
|
||||
# Определяем формат по содержимому
|
||||
mime_to_ext = {
|
||||
"audio/mpeg": ".mp3",
|
||||
"audio/ogg": ".ogg",
|
||||
"audio/flac": ".flac",
|
||||
"audio/mp4": ".m4a",
|
||||
"audio/x-m4a": ".m4a",
|
||||
"audio/aac": ".aac",
|
||||
"audio/webm": ".webm",
|
||||
}
|
||||
detected_mime = magic.from_buffer(audio_data[:1024], mime=True)
|
||||
suffix = mime_to_ext.get(detected_mime, ".wav")
|
||||
|
||||
temp_path = _make_temp_path(suffix)
|
||||
with open(temp_path, 'wb') as f:
|
||||
f.write(audio_data)
|
||||
|
||||
@@ -111,26 +140,3 @@ def get_base64_file(base64_data: str, max_file_size_mb: int = 100) -> Tuple[Opti
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при декодировании base64 данных: {e}")
|
||||
return None, None, f"Error decoding base64 data: {str(e)}"
|
||||
|
||||
|
||||
def get_local_file(file_path: str, max_file_size_mb: int = 100) -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
||||
"""
|
||||
Получает локальный аудиофайл.
|
||||
|
||||
Returns:
|
||||
Кортеж (путь к файлу, имя файла, сообщение об ошибке).
|
||||
Примечание: возвращает оригинальный путь, не копирует файл.
|
||||
"""
|
||||
if not os.path.exists(file_path):
|
||||
return None, None, f"File not found: {file_path}"
|
||||
|
||||
try:
|
||||
error = _check_size(os.path.getsize(file_path), max_file_size_mb)
|
||||
if error:
|
||||
return None, None, error
|
||||
|
||||
return file_path, os.path.basename(file_path), None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при открытии локального файла {file_path}: {e}")
|
||||
return None, None, f"Error opening local file: {str(e)}"
|
||||
|
||||
+2
-2
@@ -6,6 +6,7 @@ import os
|
||||
import subprocess
|
||||
import wave
|
||||
import numpy as np
|
||||
from scipy.signal import resample as scipy_resample
|
||||
import logging
|
||||
from typing import Tuple
|
||||
|
||||
@@ -33,9 +34,8 @@ def load_audio(file_path: str, sr: int = 16000) -> Tuple[np.ndarray, int]:
|
||||
sampling_rate = wav_file.getframerate()
|
||||
|
||||
if sampling_rate != sr:
|
||||
from scipy.signal import resample
|
||||
num_samples = int(len(audio_array) * sr / sampling_rate)
|
||||
audio_array = resample(audio_array, num_samples)
|
||||
audio_array = scipy_resample(audio_array, num_samples)
|
||||
sampling_rate = sr
|
||||
|
||||
return audio_array, sampling_rate
|
||||
|
||||
+23
-25
@@ -117,32 +117,25 @@ class WhisperTranscriber:
|
||||
"""
|
||||
logger.info(f"Загрузка модели из {self.model_path}")
|
||||
|
||||
model_kwargs = dict(
|
||||
torch_dtype=self.torch_dtype,
|
||||
low_cpu_mem_usage=True,
|
||||
use_safetensors=True,
|
||||
)
|
||||
|
||||
try:
|
||||
# Проверка возможности использования Flash Attention 2
|
||||
if self.device.type == "cuda":
|
||||
self.model = WhisperForConditionalGeneration.from_pretrained(
|
||||
self.model_path,
|
||||
torch_dtype=self.torch_dtype,
|
||||
low_cpu_mem_usage=True,
|
||||
use_safetensors=True,
|
||||
attn_implementation="flash_attention_2"
|
||||
).to(self.device)
|
||||
model_kwargs["attn_implementation"] = "flash_attention_2"
|
||||
self.model = WhisperForConditionalGeneration.from_pretrained(
|
||||
self.model_path, **model_kwargs
|
||||
).to(self.device)
|
||||
if self.device.type == "cuda":
|
||||
logger.info("Используется Flash Attention 2")
|
||||
else:
|
||||
self.model = WhisperForConditionalGeneration.from_pretrained(
|
||||
self.model_path,
|
||||
torch_dtype=self.torch_dtype,
|
||||
low_cpu_mem_usage=True,
|
||||
use_safetensors=True
|
||||
).to(self.device)
|
||||
except Exception as e:
|
||||
logger.warning(f"Не удалось загрузить модель с Flash Attention: {e}")
|
||||
# Fallback к обычной версии
|
||||
model_kwargs.pop("attn_implementation", None)
|
||||
self.model = WhisperForConditionalGeneration.from_pretrained(
|
||||
self.model_path,
|
||||
torch_dtype=self.torch_dtype,
|
||||
low_cpu_mem_usage=True,
|
||||
use_safetensors=True
|
||||
self.model_path, **model_kwargs
|
||||
).to(self.device)
|
||||
|
||||
self.processor = WhisperProcessor.from_pretrained(self.model_path)
|
||||
@@ -161,18 +154,22 @@ class WhisperTranscriber:
|
||||
|
||||
logger.info("Модель успешно загружена и готова к использованию")
|
||||
|
||||
def transcribe(self, audio_path: str) -> Union[str, Dict]:
|
||||
def transcribe(self, audio_path: str, return_timestamps: bool = None) -> Union[str, Dict]:
|
||||
"""
|
||||
Транскрибация аудиофайла.
|
||||
|
||||
Args:
|
||||
audio_path: Путь к обработанному аудиофайлу.
|
||||
return_timestamps: Флаг возврата временных меток. Если None — берётся из конфига.
|
||||
|
||||
Returns:
|
||||
В зависимости от параметра return_timestamps:
|
||||
- Если return_timestamps=False: строка с распознанным текстом
|
||||
- Если return_timestamps=True: словарь с ключами "segments" (список словарей с ключами start_time_ms, end_time_ms, text) и "text" (полный текст)
|
||||
"""
|
||||
if return_timestamps is None:
|
||||
return_timestamps = self.return_timestamps
|
||||
|
||||
logger.info(f"Начало транскрибации файла: {audio_path}")
|
||||
|
||||
try:
|
||||
@@ -187,11 +184,11 @@ class WhisperTranscriber:
|
||||
"max_new_tokens": self.max_new_tokens,
|
||||
"temperature": self.temperature
|
||||
},
|
||||
return_timestamps=self.return_timestamps
|
||||
return_timestamps=return_timestamps
|
||||
)
|
||||
|
||||
# Если временные метки не запрошены, возвращаем только текст
|
||||
if not self.return_timestamps:
|
||||
if not return_timestamps:
|
||||
transcribed_text = result.get("text", "")
|
||||
logger.info(f"Транскрибация завершена: получено {len(transcribed_text)} символов текста")
|
||||
return transcribed_text
|
||||
@@ -241,12 +238,13 @@ class WhisperTranscriber:
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
raise
|
||||
|
||||
def process_file(self, input_path: str) -> Union[str, Dict]:
|
||||
def process_file(self, input_path: str, return_timestamps: bool = None) -> Union[str, Dict]:
|
||||
"""
|
||||
Полный процесс обработки и транскрибации аудиофайла.
|
||||
|
||||
Args:
|
||||
input_path: Путь к исходному аудиофайлу.
|
||||
return_timestamps: Флаг возврата временных меток. Если None — берётся из конфига.
|
||||
|
||||
Returns:
|
||||
В зависимости от параметра return_timestamps:
|
||||
@@ -263,7 +261,7 @@ class WhisperTranscriber:
|
||||
processed_path, temp_files = self.audio_processor.process_audio(input_path)
|
||||
|
||||
# Транскрибация
|
||||
result = self.transcribe(processed_path)
|
||||
result = self.transcribe(processed_path, return_timestamps=return_timestamps)
|
||||
|
||||
elapsed_time = time.time() - start_time
|
||||
logger.info(f"Обработка и транскрибация завершены за {elapsed_time:.2f} секунд")
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
который отвечает за транскрибацию аудиофайлов.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
@@ -36,7 +37,7 @@ class TranscriptionService:
|
||||
"""
|
||||
params = params or {}
|
||||
language = params.get('language', self.config.get('language', 'en'))
|
||||
temperature = float(params.get('temperature', 0.0))
|
||||
temperature = max(0.0, min(1.0, float(params.get('temperature', 0.0))))
|
||||
prompt = params.get('prompt', '')
|
||||
|
||||
# Проверяем, запрошены ли временные метки
|
||||
@@ -44,10 +45,6 @@ class TranscriptionService:
|
||||
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
|
||||
|
||||
try:
|
||||
# Определяем длительность аудиофайла
|
||||
try:
|
||||
@@ -57,7 +54,7 @@ class TranscriptionService:
|
||||
return {"error": f"Не удалось определить длительность аудиофайла: {e}"}, 500
|
||||
|
||||
start_time = time.time()
|
||||
result = self.transcriber.process_file(file_path)
|
||||
result = self.transcriber.process_file(file_path, return_timestamps=return_timestamps)
|
||||
processing_time = time.time() - start_time
|
||||
|
||||
# Формируем ответ
|
||||
@@ -66,7 +63,7 @@ class TranscriptionService:
|
||||
"segments": result.get("segments", []),
|
||||
"text": result.get("text", ""),
|
||||
"processing_time": processing_time,
|
||||
"response_size_bytes": len(str(result).encode('utf-8')),
|
||||
"response_size_bytes": len(json.dumps(result, ensure_ascii=False).encode('utf-8')),
|
||||
"duration_seconds": duration,
|
||||
"model": os.path.basename(self.config["model_path"])
|
||||
}
|
||||
@@ -74,7 +71,7 @@ class TranscriptionService:
|
||||
response = {
|
||||
"text": result,
|
||||
"processing_time": processing_time,
|
||||
"response_size_bytes": len(str(result).encode('utf-8')),
|
||||
"response_size_bytes": len(json.dumps(result, ensure_ascii=False).encode('utf-8')),
|
||||
"duration_seconds": duration,
|
||||
"model": os.path.basename(self.config["model_path"])
|
||||
}
|
||||
@@ -86,6 +83,3 @@ class TranscriptionService:
|
||||
logger.error(f"Ошибка при транскрибации файла '{filename}': {str(e)}")
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
return {"error": str(e)}, 500
|
||||
|
||||
finally:
|
||||
self.transcriber.return_timestamps = original_return_timestamps
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Модуль history_logger.py — сохранение истории транскрибации в JSON-файлы.
|
||||
Модуль history.py — сохранение истории транскрибации в JSON-файлы.
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -13,7 +13,7 @@ import logging
|
||||
logger = logging.getLogger('app.history')
|
||||
|
||||
# Корневая директория истории (относительно корня проекта)
|
||||
_history_root = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "history")
|
||||
_history_root = os.path.join(os.path.dirname(os.path.dirname(__file__)), "history")
|
||||
|
||||
|
||||
def save_history(result: Dict[str, Any], original_filename: str, config: Dict) -> Optional[str]:
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"""
|
||||
Модуль manager.py содержит функции для асинхронной обработки задач.
|
||||
Модуль async_tasks.py содержит функции для асинхронной обработки задач.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
import time
|
||||
from typing import Dict, Any, Callable, Optional
|
||||
from threading import Thread
|
||||
from threading import Thread, Lock
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger('app.async_tasks')
|
||||
@@ -19,10 +19,13 @@ class AsyncTaskManager:
|
||||
tasks (Dict): Словарь для хранения информации о задачах.
|
||||
"""
|
||||
|
||||
TASK_TTL = 3600 # 1 час
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Инициализация менеджера асинхронных задач.
|
||||
"""
|
||||
self._lock = Lock()
|
||||
self.tasks: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
def run_task(self, func: Callable, *args, **kwargs) -> str:
|
||||
@@ -39,23 +42,33 @@ class AsyncTaskManager:
|
||||
"""
|
||||
task_id = str(uuid.uuid4())
|
||||
|
||||
# Создание информации о задаче
|
||||
self.tasks[task_id] = {
|
||||
"status": "pending",
|
||||
"result": None,
|
||||
"error": None,
|
||||
"created_at": time.time(),
|
||||
"started_at": None,
|
||||
"completed_at": None
|
||||
}
|
||||
with self._lock:
|
||||
self._cleanup_old_tasks()
|
||||
self.tasks[task_id] = {
|
||||
"status": "pending",
|
||||
"result": None,
|
||||
"error": None,
|
||||
"created_at": time.time(),
|
||||
"started_at": None,
|
||||
"completed_at": None
|
||||
}
|
||||
|
||||
# Создание и запуск потока
|
||||
thread = Thread(target=self._run_task_thread, args=(task_id, func, args, kwargs))
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
return task_id
|
||||
|
||||
def _cleanup_old_tasks(self) -> None:
|
||||
"""Удаляет завершённые задачи старше TASK_TTL. Вызывать под self._lock."""
|
||||
now = time.time()
|
||||
expired = [
|
||||
tid for tid, info in self.tasks.items()
|
||||
if info["completed_at"] and now - info["completed_at"] > self.TASK_TTL
|
||||
]
|
||||
for tid in expired:
|
||||
del self.tasks[tid]
|
||||
|
||||
def _run_task_thread(self, task_id: str, func: Callable, args: tuple, kwargs: dict) -> None:
|
||||
"""
|
||||
Функция для выполнения задачи в потоке.
|
||||
@@ -66,25 +79,24 @@ class AsyncTaskManager:
|
||||
args: Позиционные аргументы для функции.
|
||||
kwargs: Именованные аргументы для функции.
|
||||
"""
|
||||
try:
|
||||
# Обновление статуса задачи
|
||||
with self._lock:
|
||||
self.tasks[task_id]["status"] = "running"
|
||||
self.tasks[task_id]["started_at"] = time.time()
|
||||
|
||||
# Выполнение функции
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
|
||||
# Сохранение результата
|
||||
self.tasks[task_id]["status"] = "completed"
|
||||
self.tasks[task_id]["result"] = result
|
||||
self.tasks[task_id]["completed_at"] = time.time()
|
||||
with self._lock:
|
||||
self.tasks[task_id]["status"] = "completed"
|
||||
self.tasks[task_id]["result"] = result
|
||||
self.tasks[task_id]["completed_at"] = time.time()
|
||||
|
||||
logger.info(f"Задача {task_id} завершена успешно")
|
||||
except Exception as e:
|
||||
# Обработка ошибки
|
||||
self.tasks[task_id]["status"] = "failed"
|
||||
self.tasks[task_id]["error"] = str(e)
|
||||
self.tasks[task_id]["completed_at"] = time.time()
|
||||
with self._lock:
|
||||
self.tasks[task_id]["status"] = "failed"
|
||||
self.tasks[task_id]["error"] = str(e)
|
||||
self.tasks[task_id]["completed_at"] = time.time()
|
||||
|
||||
logger.error(f"Задача {task_id} завершилась с ошибкой: {e}")
|
||||
|
||||
@@ -98,7 +110,9 @@ class AsyncTaskManager:
|
||||
Returns:
|
||||
Информация о задаче или None, если задача не найдена.
|
||||
"""
|
||||
return self.tasks.get(task_id)
|
||||
with self._lock:
|
||||
task = self.tasks.get(task_id)
|
||||
return dict(task) if task else None
|
||||
|
||||
|
||||
# Глобальный экземпляр менеджера асинхронных задач
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""
|
||||
Модуль validators.py содержит классы и функции для валидации входных данных.
|
||||
Модуль validation.py содержит классы и функции для валидации входных данных.
|
||||
"""
|
||||
|
||||
import os
|
||||
import magic
|
||||
from typing import Dict, List, BinaryIO, Optional
|
||||
from typing import Dict, List, BinaryIO
|
||||
import logging
|
||||
|
||||
# Получаем логгер из централизованной настройки
|
||||
@@ -186,35 +186,3 @@ class FileValidator:
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def validate_local_file_path(file_path: str, allowed_directories: Optional[List[str]] = None) -> str:
|
||||
"""
|
||||
Валидирует путь к локальному файлу для предотвращения атак обхода пути.
|
||||
|
||||
Args:
|
||||
file_path: Путь к файлу.
|
||||
allowed_directories: Список разрешенных директорий.
|
||||
|
||||
Returns:
|
||||
Нормализованный и проверенный путь к файлу.
|
||||
|
||||
Raises:
|
||||
ValidationError: Если путь к файлу небезопасен.
|
||||
"""
|
||||
# Нормализация пути
|
||||
normalized_path = os.path.normpath(file_path)
|
||||
|
||||
# Если указаны разрешенные директории, проверяем, что путь находится в одной из них
|
||||
if allowed_directories:
|
||||
for allowed_dir in allowed_directories:
|
||||
full_allowed_path = os.path.abspath(allowed_dir)
|
||||
full_file_path = os.path.abspath(os.path.join(full_allowed_path, normalized_path))
|
||||
|
||||
if full_file_path.startswith(full_allowed_path):
|
||||
return full_file_path
|
||||
|
||||
logger.warning(f"Попытка доступа к файлу вне разрешенных директорий: {file_path}")
|
||||
raise ValidationError("Путь к файлу не находится в разрешенных директориях")
|
||||
|
||||
# Если разрешенные директории не указаны, просто возвращаем нормализованный путь
|
||||
return normalized_path
|
||||
+16
-38
@@ -9,8 +9,9 @@ from typing import Dict
|
||||
import logging
|
||||
|
||||
from .core.transcription_service import TranscriptionService
|
||||
from .audio.sources import get_uploaded_file, get_url_file, get_base64_file, get_local_file
|
||||
from .audio.sources import get_uploaded_file, get_url_file, get_base64_file
|
||||
from .infrastructure.validation import ValidationError
|
||||
from .infrastructure.storage import cleanup_temp_files
|
||||
from .infrastructure.async_tasks import transcribe_audio_async, task_manager
|
||||
|
||||
logger = logging.getLogger('app.routes')
|
||||
@@ -43,33 +44,6 @@ class Routes:
|
||||
"""Эндпоинт для получения конфигурации сервиса."""
|
||||
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"]
|
||||
|
||||
try:
|
||||
validated_path = self.file_validator.validate_local_file_path(
|
||||
file_path,
|
||||
allowed_directories=self.config.get("allowed_directories", [])
|
||||
)
|
||||
except ValidationError as e:
|
||||
client_ip = request.environ.get('HTTP_X_FORWARDED_FOR', request.environ.get('REMOTE_ADDR', 'unknown'))
|
||||
logger.warning(f"Невалидный путь '{file_path}' от {client_ip}: {e}")
|
||||
return jsonify({"error": str(e)}), 400
|
||||
|
||||
temp_path, filename, error = get_local_file(validated_path, self._max_size)
|
||||
if error:
|
||||
return jsonify({"error": error}), 400
|
||||
|
||||
response, status_code = self.transcription_service.transcribe(temp_path, filename, data)
|
||||
return jsonify(response), status_code
|
||||
|
||||
@self.app.route('/v1/models', methods=['GET'])
|
||||
def list_models():
|
||||
"""Эндпоинт для получения списка доступных моделей."""
|
||||
@@ -105,15 +79,15 @@ class Routes:
|
||||
if error:
|
||||
return jsonify({"error": error}), 400
|
||||
|
||||
# Валидация файла
|
||||
try:
|
||||
self.file_validator.validate_file_by_path(temp_path, filename)
|
||||
response, status_code = self.transcription_service.transcribe(temp_path, filename, dict(request.form))
|
||||
return jsonify(response), status_code
|
||||
except ValidationError as e:
|
||||
logger.warning(f"Ошибка валидации файла '{filename}': {e}")
|
||||
return jsonify({"error": str(e)}), 400
|
||||
|
||||
response, status_code = self.transcription_service.transcribe(temp_path, filename, dict(request.form))
|
||||
return jsonify(response), status_code
|
||||
finally:
|
||||
cleanup_temp_files([temp_path])
|
||||
|
||||
@self.app.route('/v1/audio/transcriptions/url', methods=['POST'])
|
||||
def transcribe_from_url():
|
||||
@@ -135,12 +109,13 @@ class Routes:
|
||||
|
||||
try:
|
||||
self.file_validator.validate_file_by_path(temp_path, filename)
|
||||
response, status_code = self.transcription_service.transcribe(temp_path, filename, params)
|
||||
return jsonify(response), status_code
|
||||
except ValidationError as e:
|
||||
logger.warning(f"Ошибка валидации файла '{filename}': {e}")
|
||||
return jsonify({"error": str(e)}), 400
|
||||
|
||||
response, status_code = self.transcription_service.transcribe(temp_path, filename, params)
|
||||
return jsonify(response), status_code
|
||||
finally:
|
||||
cleanup_temp_files([temp_path])
|
||||
|
||||
@self.app.route('/v1/audio/transcriptions/base64', methods=['POST'])
|
||||
def transcribe_from_base64():
|
||||
@@ -162,12 +137,13 @@ class Routes:
|
||||
|
||||
try:
|
||||
self.file_validator.validate_file_by_path(temp_path, filename)
|
||||
response, status_code = self.transcription_service.transcribe(temp_path, filename, params)
|
||||
return jsonify(response), status_code
|
||||
except ValidationError as e:
|
||||
logger.warning(f"Ошибка валидации файла '{filename}': {e}")
|
||||
return jsonify({"error": str(e)}), 400
|
||||
|
||||
response, status_code = self.transcription_service.transcribe(temp_path, filename, params)
|
||||
return jsonify(response), status_code
|
||||
finally:
|
||||
cleanup_temp_files([temp_path])
|
||||
|
||||
@self.app.route('/v1/audio/transcriptions/async', methods=['POST'])
|
||||
def transcribe_async():
|
||||
@@ -179,8 +155,10 @@ class Routes:
|
||||
try:
|
||||
self.file_validator.validate_file_by_path(temp_path, filename)
|
||||
except ValidationError as e:
|
||||
cleanup_temp_files([temp_path])
|
||||
return jsonify({"error": str(e)}), 400
|
||||
|
||||
# Не чистим temp_path здесь — async task отвечает за cleanup
|
||||
task_id = transcribe_audio_async(temp_path, self.transcription_service.transcriber)
|
||||
return jsonify({"task_id": task_id}), 202
|
||||
|
||||
|
||||
+1
-2
@@ -8,7 +8,7 @@
|
||||
"max_new_tokens": 384,
|
||||
"temperature": 0.01,
|
||||
"return_timestamps": false,
|
||||
"audio_rate": 8000,
|
||||
"audio_rate": 16000,
|
||||
"norm_level": "-0.55",
|
||||
"compand_params": "0.3,1 -90,-90,-70,-50,-40,-15,0,0 -7 0 0.15",
|
||||
"device_id": 0,
|
||||
@@ -17,7 +17,6 @@
|
||||
"allowed_extensions": [".wav", ".mp3", ".ogg", ".flac", ".m4a", ".oga", ".aac", ".webm"],
|
||||
"allowed_mime_types": ["audio/wav", "audio/mpeg", "audio/ogg", "audio/flac", "audio/mp4", "audio/x-m4a", "audio/aac", "audio/webm"]
|
||||
},
|
||||
"allowed_directories": [],
|
||||
"log_level": "INFO",
|
||||
"log_file": "logs/whisper_api.log",
|
||||
"request_logging": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Conda environment dependencies
|
||||
|
||||
# Tourch
|
||||
# Torch
|
||||
torch @ https://download.pytorch.org/whl/cu128/torch-2.7.0%2Bcu128-cp313-cp313-manylinux_2_28_x86_64.whl ; platform_system == 'Linux' and platform_machine == 'x86_64' and python_version == '3.13'
|
||||
torch @ https://download.pytorch.org/whl/cu128/torch-2.7.0%2Bcu128-cp312-cp312-manylinux_2_28_x86_64.whl ; platform_system == 'Linux' and platform_machine == 'x86_64' and python_version == '3.12'
|
||||
torch @ https://download.pytorch.org/whl/cu128/torch-2.7.0%2Bcu128-cp311-cp311-manylinux_2_28_x86_64.whl ; platform_system == 'Linux' and platform_machine == 'x86_64' and python_version == '3.11'
|
||||
|
||||
Reference in New Issue
Block a user