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:
Serge Zaigraeff
2026-03-30 00:54:50 +03:00
co-authored by Claude Opus 4.6
parent ff4bd2ba3d
commit 2138651474
12 changed files with 153 additions and 189 deletions
+33 -27
View File
@@ -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)}"