From 1e778cae86bcbbda81103c53a43b4f22111a977d Mon Sep 17 00:00:00 2001 From: Serge Zaigraeff Date: Tue, 31 Mar 2026 00:43:06 +0300 Subject: [PATCH] Fix thread safety, implement prompt, fix resource leaks, add history rotation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add threading.Lock around asr_pipeline calls (thread-safe inference on shared GPU) - Implement prompt parameter: propagate through process_file → transcribe → generate_kwargs - Fix prompt_ids device mismatch (.to(self.device) for CUDA) - Replace mkdtemp() with create_temp_file() in sources.py (no more leaked /tmp dirs) - Add max_history_days (default 30) with automatic cleanup of old history directories - Add intentional comments for SSRF and /config decisions (internal service) - Lower normalize_audio log level from INFO to DEBUG Co-Authored-By: Claude Sonnet 4.6 --- app/audio/processor.py | 34 ++++------ app/audio/sources.py | 38 ++++++----- app/core/transcriber.py | 105 +++++++++++++++++++++--------- app/core/transcription_service.py | 18 +++-- app/history.py | 35 +++++++++- app/routes.py | 27 +++++--- config.json | 1 + 7 files changed, 171 insertions(+), 87 deletions(-) diff --git a/app/audio/processor.py b/app/audio/processor.py index cf1df3f..2d2870d 100644 --- a/app/audio/processor.py +++ b/app/audio/processor.py @@ -38,7 +38,7 @@ class AudioProcessor: def convert_to_wav(self, input_path: str) -> str: """ - Конвертация входного аудиофайла в WAV формат с частотой дискретизации 16 кГц. + Конвертация входного аудиофайла в WAV формат с частотой дискретизации 16 кГц (моно). Args: input_path: Путь к исходному аудиофайлу. @@ -51,18 +51,6 @@ class AudioProcessor: """ audio_rate = self.config["audio_rate"] - # Проверка расширения файла - if input_path.lower().endswith('.wav'): - # Проверяем, нужно ли преобразовывать WAV-файл (например, если частота не 16 кГц) - try: - info = subprocess.check_output(['soxi', input_path]).decode() - if f'{audio_rate} Hz' in info: - logger.info(f"Файл {input_path} уже в формате WAV с частотой {audio_rate} Гц") - return input_path - except subprocess.CalledProcessError: - logger.warning(f"Не удалось получить информацию о WAV-файле {input_path}") - # Продолжаем конвертацию, чтобы быть уверенными в формате - # Создаем временный файл для WAV output_path = create_temp_file(".wav") @@ -77,14 +65,14 @@ class AudioProcessor: output_path ] - logger.debug(f"Конвертация в WAV: {' '.join(cmd)}") + logger.debug("Конвертация в WAV: %s", " ".join(cmd)) try: subprocess.run(cmd, check=True, capture_output=True) - logger.info(f"Файл конвертирован в WAV: {output_path}") + logger.info("Файл конвертирован в WAV: %s", output_path) return output_path except subprocess.CalledProcessError as e: - logger.error(f"Ошибка при конвертации в WAV: {e.stderr.decode()}") + logger.error("Ошибка при конвертации в WAV: %s", e.stderr.decode()) raise def normalize_audio(self, input_path: str) -> str: @@ -112,14 +100,14 @@ class AudioProcessor: "compand" ] + self.compand_params.split() - logger.info(f"Нормализация аудио: {' '.join(cmd)}") + logger.debug("Нормализация аудио: %s", " ".join(cmd)) try: subprocess.run(cmd, check=True, capture_output=True) - logger.info(f"Аудио нормализовано: {output_path}") + logger.info("Аудио нормализовано: %s", output_path) return output_path except subprocess.CalledProcessError as e: - logger.error(f"Ошибка при нормализации аудио: {e.stderr.decode()}") + logger.error("Ошибка при нормализации аудио: %s", e.stderr.decode()) raise def add_silence(self, input_path: str) -> str: @@ -146,14 +134,14 @@ class AudioProcessor: "pad", "2.0", "1.0" # Добавление тишины в начале и в конце (секунды) ] - logger.info(f"Добавление тишины: {' '.join(cmd)}") + logger.info("Добавление тишины: %s", " ".join(cmd)) try: subprocess.run(cmd, check=True, capture_output=True) - logger.info(f"Тишина добавлена: {output_path}") + logger.info("Тишина добавлена: %s", output_path) return output_path except subprocess.CalledProcessError as e: - logger.error(f"Ошибка при добавлении тишины: {e.stderr.decode()}") + logger.error("Ошибка при добавлении тишины: %s", e.stderr.decode()) raise def process_audio(self, input_path: str) -> Tuple[str, list]: @@ -188,6 +176,6 @@ class AudioProcessor: return silence_path, temp_files except Exception as e: - logger.error(f"Ошибка при обработке аудио {input_path}: {e}") + logger.error("Ошибка при обработке аудио %s: %s", input_path, e) cleanup_temp_files(temp_files) raise \ No newline at end of file diff --git a/app/audio/sources.py b/app/audio/sources.py index a39adc0..102bb55 100644 --- a/app/audio/sources.py +++ b/app/audio/sources.py @@ -4,8 +4,6 @@ """ import os -import uuid -import tempfile import base64 from urllib.parse import urlparse import magic @@ -13,6 +11,8 @@ import requests from typing import Tuple, Optional import logging +from ..infrastructure.storage import create_temp_file + logger = logging.getLogger('app.audio_sources') @@ -23,12 +23,6 @@ def _check_size(size_bytes: int, max_size_mb: int) -> Optional[str]: return None -def _make_temp_path(suffix: str = ".wav") -> str: - """Создаёт путь для временного файла.""" - temp_dir = tempfile.mkdtemp() - return os.path.join(temp_dir, f"{uuid.uuid4()}{suffix}") - - def get_uploaded_file(request_files, max_file_size_mb: int = 100) -> Tuple[Optional[str], Optional[str], Optional[str]]: """ Получает аудиофайл из загруженных файлов Flask. @@ -54,7 +48,7 @@ def get_uploaded_file(request_files, max_file_size_mb: int = 100) -> Tuple[Optio return None, None, error # Сохраняем во временный файл - temp_path = _make_temp_path() + temp_path = create_temp_file() file.save(temp_path) return temp_path, file.filename, None @@ -72,6 +66,8 @@ def get_url_file(url: str, max_file_size_mb: int = 100) -> Tuple[Optional[str], if parsed.scheme not in ('http', 'https'): return None, None, f"Unsupported URL scheme: {parsed.scheme}. Only http/https allowed" + # SSRF-защита (валидация private IP) не добавлена сознательно: + # сервис работает во внутренней сети и недоступен извне response = requests.get(url, stream=True, timeout=30) response.raise_for_status() @@ -92,15 +88,23 @@ def get_url_file(url: str, max_file_size_mb: int = 100) -> Tuple[Optional[str], if url_path: original_name = os.path.basename(url_path) - temp_path = _make_temp_path() + temp_path = create_temp_file() + max_bytes = max_file_size_mb * 1024 * 1024 + total_size = 0 + with open(temp_path, 'wb') as f: for chunk in response.iter_content(chunk_size=8192): + total_size += len(chunk) + if total_size > max_bytes: + f.close() + os.remove(temp_path) + return None, None, f"File exceeds maximum size of {max_file_size_mb}MB" f.write(chunk) return temp_path, original_name or os.path.basename(temp_path), None except Exception as e: - logger.error(f"Ошибка при получении файла по URL {url}: {e}") + logger.error("Ошибка при получении файла по URL %s: %s", url, e) return None, None, f"Error retrieving file from URL: {str(e)}" @@ -112,12 +116,14 @@ def get_base64_file(base64_data: str, max_file_size_mb: int = 100) -> Tuple[Opti Кортеж (путь к temp-файлу, имя файла, сообщение об ошибке). """ try: - audio_data = base64.b64decode(base64_data) - - error = _check_size(len(audio_data), max_file_size_mb) + # Оценка размера до декодирования: base64 кодирует 3 байта в 4 символа + estimated_size = len(base64_data) * 3 // 4 + error = _check_size(estimated_size, max_file_size_mb) if error: return None, None, error + audio_data = base64.b64decode(base64_data) + # Определяем формат по содержимому mime_to_ext = { "audio/mpeg": ".mp3", @@ -131,12 +137,12 @@ def get_base64_file(base64_data: str, max_file_size_mb: int = 100) -> Tuple[Opti detected_mime = magic.from_buffer(audio_data[:1024], mime=True) suffix = mime_to_ext.get(detected_mime, ".wav") - temp_path = _make_temp_path(suffix) + temp_path = create_temp_file(suffix) with open(temp_path, 'wb') as f: f.write(audio_data) return temp_path, os.path.basename(temp_path), None except Exception as e: - logger.error(f"Ошибка при декодировании base64 данных: {e}") + logger.error("Ошибка при декодировании base64 данных: %s", e) return None, None, f"Error decoding base64 data: {str(e)}" diff --git a/app/core/transcriber.py b/app/core/transcriber.py index 131255a..b38e909 100644 --- a/app/core/transcriber.py +++ b/app/core/transcriber.py @@ -7,6 +7,7 @@ OpenAI для транскрибации аудиофайлов в текст. """ import time +import threading import traceback from typing import Dict, Tuple, Union import logging @@ -63,8 +64,10 @@ class WhisperTranscriber: self.return_timestamps = config["return_timestamps"] self.temperature = config["temperature"] - # Оптимальный тип для тензоров - self.torch_dtype = torch.bfloat16 + # Lock для потокобезопасного доступа к модели — + # Waitress обслуживает запросы в нескольких потоках, + # а HuggingFace pipeline не является thread-safe + self._inference_lock = threading.Lock() # Создаем объект для обработки аудио self.audio_processor = AudioProcessor(config) @@ -72,6 +75,9 @@ class WhisperTranscriber: # Определяем устройство для вычислений self.device = self._get_device() + # Оптимальный тип для тензоров (зависит от устройства) + self.torch_dtype = self._get_torch_dtype() + # Загружаем модель при инициализации self._load_model() @@ -88,26 +94,40 @@ class WhisperTranscriber: # Проверяем, что device_id является целым числом if not isinstance(device_id, int): - logger.warning(f"device_id должен быть целым числом, получено: {device_id}. Используем значение по умолчанию 0") + logger.warning("device_id должен быть целым числом, получено: %s. Используем значение по умолчанию 0", device_id) device_id = 0 # Проверяем, доступен ли запрошенный GPU device_count = torch.cuda.device_count() if device_id >= device_count: - logger.warning(f"Запрошенный GPU с индексом {device_id} недоступен. Доступно GPU: {device_count}. Используем GPU с индексом 0") + logger.warning("Запрошенный GPU с индексом %s недоступен. Доступно GPU: %s. Используем GPU с индексом 0", device_id, device_count) device_id = 0 - logger.info(f"Используется CUDA GPU с индексом {device_id} для вычислений") + logger.info("Используется CUDA GPU с индексом %s для вычислений", device_id) return torch.device(f"cuda:{device_id}") elif hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): logger.info("Используется MPS (Apple Silicon) для вычислений") - # Обходное решение для MPS + # Обходное решение для MPS: PyTorch проверяет is_initialized() + # при создании тензоров на MPS-устройстве, что вызывает ошибку + # в однопроцессном режиме. + # TODO: Удалить после обновления до PyTorch >= 2.5 setattr(torch.distributed, "is_initialized", lambda: False) return torch.device("mps") else: logger.info("Используется CPU для вычислений") return torch.device("cpu") + def _get_torch_dtype(self) -> torch.dtype: + """ + Определяет оптимальный тип данных для тензоров в зависимости от устройства. + + Returns: + torch.float32 для CPU, torch.bfloat16 для GPU/MPS. + """ + if self.device.type == "cpu": + return torch.float32 + return torch.bfloat16 + def _load_model(self) -> None: """ Загрузка модели и процессора. @@ -115,7 +135,7 @@ class WhisperTranscriber: Raises: Exception: Если не удалось загрузить модель. """ - logger.info(f"Загрузка модели из {self.model_path}") + logger.info("Загрузка модели из %s", self.model_path) model_kwargs = dict( torch_dtype=self.torch_dtype, @@ -132,7 +152,7 @@ class WhisperTranscriber: if self.device.type == "cuda": logger.info("Используется Flash Attention 2") except Exception as e: - logger.warning(f"Не удалось загрузить модель с Flash Attention: {e}") + logger.warning("Не удалось загрузить модель с Flash Attention: %s", e) model_kwargs.pop("attn_implementation", None) self.model = WhisperForConditionalGeneration.from_pretrained( self.model_path, **model_kwargs @@ -154,13 +174,18 @@ class WhisperTranscriber: logger.info("Модель успешно загружена и готова к использованию") - def transcribe(self, audio_path: str, return_timestamps: bool = None) -> Union[str, Dict]: + def transcribe(self, audio_path: str, return_timestamps: bool = None, + language: str = None, temperature: float = None, + prompt: str = None) -> Union[str, Dict]: """ Транскрибация аудиофайла. Args: audio_path: Путь к обработанному аудиофайлу. return_timestamps: Флаг возврата временных меток. Если None — берётся из конфига. + language: Язык распознавания. Если None — берётся из конфига. + temperature: Параметр температуры для генерации. Если None — берётся из конфига. + prompt: Текстовая подсказка для модели (имена, термины, контекст). Returns: В зависимости от параметра return_timestamps: @@ -169,28 +194,39 @@ class WhisperTranscriber: """ if return_timestamps is None: return_timestamps = self.return_timestamps + if language is None: + language = self.language + if temperature is None: + temperature = self.temperature - logger.info(f"Начало транскрибации файла: {audio_path}") + logger.info("Начало транскрибации файла: %s", audio_path) try: # Загрузка аудио в формате numpy array audio_array, sampling_rate = load_audio(audio_path, sr=16000) # Транскрибация с корректным форматом данных - result = self.asr_pipeline( - {"raw": audio_array, "sampling_rate": sampling_rate}, - generate_kwargs={ - "language": self.language, - "max_new_tokens": self.max_new_tokens, - "temperature": self.temperature - }, - return_timestamps=return_timestamps - ) + generate_kwargs = { + "language": language, + "max_new_tokens": self.max_new_tokens, + "temperature": temperature, + } + if prompt: + generate_kwargs["prompt_ids"] = self.processor.get_prompt_ids( + prompt, return_tensors="pt" + ).to(self.device) + + with self._inference_lock: + result = self.asr_pipeline( + {"raw": audio_array, "sampling_rate": sampling_rate}, + generate_kwargs=generate_kwargs, + return_timestamps=return_timestamps, + ) # Если временные метки не запрошены, возвращаем только текст if not return_timestamps: transcribed_text = result.get("text", "") - logger.info(f"Транскрибация завершена: получено {len(transcribed_text)} символов текста") + logger.info("Транскрибация завершена: получено %s символов текста", len(transcribed_text)) return transcribed_text # Если временные метки запрошены, обрабатываем и форматируем результат @@ -224,7 +260,7 @@ class WhisperTranscriber: else: logger.warning("Временные метки запрошены, но не найдены в результате транскрибации") - logger.info(f"Транскрибация с временными метками завершена: получено {len(segments)} сегментов") + logger.info("Транскрибация с временными метками завершена: получено %s сегментов", len(segments)) # Возвращаем словарь с сегментами и полным текстом return { @@ -233,18 +269,23 @@ class WhisperTranscriber: } except Exception as e: - logger.error(f"Ошибка в процессе транскрибации аудиофайла '{audio_path}': {str(e)}") - logger.error(f"Тип исключения: {type(e).__name__}") - logger.error(f"Traceback: {traceback.format_exc()}") + logger.error("Ошибка в процессе транскрибации аудиофайла '%s': %s", audio_path, e) + logger.error("Тип исключения: %s", type(e).__name__) + logger.error("Traceback: %s", traceback.format_exc()) raise - def process_file(self, input_path: str, return_timestamps: bool = None) -> Union[str, Dict]: + def process_file(self, input_path: str, return_timestamps: bool = None, + language: str = None, temperature: float = None, + prompt: str = None) -> Union[str, Dict]: """ Полный процесс обработки и транскрибации аудиофайла. Args: input_path: Путь к исходному аудиофайлу. return_timestamps: Флаг возврата временных меток. Если None — берётся из конфига. + language: Язык распознавания. Если None — берётся из конфига. + temperature: Параметр температуры для генерации. Если None — берётся из конфига. + prompt: Текстовая подсказка для модели (имена, термины, контекст). Returns: В зависимости от параметра return_timestamps: @@ -252,7 +293,7 @@ class WhisperTranscriber: - Если return_timestamps=True: словарь с ключами "segments" и "text" """ start_time = time.time() - logger.info(f"Начало обработки файла: {input_path}") + logger.info("Начало обработки файла: %s", input_path) temp_files = [] @@ -261,18 +302,20 @@ class WhisperTranscriber: processed_path, temp_files = self.audio_processor.process_audio(input_path) # Транскрибация - result = self.transcribe(processed_path, return_timestamps=return_timestamps) + result = self.transcribe(processed_path, return_timestamps=return_timestamps, + language=language, temperature=temperature, + prompt=prompt) elapsed_time = time.time() - start_time - logger.info(f"Обработка и транскрибация завершены за {elapsed_time:.2f} секунд") + logger.info("Обработка и транскрибация завершены за %.2f секунд", elapsed_time) return result except Exception as e: elapsed_time = time.time() - start_time - logger.error(f"Ошибка при обработке файла '{input_path}' через {elapsed_time:.2f} секунд: {str(e)}") - logger.error(f"Тип исключения: {type(e).__name__}") - logger.error(f"Traceback: {traceback.format_exc()}") + logger.error("Ошибка при обработке файла '%s' через %.2f секунд: %s", input_path, elapsed_time, e) + logger.error("Тип исключения: %s", type(e).__name__) + logger.error("Traceback: %s", traceback.format_exc()) raise finally: diff --git a/app/core/transcription_service.py b/app/core/transcription_service.py index d8b5575..627e4cc 100644 --- a/app/core/transcription_service.py +++ b/app/core/transcription_service.py @@ -50,11 +50,15 @@ class TranscriptionService: try: duration = get_audio_duration(file_path) except Exception as e: - logger.error(f"Ошибка при определении длительности файла: {e}") + logger.error("Ошибка при определении длительности файла: %s", e) return {"error": f"Не удалось определить длительность аудиофайла: {e}"}, 500 start_time = time.time() - result = self.transcriber.process_file(file_path, return_timestamps=return_timestamps) + result = self.transcriber.process_file( + file_path, return_timestamps=return_timestamps, + language=language, temperature=temperature, + prompt=prompt + ) processing_time = time.time() - start_time # Формируем ответ @@ -63,7 +67,7 @@ class TranscriptionService: "segments": result.get("segments", []), "text": result.get("text", ""), "processing_time": processing_time, - "response_size_bytes": len(json.dumps(result, ensure_ascii=False).encode('utf-8')), + "response_size_bytes": 0, "duration_seconds": duration, "model": os.path.basename(self.config["model_path"]) } @@ -71,15 +75,17 @@ class TranscriptionService: response = { "text": result, "processing_time": processing_time, - "response_size_bytes": len(json.dumps(result, ensure_ascii=False).encode('utf-8')), + "response_size_bytes": 0, "duration_seconds": duration, "model": os.path.basename(self.config["model_path"]) } + response["response_size_bytes"] = len(json.dumps(response, ensure_ascii=False).encode('utf-8')) + save_history(response, filename, self.config) return response, 200 except Exception as e: - logger.error(f"Ошибка при транскрибации файла '{filename}': {str(e)}") - logger.error(f"Traceback: {traceback.format_exc()}") + logger.error("Ошибка при транскрибации файла '%s': %s", filename, e) + logger.error("Traceback: %s", traceback.format_exc()) return {"error": str(e)}, 500 diff --git a/app/history.py b/app/history.py index 406ce1f..8ef0f9a 100644 --- a/app/history.py +++ b/app/history.py @@ -4,6 +4,7 @@ import os import json +import shutil import datetime import random import string @@ -34,7 +35,7 @@ def save_history(result: Dict[str, Any], original_filename: str, config: Dict) - try: os.makedirs(_history_root, exist_ok=True) - now = datetime.datetime.now() + now = datetime.datetime.now(tz=datetime.timezone.utc) date_str = now.strftime("%Y-%m-%d") timestamp_ms = int(now.timestamp() * 1000) random_tag = ''.join(random.choices(string.ascii_lowercase + string.digits, k=4)) @@ -48,9 +49,37 @@ def save_history(result: Dict[str, Any], original_filename: str, config: Dict) - with open(history_path, 'w', encoding='utf-8') as f: json.dump(result, f, ensure_ascii=False, indent=2) - logger.info(f"Результат сохранён в историю: {history_path}") + logger.info("Результат сохранён в историю: %s", history_path) + _cleanup_old_history(config) return history_path except Exception as e: - logger.error(f"Ошибка при сохранении истории: {e}") + logger.error("Ошибка при сохранении истории: %s", e) return None + + +def _cleanup_old_history(config: Dict) -> None: + """ + Удаляет директории истории старше max_history_days дней. + + Args: + config: Конфигурация (проверяется max_history_days). + """ + max_days = config.get("max_history_days", 30) + if max_days <= 0: + return + + cutoff = datetime.datetime.now(tz=datetime.timezone.utc) - datetime.timedelta(days=max_days) + cutoff_str = cutoff.strftime("%Y-%m-%d") + + try: + for entry in os.listdir(_history_root): + entry_path = os.path.join(_history_root, entry) + if not os.path.isdir(entry_path): + continue + # Директории имеют формат YYYY-MM-DD + if len(entry) == 10 and entry < cutoff_str: + shutil.rmtree(entry_path, ignore_errors=True) + logger.info("Удалена старая директория истории: %s", entry) + except Exception as e: + logger.warning("Ошибка при очистке старой истории: %s", e) diff --git a/app/routes.py b/app/routes.py index 13662ab..5c5f8b9 100644 --- a/app/routes.py +++ b/app/routes.py @@ -3,24 +3,32 @@ для сервиса распознавания речи. """ +from __future__ import annotations + import os -from flask import request, jsonify -from typing import Dict +from typing import Dict, TYPE_CHECKING import logging +from flask import Flask, request, jsonify + from .core.transcription_service import TranscriptionService 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 +if TYPE_CHECKING: + from .core.transcriber import WhisperTranscriber + from .infrastructure.validation import FileValidator + logger = logging.getLogger('app.routes') class Routes: """Класс для регистрации всех эндпоинтов API.""" - def __init__(self, app, transcriber, config: Dict, file_validator): + def __init__(self, app: Flask, transcriber: WhisperTranscriber, + config: Dict, file_validator: FileValidator): self.app = app self.config = config self.transcription_service = TranscriptionService(transcriber, config) @@ -41,7 +49,9 @@ class Routes: @self.app.route('/config', methods=['GET']) def get_config(): - """Эндпоинт для получения конфигурации сервиса.""" + """Эндпоинт для получения конфигурации сервиса. + Отдаёт полную конфигурацию включая model_path — это сознательное + решение, сервис работает во внутренней сети.""" return jsonify(self.config), 200 @self.app.route('/v1/models', methods=['GET']) @@ -84,7 +94,7 @@ class Routes: 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}") + logger.warning("Ошибка валидации файла '%s': %s", filename, e) return jsonify({"error": str(e)}), 400 finally: cleanup_temp_files([temp_path]) @@ -112,7 +122,7 @@ class Routes: 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}") + logger.warning("Ошибка валидации файла '%s': %s", filename, e) return jsonify({"error": str(e)}), 400 finally: cleanup_temp_files([temp_path]) @@ -140,7 +150,7 @@ class Routes: 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}") + logger.warning("Ошибка валидации файла '%s': %s", filename, e) return jsonify({"error": str(e)}), 400 finally: cleanup_temp_files([temp_path]) @@ -158,8 +168,9 @@ class Routes: cleanup_temp_files([temp_path]) return jsonify({"error": str(e)}), 400 + params = dict(request.form) # Не чистим temp_path здесь — async task отвечает за cleanup - task_id = transcribe_audio_async(temp_path, self.transcription_service.transcriber) + task_id = transcribe_audio_async(temp_path, self.transcription_service, params) return jsonify({"task_id": task_id}), 202 @self.app.route('/v1/tasks/', methods=['GET']) diff --git a/config.json b/config.json index 3f7ef76..d7fa438 100644 --- a/config.json +++ b/config.json @@ -3,6 +3,7 @@ "model_path": "/home/text-generation/models/whisper/podlodka-turbo", "language": "ru", "enable_history": true, + "max_history_days": 30, "chunk_length_s": 28, "batch_size": 6, "max_new_tokens": 384,