Simplify architecture: remove overengineering, flatten structure
- Remove dead code: cache.py, context_managers.py, decorators.py, flask-limiter - Replace AudioSource ABC + FakeFile (5 classes) with 4 plain functions - Replace TempFileManager class with create_temp_file/cleanup_temp_files functions - Simplify RequestLogger from 233 to 65 lines, remove file reading side-effect - Convert HistoryLogger and AudioUtils classes to module-level functions - Remove unused speed_up_audio and audio_speed_factor config - Flatten single-file directories: shared/, api/, storage/, validation/, async_tasks/, logging/ - Merge logging config + request_logger into single infrastructure/log.py - Fix request_logging config key (was request_logger) - Trim CLAUDE.md to high-level only Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
38785398b3
commit
2e2bad8255
+8
-56
@@ -7,11 +7,10 @@
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import uuid
|
||||
from typing import Dict, Tuple
|
||||
import logging
|
||||
|
||||
from ..infrastructure.storage.file_manager import temp_file_manager
|
||||
from ..infrastructure.storage import create_temp_file, cleanup_temp_files
|
||||
|
||||
logger = logging.getLogger('app.audio_processor')
|
||||
|
||||
@@ -36,8 +35,7 @@ class AudioProcessor:
|
||||
self.config = config
|
||||
self.norm_level = config.get("norm_level", "-0.5")
|
||||
self.compand_params = config.get("compand_params", "0.3,1 -90,-90,-70,-70,-60,-20,0,0 -5 0 0.2")
|
||||
self.audio_speed_factor = config.get("audio_speed_factor", 1.25)
|
||||
|
||||
|
||||
def convert_to_wav(self, input_path: str) -> str:
|
||||
"""
|
||||
Конвертация входного аудиофайла в WAV формат с частотой дискретизации 16 кГц.
|
||||
@@ -66,7 +64,7 @@ class AudioProcessor:
|
||||
# Продолжаем конвертацию, чтобы быть уверенными в формате
|
||||
|
||||
# Создаем временный файл для WAV
|
||||
output_path, _ = temp_file_manager.create_temp_file(".wav")
|
||||
output_path = create_temp_file(".wav")
|
||||
|
||||
# Команда для конвертации
|
||||
cmd = [
|
||||
@@ -103,7 +101,7 @@ class AudioProcessor:
|
||||
subprocess.CalledProcessError: Если произошла ошибка при нормализации.
|
||||
"""
|
||||
# Создаем временный файл для нормализованного аудио
|
||||
output_path, _ = temp_file_manager.create_temp_file("_normalized.wav")
|
||||
output_path = create_temp_file("_normalized.wav")
|
||||
|
||||
# Команда для нормализации аудио с помощью sox
|
||||
cmd = [
|
||||
@@ -124,47 +122,6 @@ class AudioProcessor:
|
||||
logger.error(f"Ошибка при нормализации аудио: {e.stderr.decode()}")
|
||||
raise
|
||||
|
||||
def speed_up_audio(self, input_path: str) -> str:
|
||||
"""
|
||||
Ускоряет воспроизведение аудиофайла с использованием FFmpeg.
|
||||
|
||||
Args:
|
||||
input_path: Путь к WAV-файлу.
|
||||
|
||||
Returns:
|
||||
Путь к ускоренному WAV-файлу.
|
||||
|
||||
Raises:
|
||||
subprocess.CalledProcessError: Если произошла ошибка при ускорении.
|
||||
"""
|
||||
# Если ускорение не требуется (коэффициент = 1.0), возвращаем исходный файл
|
||||
if float(self.audio_speed_factor) == 1.0:
|
||||
logger.info(f"Ускорение не требуется (коэффициент = {self.audio_speed_factor})")
|
||||
return input_path
|
||||
|
||||
# Создаем временный файл для ускоренного аудио
|
||||
output_path, _ = temp_file_manager.create_temp_file("_speedup.wav")
|
||||
|
||||
# Команда для ускорения аудио с помощью FFmpeg
|
||||
cmd = [
|
||||
"ffmpeg",
|
||||
"-hide_banner",
|
||||
"-loglevel", "warning",
|
||||
"-i", input_path,
|
||||
"-filter:a", f"atempo={self.audio_speed_factor}",
|
||||
output_path
|
||||
]
|
||||
|
||||
logger.info(f"Ускорение аудио в {self.audio_speed_factor}x: {' '.join(cmd)}")
|
||||
|
||||
try:
|
||||
subprocess.run(cmd, check=True, capture_output=True)
|
||||
logger.info(f"Аудио ускорено: {output_path}")
|
||||
return output_path
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Ошибка при ускорении аудио: {e.stderr.decode()}")
|
||||
raise
|
||||
|
||||
def add_silence(self, input_path: str) -> str:
|
||||
"""
|
||||
Добавляет тишину в начало аудиофайла.
|
||||
@@ -179,7 +136,7 @@ class AudioProcessor:
|
||||
subprocess.CalledProcessError: Если произошла ошибка при добавлении тишины.
|
||||
"""
|
||||
# Создаем временный файл
|
||||
output_path, _ = temp_file_manager.create_temp_file("_silence.wav")
|
||||
output_path = create_temp_file("_silence.wav")
|
||||
|
||||
# Команда для добавления тишины в начало файла
|
||||
cmd = [
|
||||
@@ -223,19 +180,14 @@ class AudioProcessor:
|
||||
# Нормализация
|
||||
normalized_path = self.normalize_audio(wav_path)
|
||||
temp_files.append(normalized_path)
|
||||
|
||||
# УСКОРЕНИЕ ЗВУКА (НОВЫЙ ШАГ)
|
||||
speedup_path = self.speed_up_audio(normalized_path)
|
||||
if speedup_path != normalized_path: # Если был создан временный файл
|
||||
temp_files.append(speedup_path)
|
||||
|
||||
|
||||
# Добавление тишины
|
||||
silence_path = self.add_silence(speedup_path)
|
||||
silence_path = self.add_silence(normalized_path)
|
||||
temp_files.append(silence_path)
|
||||
|
||||
return silence_path, temp_files
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при обработке аудио {input_path}: {e}")
|
||||
temp_file_manager.cleanup_temp_files(temp_files)
|
||||
cleanup_temp_files(temp_files)
|
||||
raise
|
||||
+108
-284
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
Модуль sources.py содержит абстрактный класс AudioSource и его конкретные реализации
|
||||
для обработки различных источников аудиофайлов (загруженные файлы, URL, base64, локальные файлы).
|
||||
Модуль sources.py содержит функции для получения аудиофайлов
|
||||
из различных источников (загруженные файлы, URL, base64, локальные файлы).
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -8,305 +8,129 @@ import uuid
|
||||
import tempfile
|
||||
import base64
|
||||
import requests
|
||||
import abc
|
||||
from typing import Dict, Tuple, Optional, BinaryIO
|
||||
from typing import Tuple, Optional
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger('app.audio_sources')
|
||||
|
||||
class AudioSource(abc.ABC):
|
||||
"""Абстрактный класс для различных источников аудиофайлов.
|
||||
|
||||
Определяет интерфейс для различных источников аудио и предоставляет общие
|
||||
методы для работы с аудиофайлами, такие как проверка размера файла.
|
||||
|
||||
def _check_size(size_bytes: int, max_size_mb: int) -> Optional[str]:
|
||||
"""Проверяет размер файла. Возвращает сообщение об ошибке или None."""
|
||||
if size_bytes > max_size_mb * 1024 * 1024:
|
||||
return f"File exceeds maximum size of {max_size_mb}MB"
|
||||
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]]:
|
||||
"""
|
||||
|
||||
def __init__(self, max_file_size_mb: int = 100):
|
||||
"""
|
||||
Инициализация источника аудио.
|
||||
|
||||
Args:
|
||||
max_file_size_mb: Максимальный размер файла в МБ.
|
||||
"""
|
||||
self.max_file_size_mb = max_file_size_mb
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_audio_file(self) -> Tuple[Optional[BinaryIO], Optional[str], Optional[str]]:
|
||||
"""
|
||||
Получает аудиофайл из источника.
|
||||
|
||||
Returns:
|
||||
Кортеж (файловый объект, имя файла, сообщение об ошибке).
|
||||
В случае ошибки, возвращает (None, None, сообщение об ошибке).
|
||||
"""
|
||||
pass
|
||||
|
||||
def check_file_size(self, file: BinaryIO) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Проверяет размер файла.
|
||||
|
||||
Args:
|
||||
file: Файловый объект для проверки.
|
||||
|
||||
Returns:
|
||||
Кортеж (результат проверки, сообщение об ошибке).
|
||||
Если проверка пройдена, сообщение об ошибке будет None.
|
||||
"""
|
||||
file.seek(0, os.SEEK_END)
|
||||
file_length = file.tell()
|
||||
file.seek(0) # Сброс указателя файла после проверки размера
|
||||
|
||||
if file_length > self.max_file_size_mb * 1024 * 1024:
|
||||
return False, f"File exceeds maximum size of {self.max_file_size_mb}MB"
|
||||
|
||||
return True, None
|
||||
Получает аудиофайл из загруженных файлов Flask.
|
||||
|
||||
|
||||
class FakeFile:
|
||||
"""Имитирует файловый объект для унификации обработки из разных источников.
|
||||
|
||||
Позволяет обрабатывать файлы из различных источников (локальный путь, URL, base64)
|
||||
как стандартные файловые объекты Flask, обеспечивая совместимость с существующей
|
||||
логикой обработки файлов.
|
||||
Returns:
|
||||
Кортеж (путь к temp-файлу, имя файла, сообщение об ошибке).
|
||||
"""
|
||||
|
||||
def __init__(self, file: BinaryIO, filename: str):
|
||||
"""
|
||||
Инициализация объекта FakeFile.
|
||||
|
||||
Args:
|
||||
file: Исходный файловый объект или поток.
|
||||
filename: Имя файла для метаданных.
|
||||
"""
|
||||
self.file = file
|
||||
self.filename = filename
|
||||
if 'file' not in request_files:
|
||||
return None, None, "No file part"
|
||||
|
||||
def read(self):
|
||||
"""Чтение содержимого файла."""
|
||||
return self.file.read()
|
||||
file = request_files['file']
|
||||
|
||||
def seek(self, offset: int, whence: int = 0):
|
||||
"""Перемещение позиции чтения."""
|
||||
self.file.seek(offset, whence)
|
||||
if file.filename == '':
|
||||
return None, None, "No selected file"
|
||||
|
||||
def tell(self):
|
||||
"""Получение текущей позиции чтения."""
|
||||
return self.file.tell()
|
||||
# Проверка размера
|
||||
file.seek(0, os.SEEK_END)
|
||||
size = file.tell()
|
||||
file.seek(0)
|
||||
|
||||
def save(self, destination: str):
|
||||
"""
|
||||
Сохраняет содержимое файла в указанное место назначения.
|
||||
|
||||
Args:
|
||||
destination: Путь для сохранения файла.
|
||||
"""
|
||||
with open(destination, 'wb') as f:
|
||||
content = self.file.read()
|
||||
f.write(content)
|
||||
self.file.seek(0) # Сброс указателя после чтения
|
||||
error = _check_size(size, max_file_size_mb)
|
||||
if error:
|
||||
return None, None, error
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Возвращает имя файла."""
|
||||
return self.filename
|
||||
# Сохраняем во временный файл
|
||||
temp_path = _make_temp_path()
|
||||
file.save(temp_path)
|
||||
|
||||
return temp_path, file.filename, None
|
||||
|
||||
|
||||
class UploadedFileSource(AudioSource):
|
||||
"""Источник аудио для файлов, загруженных через HTTP-запрос."""
|
||||
|
||||
def __init__(self, request_files, max_file_size_mb: int = 100):
|
||||
"""
|
||||
Инициализация источника для загруженных файлов.
|
||||
|
||||
Args:
|
||||
request_files: Объект request.files из Flask.
|
||||
max_file_size_mb: Максимальный размер файла в МБ.
|
||||
"""
|
||||
super().__init__(max_file_size_mb)
|
||||
self.request_files = request_files
|
||||
|
||||
def get_audio_file(self) -> Tuple[Optional[BinaryIO], Optional[str], Optional[str]]:
|
||||
"""
|
||||
Получает аудиофайл из загруженных файлов.
|
||||
|
||||
Returns:
|
||||
Кортеж (файловый объект, имя файла, сообщение об ошибке).
|
||||
"""
|
||||
if 'file' not in self.request_files:
|
||||
return None, None, "No file part"
|
||||
|
||||
file = self.request_files['file']
|
||||
|
||||
if file.filename == '':
|
||||
return None, None, "No selected file"
|
||||
|
||||
# Проверка размера файла
|
||||
is_valid, error_message = self.check_file_size(file)
|
||||
if not is_valid:
|
||||
return None, None, error_message
|
||||
|
||||
return file, file.filename, None
|
||||
def get_url_file(url: str, max_file_size_mb: int = 100) -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
||||
"""
|
||||
Получает аудиофайл по URL.
|
||||
|
||||
Returns:
|
||||
Кортеж (путь к temp-файлу, имя файла, сообщение об ошибке).
|
||||
"""
|
||||
try:
|
||||
response = requests.get(url, stream=True)
|
||||
response.raise_for_status()
|
||||
|
||||
# Проверка размера по Content-Length
|
||||
content_length = response.headers.get('Content-Length')
|
||||
if content_length:
|
||||
error = _check_size(int(content_length), max_file_size_mb)
|
||||
if error:
|
||||
return None, None, error
|
||||
|
||||
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
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении файла по URL {url}: {e}")
|
||||
return None, None, f"Error retrieving file from URL: {str(e)}"
|
||||
|
||||
|
||||
class URLSource(AudioSource):
|
||||
"""Источник аудио для файлов, доступных по URL."""
|
||||
|
||||
def __init__(self, url: str, max_file_size_mb: int = 100):
|
||||
"""
|
||||
Инициализация источника для файлов по URL.
|
||||
|
||||
Args:
|
||||
url: URL аудиофайла.
|
||||
max_file_size_mb: Максимальный размер файла в МБ.
|
||||
"""
|
||||
super().__init__(max_file_size_mb)
|
||||
self.url = url
|
||||
self.temp_file_path = None
|
||||
self.temp_dir = None
|
||||
|
||||
def get_audio_file(self) -> Tuple[Optional[BinaryIO], Optional[str], Optional[str]]:
|
||||
"""
|
||||
Получает аудиофайл по URL.
|
||||
|
||||
Returns:
|
||||
Кортеж (файловый объект, имя файла, сообщение об ошибке).
|
||||
"""
|
||||
try:
|
||||
# Скачиваем файл по URL
|
||||
response = requests.get(self.url, stream=True)
|
||||
response.raise_for_status()
|
||||
|
||||
# Проверка размера файла (если сервер предоставил информацию о размере)
|
||||
content_length = response.headers.get('Content-Length')
|
||||
if content_length and int(content_length) > self.max_file_size_mb * 1024 * 1024:
|
||||
return None, None, f"File exceeds maximum size of {self.max_file_size_mb}MB"
|
||||
|
||||
# Сохраняем файл во временный файл
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.temp_file_path = os.path.join(self.temp_dir, str(uuid.uuid4()) + ".wav")
|
||||
|
||||
with open(self.temp_file_path, 'wb') as f:
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
f.write(chunk)
|
||||
|
||||
# Открываем файл для обработки
|
||||
file = open(self.temp_file_path, 'rb')
|
||||
|
||||
# Создаем объект файла, как будто он пришел из request.files
|
||||
fake_file = FakeFile(file, os.path.basename(self.temp_file_path))
|
||||
|
||||
return fake_file, fake_file.filename, None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении файла по URL {self.url}: {e}")
|
||||
self.cleanup()
|
||||
return None, None, f"Error retrieving file from URL: {str(e)}"
|
||||
|
||||
def cleanup(self):
|
||||
"""Очищает временные файлы и директории."""
|
||||
if self.temp_file_path and os.path.exists(self.temp_file_path):
|
||||
os.remove(self.temp_file_path)
|
||||
if self.temp_dir and os.path.exists(self.temp_dir):
|
||||
os.rmdir(self.temp_dir)
|
||||
def get_base64_file(base64_data: str, max_file_size_mb: int = 100) -> Tuple[Optional[str], Optional[str], Optional[str]]:
|
||||
"""
|
||||
Получает аудиофайл из base64 данных.
|
||||
|
||||
Returns:
|
||||
Кортеж (путь к temp-файлу, имя файла, сообщение об ошибке).
|
||||
"""
|
||||
try:
|
||||
audio_data = base64.b64decode(base64_data)
|
||||
|
||||
error = _check_size(len(audio_data), max_file_size_mb)
|
||||
if error:
|
||||
return None, None, error
|
||||
|
||||
temp_path = _make_temp_path()
|
||||
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}")
|
||||
return None, None, f"Error decoding base64 data: {str(e)}"
|
||||
|
||||
|
||||
class Base64Source(AudioSource):
|
||||
"""Источник аудио для файлов, закодированных в base64."""
|
||||
|
||||
def __init__(self, base64_data: str, max_file_size_mb: int = 100):
|
||||
"""
|
||||
Инициализация источника для base64 файлов.
|
||||
|
||||
Args:
|
||||
base64_data: Данные аудиофайла в формате base64.
|
||||
max_file_size_mb: Максимальный размер файла в МБ.
|
||||
"""
|
||||
super().__init__(max_file_size_mb)
|
||||
self.base64_data = base64_data
|
||||
self.temp_file_path = None
|
||||
self.temp_dir = None
|
||||
|
||||
def get_audio_file(self) -> Tuple[Optional[BinaryIO], Optional[str], Optional[str]]:
|
||||
"""
|
||||
Получает аудиофайл из base64 данных.
|
||||
|
||||
Returns:
|
||||
Кортеж (файловый объект, имя файла, сообщение об ошибке).
|
||||
"""
|
||||
try:
|
||||
# Декодируем base64
|
||||
audio_data = base64.b64decode(self.base64_data)
|
||||
|
||||
# Проверка размера файла
|
||||
if len(audio_data) > self.max_file_size_mb * 1024 * 1024:
|
||||
return None, None, f"File exceeds maximum size of {self.max_file_size_mb}MB"
|
||||
|
||||
# Сохраняем файл во временный файл
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.temp_file_path = os.path.join(self.temp_dir, str(uuid.uuid4()) + ".wav")
|
||||
|
||||
with open(self.temp_file_path, 'wb') as f:
|
||||
f.write(audio_data)
|
||||
|
||||
# Открываем файл для обработки
|
||||
file = open(self.temp_file_path, 'rb')
|
||||
|
||||
# Создаем объект файла, как будто он пришел из request.files
|
||||
fake_file = FakeFile(file, os.path.basename(self.temp_file_path))
|
||||
|
||||
return fake_file, fake_file.filename, None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при декодировании base64 данных: {e}")
|
||||
self.cleanup()
|
||||
return None, None, f"Error decoding base64 data: {str(e)}"
|
||||
|
||||
def cleanup(self):
|
||||
"""Очищает временные файлы и директории."""
|
||||
if self.temp_file_path and os.path.exists(self.temp_file_path):
|
||||
os.remove(self.temp_file_path)
|
||||
if self.temp_dir and os.path.exists(self.temp_dir):
|
||||
os.rmdir(self.temp_dir)
|
||||
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}"
|
||||
|
||||
class LocalFileSource(AudioSource):
|
||||
"""Источник аудио для локальных файлов на сервере."""
|
||||
|
||||
def __init__(self, file_path: str, max_file_size_mb: int = 100):
|
||||
"""
|
||||
Инициализация источника для локальных файлов.
|
||||
|
||||
Args:
|
||||
file_path: Путь к локальному файлу.
|
||||
max_file_size_mb: Максимальный размер файла в МБ.
|
||||
"""
|
||||
super().__init__(max_file_size_mb)
|
||||
self.file_path = file_path
|
||||
|
||||
def get_audio_file(self) -> Tuple[Optional[BinaryIO], Optional[str], Optional[str]]:
|
||||
"""
|
||||
Получает локальный аудиофайл.
|
||||
|
||||
Returns:
|
||||
Кортеж (файловый объект, имя файла, сообщение об ошибке).
|
||||
"""
|
||||
if not os.path.exists(self.file_path):
|
||||
return None, None, f"File not found: {self.file_path}"
|
||||
|
||||
try:
|
||||
# Проверка размера файла
|
||||
file_size = os.path.getsize(self.file_path)
|
||||
if file_size > self.max_file_size_mb * 1024 * 1024:
|
||||
return None, None, f"File exceeds maximum size of {self.max_file_size_mb}MB"
|
||||
|
||||
# Открываем файл для обработки
|
||||
file = open(self.file_path, 'rb')
|
||||
|
||||
# Создаем объект файла, как будто он пришел из request.files
|
||||
fake_file = FakeFile(file, os.path.basename(self.file_path))
|
||||
|
||||
return fake_file, fake_file.filename, None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при открытии локального файла {self.file_path}: {e}")
|
||||
return None, None, f"Error opening local file: {str(e)}"
|
||||
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)}"
|
||||
|
||||
+62
-94
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Модуль utils.py содержит утилитарные функции для работы с аудио.
|
||||
Утилитарные функции для работы с аудио.
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -12,98 +12,66 @@ from typing import Tuple
|
||||
logger = logging.getLogger('app.audio_utils')
|
||||
|
||||
|
||||
class AudioUtils:
|
||||
"""Утилитарный класс для работы с аудио."""
|
||||
|
||||
@staticmethod
|
||||
def load_audio(file_path: str, sr: int = 16000) -> Tuple[np.ndarray, int]:
|
||||
"""
|
||||
Загрузка аудиофайла с использованием встроенной библиотеки wave.
|
||||
def load_audio(file_path: str, sr: int = 16000) -> Tuple[np.ndarray, int]:
|
||||
"""
|
||||
Загрузка аудиофайла с использованием встроенной библиотеки wave.
|
||||
|
||||
Args:
|
||||
file_path: Путь к аудиофайлу.
|
||||
sr: Целевая частота дискретизации.
|
||||
Args:
|
||||
file_path: Путь к аудиофайлу.
|
||||
sr: Целевая частота дискретизации.
|
||||
|
||||
Returns:
|
||||
Кортеж (массив numpy, частота дискретизации).
|
||||
|
||||
Raises:
|
||||
Exception: Если не удалось загрузить аудиофайл.
|
||||
"""
|
||||
try:
|
||||
# Открываем WAV файл
|
||||
with wave.open(file_path, 'rb') as wav_file:
|
||||
# Проверяем, что это моно-аудио
|
||||
if wav_file.getnchannels() != 1:
|
||||
logger.warning(f"Файл {file_path} не моно-аудио, конвертируем в моно")
|
||||
|
||||
# Читаем аудиоданные
|
||||
frames = wav_file.readframes(-1)
|
||||
# Конвертируем 16-битные целые числа в float32 в диапазоне [-1.0, 1.0]
|
||||
# 32768.0 - это 2^15, максимальное значение для 16-битного знакового целого
|
||||
audio_array = np.frombuffer(frames, dtype=np.int16).astype(np.float32) / 32768.0
|
||||
|
||||
# Получаем частоту дискретизации
|
||||
sampling_rate = wav_file.getframerate()
|
||||
|
||||
# Если частота дискретизации не совпадает с целевой, выполняем ресемплинг
|
||||
if sampling_rate != sr:
|
||||
from scipy.signal import resample
|
||||
num_samples = int(len(audio_array) * sr / sampling_rate)
|
||||
audio_array = resample(audio_array, num_samples)
|
||||
sampling_rate = sr
|
||||
|
||||
return audio_array, sampling_rate
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при загрузке аудио {file_path}: {e}")
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def get_audio_duration(file_path: str) -> float:
|
||||
"""
|
||||
Определяет длительность аудиофайла с использованием ffprobe.
|
||||
|
||||
Args:
|
||||
file_path: Путь к аудиофайлу.
|
||||
|
||||
Returns:
|
||||
Длительность в секундах.
|
||||
"""
|
||||
try:
|
||||
# Проверяем, что файл существует
|
||||
if not os.path.exists(file_path):
|
||||
logger.error(f"Файл не существует: {file_path}")
|
||||
raise Exception(f"Файл не существует: {file_path}")
|
||||
|
||||
cmd = [
|
||||
"ffprobe",
|
||||
"-v", "error",
|
||||
"-show_entries", "format=duration",
|
||||
"-of", "default=noprint_wrappers=1:nokey=1",
|
||||
file_path
|
||||
]
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
timeout=10 # Ограничение по времени выполнения
|
||||
)
|
||||
|
||||
duration = float(result.stdout.strip())
|
||||
return duration
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error(f"Таймаут при определении длительности файла {file_path}")
|
||||
raise Exception(f"Таймаут при определении длительности файла {file_path}")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Ошибка при выполнении ffprobe для файла {file_path}: {e.stderr}")
|
||||
raise Exception(f"Ошибка при выполнении ffprobe для файла {file_path}: {e.stderr}")
|
||||
except (ValueError, TypeError) as e:
|
||||
logger.error(f"Ошибка при преобразовании длительности для файла {file_path}: {e}")
|
||||
raise Exception(f"Ошибка при преобразовании длительности для файла {file_path}: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Неожиданная ошибка при определении длительности файла {file_path}: {e}")
|
||||
raise Exception(f"Неожиданная ошибка при определении длительности файла {file_path}: {e}")
|
||||
Returns:
|
||||
Кортеж (массив numpy, частота дискретизации).
|
||||
"""
|
||||
try:
|
||||
with wave.open(file_path, 'rb') as wav_file:
|
||||
if wav_file.getnchannels() != 1:
|
||||
logger.warning(f"Файл {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:
|
||||
from scipy.signal import resample
|
||||
num_samples = int(len(audio_array) * sr / sampling_rate)
|
||||
audio_array = resample(audio_array, num_samples)
|
||||
sampling_rate = sr
|
||||
|
||||
return audio_array, sampling_rate
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при загрузке аудио {file_path}: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def get_audio_duration(file_path: str) -> float:
|
||||
"""
|
||||
Определяет длительность аудиофайла с использованием ffprobe.
|
||||
|
||||
Args:
|
||||
file_path: Путь к аудиофайлу.
|
||||
|
||||
Returns:
|
||||
Длительность в секундах.
|
||||
"""
|
||||
if not os.path.exists(file_path):
|
||||
raise Exception(f"Файл не существует: {file_path}")
|
||||
|
||||
cmd = [
|
||||
"ffprobe",
|
||||
"-v", "error",
|
||||
"-show_entries", "format=duration",
|
||||
"-of", "default=noprint_wrappers=1:nokey=1",
|
||||
file_path
|
||||
]
|
||||
|
||||
try:
|
||||
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}")
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise Exception(f"Ошибка ffprobe для файла {file_path}: {e.stderr}")
|
||||
except (ValueError, TypeError) as e:
|
||||
raise Exception(f"Ошибка при преобразовании длительности для файла {file_path}: {e}")
|
||||
|
||||
Reference in New Issue
Block a user