From 0c7f93d067c59e729373dd61d6e5ba020a884c30 Mon Sep 17 00:00:00 2001 From: Dymas Date: Sat, 6 Jun 2026 12:47:59 +0200 Subject: [PATCH] Add model auto-download and English logging --- README.md | 36 +++++++++ app/__init__.py | 8 +- app/audio/processor.py | 22 ++--- app/audio/sources.py | 4 +- app/audio/utils.py | 12 +-- app/core/config.py | 8 +- app/core/transcriber.py | 129 +++++++++++++++++++++++++----- app/core/transcription_service.py | 6 +- app/history.py | 8 +- app/infrastructure/async_tasks.py | 6 +- app/infrastructure/log.py | 4 +- app/infrastructure/storage.py | 6 +- app/infrastructure/validation.py | 18 ++--- app/routes.py | 6 +- config.docker.json | 2 + config.json | 4 +- docker-compose.yml | 2 + 17 files changed, 204 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 64ab736..3d04d55 100644 --- a/README.md +++ b/README.md @@ -75,9 +75,39 @@ The repository now includes a container setup for running the API in Docker. - `./models` -> `/models` Place your Whisper model inside `./models/whisper` or update `model_path` in `config.docker.json`. +That path must contain the actual Hugging Face model files, not just an empty folder. + +If you prefer automatic bootstrap, set: + +```json +"auto_download_missing_model": true, +"model_repo_id": "openai/whisper-large-v3" +``` + +With the default Docker config, the container will automatically download that +model into `/models/whisper` on first start if the directory is missing or incomplete. + +Expected example layout: + +```text +models/ + whisper/ + config.json + generation_config.json + preprocessor_config.json + tokenizer.json + tokenizer_config.json + model.safetensors +``` ### Build and run with Docker Compose +If your selected model requires authentication on Hugging Face, export a token first: + +```bash +export HF_TOKEN=your_huggingface_token +``` + ```bash docker compose up --build ``` @@ -93,6 +123,7 @@ http://localhost:5042 ```bash docker build -t whisper-api-server . docker run --rm -p 5042:5042 \ + -e HF_TOKEN="$HF_TOKEN" \ -v "$(pwd)/config.docker.json:/app/config.docker.json:ro" \ -v "$(pwd)/logs:/app/logs" \ -v "$(pwd)/history:/app/history" \ @@ -106,6 +137,7 @@ The container image installs the same Python dependencies as the local setup, in ```bash docker run --rm --gpus all -p 5042:5042 \ + -e HF_TOKEN="$HF_TOKEN" \ -v "$(pwd)/config.docker.json:/app/config.docker.json:ro" \ -v "$(pwd)/logs:/app/logs" \ -v "$(pwd)/history:/app/history" \ @@ -121,6 +153,8 @@ The service is configured through the `config.json` file: { "service_port": 5042, "model_path": "/path/to/whisper/model", + "auto_download_missing_model": false, + "model_repo_id": "openai/whisper-large-v3", "language": "russian", "enable_history": true, "max_history_days": 30, @@ -152,6 +186,8 @@ The service is configured through the `config.json` file: |-----------|-------------| | `service_port` | Port on which the service will run | | `model_path` | Path to the Whisper model directory | +| `auto_download_missing_model` | Download the configured fallback model into `model_path` when the local directory is missing or incomplete | +| `model_repo_id` | Hugging Face model repo to download when auto-download is enabled | | `language` | Language for transcription (e.g., "russian", "english") | | `enable_history` | Whether to save transcription history (true/false) | | `max_history_days` | Number of days to keep transcription history before rotation | diff --git a/app/__init__.py b/app/__init__.py index 4be6cc7..f92e315 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -48,7 +48,7 @@ class WhisperServiceAPI: # Получаем логгер для этого модуля self.logger = logging.getLogger('app') - self.logger.info("Инициализация WhisperServiceAPI") + self.logger.info("Initializing WhisperServiceAPI") # Инициализация Flask приложения self.app = Flask(__name__) @@ -66,11 +66,11 @@ class WhisperServiceAPI: # Регистрация маршрутов routes = Routes(self.app, self.transcriber, self.config, self.file_validator) - self.logger.info("WhisperServiceAPI успешно инициализирован") + self.logger.info("WhisperServiceAPI initialized successfully") def run(self) -> None: """Запуск сервиса через Waitress.""" - self.logger.info("Запуск сервиса на 0.0.0.0:%s", self.port) + self.logger.info("Starting service on 0.0.0.0:%s", self.port) waitress.serve(self.app, host='0.0.0.0', port=self.port) def create_app(self) -> Flask: @@ -80,4 +80,4 @@ class WhisperServiceAPI: Returns: Настроенное Flask приложение. """ - return self.app \ No newline at end of file + return self.app diff --git a/app/audio/processor.py b/app/audio/processor.py index 5952103..78a0950 100644 --- a/app/audio/processor.py +++ b/app/audio/processor.py @@ -66,14 +66,14 @@ class AudioProcessor: output_path ] - logger.debug("Конвертация в WAV: %s", " ".join(cmd)) + logger.debug("Converting to WAV: %s", " ".join(cmd)) try: subprocess.run(cmd, check=True, capture_output=True) - logger.info("Файл конвертирован в WAV: %s", output_path) + logger.info("Converted file to WAV: %s", output_path) return output_path except subprocess.CalledProcessError as e: - logger.error("Ошибка при конвертации в WAV: %s", e.stderr.decode()) + logger.error("Failed to convert to WAV: %s", e.stderr.decode()) raise def normalize_audio(self, input_path: str) -> str: @@ -101,14 +101,14 @@ class AudioProcessor: "compand" ] + self.compand_params.split() - logger.debug("Нормализация аудио: %s", " ".join(cmd)) + logger.debug("Normalizing audio: %s", " ".join(cmd)) try: subprocess.run(cmd, check=True, capture_output=True) - logger.info("Аудио нормализовано: %s", output_path) + logger.info("Audio normalized: %s", output_path) return output_path except subprocess.CalledProcessError as e: - logger.error("Ошибка при нормализации аудио: %s", e.stderr.decode()) + logger.error("Failed to normalize audio: %s", e.stderr.decode()) raise def add_silence(self, input_path: str) -> str: @@ -135,14 +135,14 @@ class AudioProcessor: "pad", "2.0", "1.0" # Добавление тишины в начале и в конце (секунды) ] - logger.info("Добавление тишины: %s", " ".join(cmd)) + logger.info("Adding silence padding: %s", " ".join(cmd)) try: subprocess.run(cmd, check=True, capture_output=True) - logger.info("Тишина добавлена: %s", output_path) + logger.info("Silence padding added: %s", output_path) return output_path except subprocess.CalledProcessError as e: - logger.error("Ошибка при добавлении тишины: %s", e.stderr.decode()) + logger.error("Failed to add silence padding: %s", e.stderr.decode()) raise def process_audio(self, input_path: str) -> Tuple[str, list]: @@ -177,6 +177,6 @@ class AudioProcessor: return silence_path, temp_files except Exception as e: - logger.error("Ошибка при обработке аудио %s: %s", input_path, e) + logger.error("Failed to process audio %s: %s", input_path, e) cleanup_temp_files(temp_files) - raise \ No newline at end of file + raise diff --git a/app/audio/sources.py b/app/audio/sources.py index 102bb55..2c49054 100644 --- a/app/audio/sources.py +++ b/app/audio/sources.py @@ -104,7 +104,7 @@ def get_url_file(url: str, max_file_size_mb: int = 100) -> Tuple[Optional[str], return temp_path, original_name or os.path.basename(temp_path), None except Exception as e: - logger.error("Ошибка при получении файла по URL %s: %s", url, e) + logger.error("Failed to fetch file from URL %s: %s", url, e) return None, None, f"Error retrieving file from URL: {str(e)}" @@ -144,5 +144,5 @@ def get_base64_file(base64_data: str, max_file_size_mb: int = 100) -> Tuple[Opti return temp_path, os.path.basename(temp_path), None except Exception as e: - logger.error("Ошибка при декодировании base64 данных: %s", e) + logger.error("Failed to decode base64 data: %s", e) return None, None, f"Error decoding base64 data: {str(e)}" diff --git a/app/audio/utils.py b/app/audio/utils.py index c9f656e..e1a6814 100644 --- a/app/audio/utils.py +++ b/app/audio/utils.py @@ -27,7 +27,7 @@ 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("Файл %s не моно-аудио", file_path) + logger.warning("File %s is not mono audio", file_path) frames = wav_file.readframes(-1) audio_array = np.frombuffer(frames, dtype=np.int16).astype(np.float32) / 32768.0 @@ -41,7 +41,7 @@ def load_audio(file_path: str, sr: int = 16000) -> Tuple[np.ndarray, int]: return audio_array, sampling_rate except Exception as e: - logger.error("Ошибка при загрузке аудио %s: %s", file_path, e) + logger.error("Failed to load audio %s: %s", file_path, e) raise @@ -56,7 +56,7 @@ def get_audio_duration(file_path: str) -> float: Длительность в секундах. """ if not os.path.exists(file_path): - raise Exception(f"Файл не существует: {file_path}") + raise Exception(f"File does not exist: {file_path}") cmd = [ "ffprobe", @@ -70,8 +70,8 @@ def get_audio_duration(file_path: str) -> float: result = subprocess.run(cmd, capture_output=True, text=True, check=True, timeout=10) return float(result.stdout.strip()) except subprocess.TimeoutExpired: - raise Exception(f"Таймаут при определении длительности файла {file_path}") + raise Exception(f"Timed out while determining duration for file {file_path}") except subprocess.CalledProcessError as e: - raise Exception(f"Ошибка ffprobe для файла {file_path}: {e.stderr}") + raise Exception(f"ffprobe error for file {file_path}: {e.stderr}") except (ValueError, TypeError) as e: - raise Exception(f"Ошибка при преобразовании длительности для файла {file_path}: {e}") + raise Exception(f"Failed to parse duration for file {file_path}: {e}") diff --git a/app/core/config.py b/app/core/config.py index 11ce8bb..702c5a9 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -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("Конфигурация успешно загружена из %s", config_path) + logger.info("Configuration loaded successfully from %s", config_path) return config except FileNotFoundError as e: - logger.error("Файл конфигурации не найден: %s", e) + logger.error("Configuration file not found: %s", e) raise except json.JSONDecodeError as e: - logger.error("Ошибка при загрузке конфигурации: %s", e) - raise \ No newline at end of file + logger.error("Failed to load configuration: %s", e) + raise diff --git a/app/core/transcriber.py b/app/core/transcriber.py index f0d4276..d995a1e 100644 --- a/app/core/transcriber.py +++ b/app/core/transcriber.py @@ -6,6 +6,7 @@ OpenAI для транскрибации аудиофайлов в текст. возможность использования Flash Attention 2 для ускорения работы модели на поддерживаемых GPU. """ +import os import time import threading import traceback @@ -14,6 +15,7 @@ import logging import numpy as np import torch +from huggingface_hub import snapshot_download from transformers import ( WhisperForConditionalGeneration, WhisperProcessor, @@ -94,19 +96,19 @@ class WhisperTranscriber: # Проверяем, что device_id является целым числом if not isinstance(device_id, int): - logger.warning("device_id должен быть целым числом, получено: %s. Используем значение по умолчанию 0", device_id) + logger.warning("device_id must be an integer, got: %s. Using default value 0", device_id) device_id = 0 # Проверяем, доступен ли запрошенный GPU device_count = torch.cuda.device_count() if device_id >= device_count: - logger.warning("Запрошенный GPU с индексом %s недоступен. Доступно GPU: %s. Используем GPU с индексом 0", device_id, device_count) + logger.warning("Requested GPU index %s is not available. Available GPU count: %s. Using GPU index 0", device_id, device_count) device_id = 0 - logger.info("Используется CUDA GPU с индексом %s для вычислений", device_id) + logger.info("Using CUDA GPU with index %s for inference", 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) для вычислений") + logger.info("Using MPS (Apple Silicon) for inference") # Обходное решение для MPS: PyTorch проверяет is_initialized() # при создании тензоров на MPS-устройстве, что вызывает ошибку # в однопроцессном режиме. @@ -114,7 +116,7 @@ class WhisperTranscriber: setattr(torch.distributed, "is_initialized", lambda: False) return torch.device("mps") else: - logger.info("Используется CPU для вычислений") + logger.info("Using CPU for inference") return torch.device("cpu") def _get_torch_dtype(self) -> torch.dtype: @@ -135,7 +137,8 @@ class WhisperTranscriber: Raises: Exception: Если не удалось загрузить модель. """ - logger.info("Загрузка модели из %s", self.model_path) + logger.info("Loading model from %s", self.model_path) + self.model_path = self._prepare_model_path() model_kwargs = dict( torch_dtype=self.torch_dtype, @@ -149,9 +152,9 @@ class WhisperTranscriber: capability = torch.cuda.get_device_capability(self.device.index) if capability[0] >= 8: use_flash_attn = True - logger.info("GPU поддерживает Flash Attention 2 (compute capability: %d.%d)", *capability) + logger.info("GPU supports Flash Attention 2 (compute capability: %d.%d)", *capability) else: - logger.info("GPU не поддерживает Flash Attention 2 (compute capability: %d.%d), используется стандартный режим", *capability) + logger.info("GPU does not support Flash Attention 2 (compute capability: %d.%d), using standard attention", *capability) try: if use_flash_attn: @@ -160,9 +163,9 @@ class WhisperTranscriber: self.model_path, **model_kwargs ).to(self.device) if use_flash_attn: - logger.info("Используется Flash Attention 2") + logger.info("Using Flash Attention 2") except Exception as e: - logger.warning("Не удалось загрузить модель с Flash Attention: %s", e) + logger.warning("Failed to load model with Flash Attention: %s", e) model_kwargs.pop("attn_implementation", None) self.model = WhisperForConditionalGeneration.from_pretrained( self.model_path, **model_kwargs @@ -182,7 +185,89 @@ class WhisperTranscriber: device=self.device, ) - logger.info("Модель успешно загружена и готова к использованию") + logger.info("Model loaded successfully and is ready to use") + + def _prepare_model_path(self) -> str: + """ + Ensures that the configured model path is usable. + + If ``model_path`` looks like a local path and the model is missing, + this method can optionally download a fallback Whisper model into that + directory. If ``model_path`` is a Hugging Face repo ID, it is returned + unchanged and transformers will resolve it normally. + """ + model_path = self.model_path + + looks_like_local_path = ( + os.path.isabs(model_path) + or model_path.startswith(".") + or os.path.sep in model_path + ) + if not looks_like_local_path: + return model_path + + if self._is_valid_local_model_dir(model_path): + return model_path + + if self.config.get("auto_download_missing_model", False): + return self._download_missing_model(model_path) + + if not os.path.isdir(model_path): + raise FileNotFoundError( + "Local model directory not found: " + f"'{model_path}'. Mount the Whisper model into the container, " + "set 'model_path' to that folder, or enable " + "'auto_download_missing_model'." + ) + + config_file = os.path.join(model_path, "config.json") + raise FileNotFoundError( + "The configured model directory exists but does not look like a " + f"Hugging Face model folder: '{model_path}'. Missing required file " + f"'{config_file}'. Make sure the directory contains files like " + "'config.json', tokenizer files, and model weights, or enable " + "'auto_download_missing_model'." + ) + + def _is_valid_local_model_dir(self, model_path: str) -> bool: + """Returns True when the directory looks like a local HF model folder.""" + return os.path.isdir(model_path) and os.path.isfile( + os.path.join(model_path, "config.json") + ) + + def _download_missing_model(self, model_path: str) -> str: + """ + Downloads a fallback Whisper model into the configured local directory. + """ + model_repo_id = self.config.get("model_repo_id", "openai/whisper-large-v3") + model_revision = self.config.get("model_revision") + + logger.warning( + "Local model was not found at %s. Downloading fallback model %s", + model_path, + model_repo_id, + ) + + os.makedirs(model_path, exist_ok=True) + snapshot_download( + repo_id=model_repo_id, + local_dir=model_path, + local_dir_use_symlinks=False, + revision=model_revision, + ) + + if not self._is_valid_local_model_dir(model_path): + raise FileNotFoundError( + "Model download completed but the target directory still does not " + f"look valid: '{model_path}'. Expected to find '{os.path.join(model_path, 'config.json')}'." + ) + + logger.info( + "Downloaded Whisper model %s into %s", + model_repo_id, + model_path, + ) + return model_path def transcribe(self, audio_path: str, return_timestamps: bool = None, language: str = None, temperature: float = None, @@ -209,7 +294,7 @@ class WhisperTranscriber: if temperature is None: temperature = self.temperature - logger.info("Начало транскрибации файла: %s", audio_path) + logger.info("Starting transcription for file: %s", audio_path) try: # Загрузка аудио в формате numpy array @@ -236,7 +321,7 @@ class WhisperTranscriber: # Если временные метки не запрошены, возвращаем только текст if not return_timestamps: transcribed_text = result.get("text", "") - logger.info("Транскрибация завершена: получено %s символов текста", len(transcribed_text)) + logger.info("Transcription completed: produced %s text characters", len(transcribed_text)) return transcribed_text # Если временные метки запрошены, обрабатываем и форматируем результат @@ -268,9 +353,9 @@ class WhisperTranscriber: "text": text }) else: - logger.warning("Временные метки запрошены, но не найдены в результате транскрибации") + logger.warning("Timestamps were requested but not found in the transcription result") - logger.info("Транскрибация с временными метками завершена: получено %s сегментов", len(segments)) + logger.info("Timestamped transcription completed: produced %s segments", len(segments)) # Возвращаем словарь с сегментами и полным текстом return { @@ -279,8 +364,8 @@ class WhisperTranscriber: } except Exception as e: - logger.error("Ошибка в процессе транскрибации аудиофайла '%s': %s", audio_path, e) - logger.error("Тип исключения: %s", type(e).__name__) + logger.error("Error while transcribing audio file '%s': %s", audio_path, e) + logger.error("Exception type: %s", type(e).__name__) logger.error("Traceback: %s", traceback.format_exc()) raise @@ -303,7 +388,7 @@ class WhisperTranscriber: - Если return_timestamps=True: словарь с ключами "segments" и "text" """ start_time = time.time() - logger.info("Начало обработки файла: %s", input_path) + logger.info("Starting file processing: %s", input_path) temp_files = [] @@ -317,17 +402,17 @@ class WhisperTranscriber: prompt=prompt) elapsed_time = time.time() - start_time - logger.info("Обработка и транскрибация завершены за %.2f секунд", elapsed_time) + logger.info("Processing and transcription finished in %.2f seconds", elapsed_time) return result except Exception as e: elapsed_time = time.time() - start_time - logger.error("Ошибка при обработке файла '%s' через %.2f секунд: %s", input_path, elapsed_time, e) - logger.error("Тип исключения: %s", type(e).__name__) + logger.error("Error while processing file '%s' after %.2f seconds: %s", input_path, elapsed_time, e) + logger.error("Exception type: %s", type(e).__name__) logger.error("Traceback: %s", traceback.format_exc()) raise finally: # Очистка временных файлов - cleanup_temp_files(temp_files) \ No newline at end of file + cleanup_temp_files(temp_files) diff --git a/app/core/transcription_service.py b/app/core/transcription_service.py index 2d0ba84..57c7cc0 100644 --- a/app/core/transcription_service.py +++ b/app/core/transcription_service.py @@ -50,8 +50,8 @@ class TranscriptionService: try: duration = get_audio_duration(file_path) except Exception as e: - logger.error("Ошибка при определении длительности файла: %s", e) - return {"error": f"Не удалось определить длительность аудиофайла: {e}"}, 500 + logger.error("Failed to determine file duration: %s", e) + return {"error": f"Failed to determine audio duration: {e}"}, 500 start_time = time.time() result = self.transcriber.process_file( @@ -86,6 +86,6 @@ class TranscriptionService: return response, 200 except Exception as e: - logger.error("Ошибка при транскрибации файла '%s': %s", filename, e) + logger.error("Failed to transcribe file '%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 8ef0f9a..5d6a37f 100644 --- a/app/history.py +++ b/app/history.py @@ -49,12 +49,12 @@ 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("Результат сохранён в историю: %s", history_path) + logger.info("Saved transcription result to history: %s", history_path) _cleanup_old_history(config) return history_path except Exception as e: - logger.error("Ошибка при сохранении истории: %s", e) + logger.error("Failed to save history: %s", e) return None @@ -80,6 +80,6 @@ def _cleanup_old_history(config: Dict) -> None: # Директории имеют формат YYYY-MM-DD if len(entry) == 10 and entry < cutoff_str: shutil.rmtree(entry_path, ignore_errors=True) - logger.info("Удалена старая директория истории: %s", entry) + logger.info("Removed old history directory: %s", entry) except Exception as e: - logger.warning("Ошибка при очистке старой истории: %s", e) + logger.warning("Failed to clean up old history: %s", e) diff --git a/app/infrastructure/async_tasks.py b/app/infrastructure/async_tasks.py index 936b731..ab2281a 100644 --- a/app/infrastructure/async_tasks.py +++ b/app/infrastructure/async_tasks.py @@ -93,14 +93,14 @@ class AsyncTaskManager: self.tasks[task_id]["result"] = result self.tasks[task_id]["completed_at"] = time.time() - logger.info("Задача %s завершена успешно", task_id) + logger.info("Task %s completed successfully", 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("Задача %s завершилась с ошибкой: %s", task_id, e) + logger.error("Task %s failed: %s", task_id, e) def get_task_status(self, task_id: str) -> Optional[Dict[str, Any]]: """ @@ -147,4 +147,4 @@ def transcribe_audio_async(file_path: str, transcription_service, params: Dict = params = params or {} def _transcribe(): return transcription_service.transcribe(file_path, "async_task", params) - return task_manager.run_task(_transcribe) \ No newline at end of file + return task_manager.run_task(_transcribe) diff --git a/app/infrastructure/log.py b/app/infrastructure/log.py index 44294eb..0dcfe23 100644 --- a/app/infrastructure/log.py +++ b/app/infrastructure/log.py @@ -91,7 +91,7 @@ class RequestLogger: return g.start_time = time.time() self.logger.info( - "%s %s от %s", + "%s %s from %s", request.method, request.path, self._get_client_ip(), extra={"type": "request"} ) @@ -101,7 +101,7 @@ class RequestLogger: return response processing_time = time.time() - getattr(g, 'start_time', time.time()) self.logger.info( - "%s за %.3f сек", + "%s in %.3f sec", response.status_code, processing_time, extra={"type": "response"} ) diff --git a/app/infrastructure/storage.py b/app/infrastructure/storage.py index b5d04b3..f4be552 100644 --- a/app/infrastructure/storage.py +++ b/app/infrastructure/storage.py @@ -21,7 +21,7 @@ def create_temp_file(suffix: str = ".wav") -> str: """ fd, temp_path = tempfile.mkstemp(suffix=suffix) os.close(fd) - logger.debug("Создан временный файл: %s", temp_path) + logger.debug("Created temporary file: %s", temp_path) return temp_path @@ -36,6 +36,6 @@ def cleanup_temp_files(file_paths: list) -> None: try: if os.path.exists(path): os.remove(path) - logger.debug("Удалён временный файл: %s", path) + logger.debug("Removed temporary file: %s", path) except Exception as e: - logger.warning("Не удалось очистить временный файл %s: %s", path, e) + logger.warning("Failed to clean up temporary file %s: %s", path, e) diff --git a/app/infrastructure/validation.py b/app/infrastructure/validation.py index 9069408..80e1a0b 100644 --- a/app/infrastructure/validation.py +++ b/app/infrastructure/validation.py @@ -51,11 +51,11 @@ 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("Попытка загрузки файла с неразрешенным расширением '%s'. " - "Имя файла: %s. Разрешенные расширения: %s", file_extension, filename, ", ".join(self.allowed_extensions)) + logger.warning("Attempt to upload a file with disallowed extension '%s'. " + "Filename: %s. Allowed extensions: %s", file_extension, filename, ", ".join(self.allowed_extensions)) - raise ValidationError(f"Расширение файла не разрешено. " - f"Разрешенные расширения: {', '.join(self.allowed_extensions)}") + raise ValidationError(f"File extension is not allowed. " + f"Allowed extensions: {', '.join(self.allowed_extensions)}") def validate_file_by_path(self, file_path: str, filename: str) -> bool: """ @@ -78,18 +78,18 @@ class FileValidator: file_size = os.path.getsize(file_path) max_size_bytes = self.max_file_size_mb * 1024 * 1024 if file_size > max_size_bytes: - raise ValidationError(f"Размер файла ({file_size / (1024*1024):.2f} МБ) " - f"превышает максимально допустимый ({self.max_file_size_mb} МБ)") + raise ValidationError(f"File size ({file_size / (1024*1024):.2f} MB) " + f"exceeds the maximum allowed size ({self.max_file_size_mb} MB)") # Проверка MIME-типа try: mime_type = magic.from_file(file_path, mime=True) if mime_type not in self.allowed_mime_types: - raise ValidationError(f"MIME-тип файла ({mime_type}) не разрешен. " - f"Разрешенные MIME-типы: {', '.join(self.allowed_mime_types)}") + raise ValidationError(f"File MIME type ({mime_type}) is not allowed. " + f"Allowed MIME types: {', '.join(self.allowed_mime_types)}") except ValidationError: raise except Exception as e: - logger.warning("Не удалось определить MIME-тип файла: %s", e) + logger.warning("Failed to determine file MIME type: %s", e) return True diff --git a/app/routes.py b/app/routes.py index 5c5f8b9..93311a7 100644 --- a/app/routes.py +++ b/app/routes.py @@ -94,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("Ошибка валидации файла '%s': %s", filename, e) + logger.warning("File validation failed for '%s': %s", filename, e) return jsonify({"error": str(e)}), 400 finally: cleanup_temp_files([temp_path]) @@ -122,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("Ошибка валидации файла '%s': %s", filename, e) + logger.warning("File validation failed for '%s': %s", filename, e) return jsonify({"error": str(e)}), 400 finally: cleanup_temp_files([temp_path]) @@ -150,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("Ошибка валидации файла '%s': %s", filename, e) + logger.warning("File validation failed for '%s': %s", filename, e) return jsonify({"error": str(e)}), 400 finally: cleanup_temp_files([temp_path]) diff --git a/config.docker.json b/config.docker.json index 086ee85..7d67054 100644 --- a/config.docker.json +++ b/config.docker.json @@ -1,6 +1,8 @@ { "service_port": 5042, "model_path": "/models/whisper", + "auto_download_missing_model": true, + "model_repo_id": "openai/whisper-large-v3", "language": "russian", "enable_history": true, "max_history_days": 30, diff --git a/config.json b/config.json index 01debf3..3c59f98 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,8 @@ { "service_port": 5042, "model_path": "/home/text-generation/models/whisper/podlodka-turbo", + "auto_download_missing_model": false, + "model_repo_id": "openai/whisper-large-v3", "language": "russian", "enable_history": true, "max_history_days": 30, @@ -23,4 +25,4 @@ "request_logging": { "exclude_endpoints": ["/health", "/static"] } -} \ No newline at end of file +} diff --git a/docker-compose.yml b/docker-compose.yml index 1d52586..b2b5230 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,8 @@ services: container_name: whisper-api-server ports: - "5042:5042" + environment: + HF_TOKEN: ${HF_TOKEN:-} volumes: - ./config.docker.json:/app/config.docker.json:ro - ./logs:/app/logs