Add model auto-download and English logging

This commit is contained in:
Dymas
2026-06-06 12:47:59 +02:00
parent d8b92d5025
commit 0c7f93d067
17 changed files with 204 additions and 77 deletions
+36
View File
@@ -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 |
+4 -4
View File
@@ -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
return self.app
+11 -11
View File
@@ -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
raise
+2 -2
View File
@@ -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)}"
+6 -6
View File
@@ -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}")
+4 -4
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("Конфигурация успешно загружена из %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
logger.error("Failed to load configuration: %s", e)
raise
+107 -22
View File
@@ -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)
cleanup_temp_files(temp_files)
+3 -3
View File
@@ -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
+4 -4
View File
@@ -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)
+3 -3
View File
@@ -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)
return task_manager.run_task(_transcribe)
+2 -2
View File
@@ -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"}
)
+3 -3
View File
@@ -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)
+9 -9
View File
@@ -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
+3 -3
View File
@@ -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])
+2
View File
@@ -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,
+3 -1
View File
@@ -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"]
}
}
}
+2
View File
@@ -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