Refactor logging, clean dead code, fix temp files and async tasks

- Replace f-string logging with % formatting throughout
- Fix create_temp_file: use mkstemp() instead of mkdtemp() (no leaked dirs)
- Add cleanup background thread and copy.deepcopy to AsyncTaskManager
- Fix transcribe_audio_async signature to accept transcription_service + params
- Use resample_poly instead of scipy_resample for better audio quality
- Remove unused validate_file/BinaryIO methods from FileValidator
- Add requests to requirements.txt
- Add error handling for pip install in server.sh
- Add __all__ and module docstrings to __init__.py files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Serge Zaigraeff
2026-03-31 00:43:53 +03:00
co-authored by Claude Sonnet 4.6
parent 1e778cae86
commit ca2e41e0f5
13 changed files with 60 additions and 127 deletions
+4 -1
View File
@@ -2,7 +2,10 @@
"permissions": {
"allow": [
"Bash(git -C /Users/serge/Develop/nnp-whisper-api-server log --oneline -10 --all)",
"Bash(git -C /Users/serge/Develop/nnp-whisper-api-server log --oneline main..cleanup/dead-code)"
"Bash(git -C /Users/serge/Develop/nnp-whisper-api-server log --oneline main..cleanup/dead-code)",
"Bash(ssh orange:*)",
"Bash(curl -s http://stt.ai.gray/health)",
"Bash(curl -s http://stt.ai.gray/v1/models)"
]
}
}
+3 -1
View File
@@ -3,6 +3,8 @@
и запуска сервиса распознавания речи.
"""
__all__ = ['WhisperServiceAPI']
import os
import logging
from flask import Flask
@@ -68,7 +70,7 @@ class WhisperServiceAPI:
def run(self) -> None:
"""Запуск сервиса через Waitress."""
self.logger.info(f"Запуск сервиса на 0.0.0.0:{self.port}")
self.logger.info("Запуск сервиса на 0.0.0.0:%s", self.port)
waitress.serve(self.app, host='0.0.0.0', port=self.port)
def create_app(self) -> Flask:
+1
View File
@@ -0,0 +1 @@
"""Модуль audio — обработка и подготовка аудиофайлов."""
+5 -5
View File
@@ -6,7 +6,7 @@ import os
import subprocess
import wave
import numpy as np
from scipy.signal import resample as scipy_resample
from scipy.signal import resample_poly
import logging
from typing import Tuple
@@ -27,21 +27,21 @@ def load_audio(file_path: str, sr: int = 16000) -> Tuple[np.ndarray, int]:
try:
with wave.open(file_path, 'rb') as wav_file:
if wav_file.getnchannels() != 1:
logger.warning(f"Файл {file_path} не моно-аудио")
logger.warning("Файл %s не моно-аудио", file_path)
frames = wav_file.readframes(-1)
audio_array = np.frombuffer(frames, dtype=np.int16).astype(np.float32) / 32768.0
sampling_rate = wav_file.getframerate()
if sampling_rate != sr:
num_samples = int(len(audio_array) * sr / sampling_rate)
audio_array = scipy_resample(audio_array, num_samples)
gcd = np.gcd(sr, sampling_rate)
audio_array = resample_poly(audio_array, sr // gcd, sampling_rate // gcd)
sampling_rate = sr
return audio_array, sampling_rate
except Exception as e:
logger.error(f"Ошибка при загрузке аудио {file_path}: {e}")
logger.error("Ошибка при загрузке аудио %s: %s", file_path, e)
raise
+1
View File
@@ -0,0 +1 @@
"""Модуль core — основные компоненты сервиса распознавания речи."""
+3 -3
View File
@@ -27,11 +27,11 @@ def load_config(config_path: str) -> Dict:
try:
with open(config_path, "r", encoding="utf-8") as f:
config = json.load(f)
logger.info(f"Конфигурация успешно загружена из {config_path}")
logger.info("Конфигурация успешно загружена из %s", config_path)
return config
except FileNotFoundError as e:
logger.error(f"Файл конфигурации не найден: {e}")
logger.error("Файл конфигурации не найден: %s", e)
raise
except json.JSONDecodeError as e:
logger.error(f"Ошибка при загрузке конфигурации: {e}")
logger.error("Ошибка при загрузке конфигурации: %s", e)
raise
+1
View File
@@ -0,0 +1 @@
"""Модуль infrastructure — вспомогательные компоненты сервиса."""
+23 -6
View File
@@ -2,6 +2,7 @@
Модуль async_tasks.py содержит функции для асинхронной обработки задач.
"""
import copy
import uuid
import time
from typing import Dict, Any, Callable, Optional
@@ -27,6 +28,7 @@ class AsyncTaskManager:
"""
self._lock = Lock()
self.tasks: Dict[str, Dict[str, Any]] = {}
self._start_cleanup_thread()
def run_task(self, func: Callable, *args, **kwargs) -> str:
"""
@@ -91,14 +93,14 @@ class AsyncTaskManager:
self.tasks[task_id]["result"] = result
self.tasks[task_id]["completed_at"] = time.time()
logger.info(f"Задача {task_id} завершена успешно")
logger.info("Задача %s завершена успешно", task_id)
except Exception as e:
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}")
logger.error("Задача %s завершилась с ошибкой: %s", task_id, e)
def get_task_status(self, task_id: str) -> Optional[Dict[str, Any]]:
"""
@@ -112,22 +114,37 @@ class AsyncTaskManager:
"""
with self._lock:
task = self.tasks.get(task_id)
return dict(task) if task else None
return copy.deepcopy(task) if task else None
def _start_cleanup_thread(self) -> None:
"""Запуск фонового потока для периодической очистки старых задач."""
def _cleanup_loop():
while True:
time.sleep(600) # Каждые 10 минут
with self._lock:
self._cleanup_old_tasks()
thread = Thread(target=_cleanup_loop, daemon=True)
thread.start()
# Глобальный экземпляр менеджера асинхронных задач
task_manager = AsyncTaskManager()
def transcribe_audio_async(file_path: str, transcriber) -> str:
def transcribe_audio_async(file_path: str, transcription_service, params: Dict = None) -> str:
"""
Асинхронная транскрибация аудиофайла.
Args:
file_path: Путь к аудиофайлу.
transcriber: Экземпляр транскрайбера.
transcription_service: Экземпляр TranscriptionService.
params: Параметры транскрибации.
Returns:
ID задачи.
"""
return task_manager.run_task(transcriber.process_file, file_path)
params = params or {}
def _transcribe():
return transcription_service.transcribe(file_path, "async_task", params)
return task_manager.run_task(_transcribe)
+4 -2
View File
@@ -91,7 +91,8 @@ class RequestLogger:
return
g.start_time = time.time()
self.logger.info(
f"{request.method} {request.path} от {self._get_client_ip()}",
"%s %s от %s",
request.method, request.path, self._get_client_ip(),
extra={"type": "request"}
)
@@ -100,7 +101,8 @@ class RequestLogger:
return response
processing_time = time.time() - getattr(g, 'start_time', time.time())
self.logger.info(
f"{response.status_code} за {processing_time:.3f} сек",
"%s за %.3f сек",
response.status_code, processing_time,
extra={"type": "response"}
)
return response
+6 -11
View File
@@ -3,7 +3,6 @@
"""
import os
import uuid
import tempfile
import logging
@@ -20,15 +19,15 @@ def create_temp_file(suffix: str = ".wav") -> str:
Returns:
Путь к временному файлу.
"""
temp_dir = tempfile.mkdtemp()
temp_path = os.path.join(temp_dir, f"{uuid.uuid4()}{suffix}")
logger.debug(f"Создан временный файл: {temp_path}")
fd, temp_path = tempfile.mkstemp(suffix=suffix)
os.close(fd)
logger.debug("Создан временный файл: %s", temp_path)
return temp_path
def cleanup_temp_files(file_paths: list) -> None:
"""
Удаляет временные файлы и их директории.
Удаляет временные файлы.
Args:
file_paths: Список путей к файлам для удаления.
@@ -37,10 +36,6 @@ def cleanup_temp_files(file_paths: list) -> None:
try:
if os.path.exists(path):
os.remove(path)
logger.debug(f"Удалён временный файл: {path}")
temp_dir = os.path.dirname(path)
if os.path.exists(temp_dir) and not os.listdir(temp_dir):
os.rmdir(temp_dir)
logger.debug("Удалён временный файл: %s", path)
except Exception as e:
logger.warning(f"Не удалось очистить временный файл {path}: {e}")
logger.warning("Не удалось очистить временный файл %s: %s", path, e)
+4 -97
View File
@@ -4,7 +4,7 @@
import os
import magic
from typing import Dict, List, BinaryIO
from typing import Dict, List
import logging
# Получаем логгер из централизованной настройки
@@ -38,64 +38,6 @@ class FileValidator:
["audio/wav", "audio/mpeg", "audio/ogg",
"audio/flac", "audio/mp4"])
def validate_file(self, file: BinaryIO, filename: str) -> bool:
"""
Валидирует файл на основе конфигурации.
Args:
file: Файловый объект.
filename: Имя файла.
Returns:
True, если файл прошел валидацию.
Raises:
ValidationError: Если файл не прошел валидацию.
"""
try:
# Проверка размера файла
self._validate_file_size(file)
# Проверка расширения файла
self._validate_file_extension(filename)
# Проверка MIME-типа файла
self._validate_file_mime_type(file)
return True
except ValidationError as e:
# Логирование общей ошибки валидации
logger.warning(f"Ошибка валидации файла '{filename}': {str(e)}")
raise
def _validate_file_size(self, file: BinaryIO) -> None:
"""
Валидирует размер файла.
Args:
file: Файловый объект.
Raises:
ValidationError: Если размер файла превышает максимально допустимый.
"""
# Сохранение текущей позиции
current_position = file.tell()
# Переход в конец файла для определения размера
file.seek(0, os.SEEK_END)
file_length = file.tell()
# Возврат к исходной позиции
file.seek(current_position)
max_size_bytes = self.max_file_size_mb * 1024 * 1024
if file_length > max_size_bytes:
logger.warning(f"Попытка загрузки файла размером {file_length / (1024*1024):.2f} МБ, "
f"что превышает максимально допустимый размер {self.max_file_size_mb} МБ")
raise ValidationError(f"Размер файла ({file_length / (1024*1024):.2f} МБ) "
f"превышает максимально допустимый ({self.max_file_size_mb} МБ)")
def _validate_file_extension(self, filename: str) -> None:
"""
Валидирует расширение файла.
@@ -109,46 +51,12 @@ class FileValidator:
if not any(filename.lower().endswith(ext.lower()) for ext in self.allowed_extensions):
# Логирование попытки загрузки файла с неразрешенным расширением
file_extension = os.path.splitext(filename)[1]
logger.warning(f"Попытка загрузки файла с неразрешенным расширением '{file_extension}'. "
f"Имя файла: {filename}. Разрешенные расширения: {', '.join(self.allowed_extensions)}")
logger.warning("Попытка загрузки файла с неразрешенным расширением '%s'. "
"Имя файла: %s. Разрешенные расширения: %s", file_extension, filename, ", ".join(self.allowed_extensions))
raise ValidationError(f"Расширение файла не разрешено. "
f"Разрешенные расширения: {', '.join(self.allowed_extensions)}")
def _validate_file_mime_type(self, file: BinaryIO) -> None:
"""
Валидирует MIME-тип файла.
Args:
file: Файловый объект.
Raises:
ValidationError: Если MIME-тип файла не входит в список разрешенных.
"""
# Сохранение текущей позиции
current_position = file.tell()
try:
# Чтение первых байтов для определения MIME-типа
header = file.read(1024)
mime_type = magic.from_buffer(header, mime=True)
# Возврат к исходной позиции
file.seek(current_position)
if mime_type not in self.allowed_mime_types:
# Логирование попытки загрузки файла с неразрешенным MIME-типом
logger.warning(f"Попытка загрузки файла с неразрешенным MIME-типом '{mime_type}'. "
f"Разрешенные MIME-типы: {', '.join(self.allowed_mime_types)}")
raise ValidationError(f"MIME-тип файла ({mime_type}) не разрешен. "
f"Разрешенные MIME-типы: {', '.join(self.allowed_mime_types)}")
except Exception as e:
# Возврат к исходной позиции в случае ошибки
file.seek(current_position)
logger.warning(f"Не удалось определить MIME-тип файла: {e}")
# Не прерываем валидацию, если не удалось определить MIME-тип
def validate_file_by_path(self, file_path: str, filename: str) -> bool:
"""
Валидирует файл по пути на диске.
@@ -182,7 +90,6 @@ class FileValidator:
except ValidationError:
raise
except Exception as e:
logger.warning(f"Не удалось определить MIME-тип файла: {e}")
logger.warning("Не удалось определить MIME-тип файла: %s", e)
return True
+1
View File
@@ -17,3 +17,4 @@ scipy==1.13.1
transformers==4.49.0
accelerate==1.4.0
python-magic==0.4.27
requests==2.32.3
+4 -1
View File
@@ -48,7 +48,10 @@ if [[ "$NEW_ENV_CREATED" == true || "$UPDATE_ENV" == true ]]; then
# Установка зависимостей из requirements.txt
if [ -f "requirements.txt" ]; then
echo "Установка зависимостей из requirements.txt"
pip install --no-cache-dir -r requirements.txt
if ! pip install --no-cache-dir -r requirements.txt; then
echo "Ошибка при установке зависимостей."
exit 1
fi
else
echo "Файл requirements.txt не найден. Убедитесь, что он находится в той же директории, что и скрипт."
exit 1