Refactor project structure and add new features
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
"""
|
||||
Модуль processor.py содержит класс AudioProcessor, предназначенный для предобработки аудиофайлов
|
||||
перед их использованием в системах распознавания речи. Класс предоставляет методы для конвертации
|
||||
аудио в формат WAV с частотой дискретизации 16 кГц, нормализации уровня громкости,
|
||||
добавления тишины в начало записи, а также для удаления временных файлов, созданных в процессе обработки.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import uuid
|
||||
from typing import Dict, Tuple
|
||||
import logging
|
||||
|
||||
from ..infrastructure.storage.file_manager import temp_file_manager
|
||||
from ..shared.context_managers import open_file
|
||||
|
||||
logger = logging.getLogger('app.audio_processor')
|
||||
|
||||
|
||||
class AudioProcessor:
|
||||
"""
|
||||
Класс для предобработки аудиофайлов перед распознаванием.
|
||||
|
||||
Attributes:
|
||||
config (Dict): Словарь с параметрами конфигурации.
|
||||
norm_level (str): Уровень нормализации аудио.
|
||||
compand_params (str): Параметры компрессора аудио.
|
||||
"""
|
||||
|
||||
def __init__(self, config: Dict):
|
||||
"""
|
||||
Инициализация обработчика аудио.
|
||||
|
||||
Args:
|
||||
config: Словарь с параметрами конфигурации.
|
||||
"""
|
||||
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 кГц.
|
||||
|
||||
Args:
|
||||
input_path: Путь к исходному аудиофайлу.
|
||||
|
||||
Returns:
|
||||
Путь к сконвертированному WAV-файлу.
|
||||
|
||||
Raises:
|
||||
subprocess.CalledProcessError: Если произошла ошибка при конвертации.
|
||||
"""
|
||||
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, _ = temp_file_manager.create_temp_file(".wav")
|
||||
|
||||
# Команда для конвертации
|
||||
cmd = [
|
||||
"ffmpeg",
|
||||
"-hide_banner",
|
||||
"-loglevel", "warning",
|
||||
"-i", input_path,
|
||||
"-ar", f"{audio_rate}",
|
||||
"-ac", "1", # Монофонический звук
|
||||
output_path
|
||||
]
|
||||
|
||||
logger.info(f"Конвертация в WAV: {' '.join(cmd)}")
|
||||
|
||||
try:
|
||||
subprocess.run(cmd, check=True, capture_output=True)
|
||||
logger.info(f"Файл конвертирован в WAV: {output_path}")
|
||||
return output_path
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Ошибка при конвертации в WAV: {e.stderr.decode()}")
|
||||
raise
|
||||
|
||||
def normalize_audio(self, input_path: str) -> str:
|
||||
"""
|
||||
Нормализация аудиофайла с использованием sox.
|
||||
|
||||
Args:
|
||||
input_path: Путь к WAV-файлу.
|
||||
|
||||
Returns:
|
||||
Путь к нормализованному WAV-файлу.
|
||||
|
||||
Raises:
|
||||
subprocess.CalledProcessError: Если произошла ошибка при нормализации.
|
||||
"""
|
||||
# Создаем временный файл для нормализованного аудио
|
||||
output_path, _ = temp_file_manager.create_temp_file("_normalized.wav")
|
||||
|
||||
# Команда для нормализации аудио с помощью sox
|
||||
cmd = [
|
||||
"sox",
|
||||
input_path,
|
||||
output_path,
|
||||
"norm", self.norm_level,
|
||||
"compand"
|
||||
] + self.compand_params.split()
|
||||
|
||||
logger.info(f"Нормализация аудио: {' '.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 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:
|
||||
"""
|
||||
Добавляет тишину в начало аудиофайла.
|
||||
|
||||
Args:
|
||||
input_path: Путь к аудиофайлу.
|
||||
|
||||
Returns:
|
||||
Путь к аудиофайлу с добавленной тишиной.
|
||||
|
||||
Raises:
|
||||
subprocess.CalledProcessError: Если произошла ошибка при добавлении тишины.
|
||||
"""
|
||||
# Создаем временный файл
|
||||
output_path, _ = temp_file_manager.create_temp_file("_silence.wav")
|
||||
|
||||
# Команда для добавления тишины в начало файла
|
||||
cmd = [
|
||||
"sox",
|
||||
input_path,
|
||||
output_path,
|
||||
"pad", "2.0", "1.0" # Добавление тишины в начале и в конце (секунды)
|
||||
]
|
||||
|
||||
logger.info(f"Добавление тишины: {' '.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 process_audio(self, input_path: str) -> Tuple[str, list]:
|
||||
"""
|
||||
Полная обработка аудиофайла: конвертация, нормализация и добавление тишины.
|
||||
|
||||
Args:
|
||||
input_path: Путь к исходному аудиофайлу.
|
||||
|
||||
Returns:
|
||||
Кортеж: (путь к обработанному файлу, список временных файлов для удаления)
|
||||
|
||||
Raises:
|
||||
Exception: Если произошла ошибка при обработке аудио.
|
||||
"""
|
||||
temp_files = []
|
||||
|
||||
try:
|
||||
# Конвертация в WAV
|
||||
wav_path = self.convert_to_wav(input_path)
|
||||
if wav_path != input_path: # Если был создан временный файл
|
||||
temp_files.append(wav_path)
|
||||
|
||||
# Нормализация
|
||||
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)
|
||||
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)
|
||||
raise
|
||||
@@ -0,0 +1,312 @@
|
||||
"""
|
||||
Модуль sources.py содержит абстрактный класс AudioSource и его конкретные реализации
|
||||
для обработки различных источников аудиофайлов (загруженные файлы, URL, base64, локальные файлы).
|
||||
"""
|
||||
|
||||
import os
|
||||
import uuid
|
||||
import tempfile
|
||||
import base64
|
||||
import requests
|
||||
import abc
|
||||
from typing import Dict, Tuple, Optional, BinaryIO
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger('app.audio_sources')
|
||||
|
||||
class AudioSource(abc.ABC):
|
||||
"""Абстрактный класс для различных источников аудиофайлов.
|
||||
|
||||
Определяет интерфейс для различных источников аудио и предоставляет общие
|
||||
методы для работы с аудиофайлами, такие как проверка размера файла.
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
|
||||
class FakeFile:
|
||||
"""Имитирует файловый объект для унификации обработки из разных источников.
|
||||
|
||||
Позволяет обрабатывать файлы из различных источников (локальный путь, URL, base64)
|
||||
как стандартные файловые объекты Flask, обеспечивая совместимость с существующей
|
||||
логикой обработки файлов.
|
||||
"""
|
||||
|
||||
def __init__(self, file: BinaryIO, filename: str):
|
||||
"""
|
||||
Инициализация объекта FakeFile.
|
||||
|
||||
Args:
|
||||
file: Исходный файловый объект или поток.
|
||||
filename: Имя файла для метаданных.
|
||||
"""
|
||||
self.file = file
|
||||
self.filename = filename
|
||||
|
||||
def read(self):
|
||||
"""Чтение содержимого файла."""
|
||||
return self.file.read()
|
||||
|
||||
def seek(self, offset: int, whence: int = 0):
|
||||
"""Перемещение позиции чтения."""
|
||||
self.file.seek(offset, whence)
|
||||
|
||||
def tell(self):
|
||||
"""Получение текущей позиции чтения."""
|
||||
return self.file.tell()
|
||||
|
||||
def save(self, destination: str):
|
||||
"""
|
||||
Сохраняет содержимое файла в указанное место назначения.
|
||||
|
||||
Args:
|
||||
destination: Путь для сохранения файла.
|
||||
"""
|
||||
with open(destination, 'wb') as f:
|
||||
content = self.file.read()
|
||||
f.write(content)
|
||||
self.file.seek(0) # Сброс указателя после чтения
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Возвращает имя файла."""
|
||||
return self.filename
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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)}"
|
||||
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
Модуль utils.py содержит утилитарные функции для работы с аудио.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import wave
|
||||
import numpy as np
|
||||
import logging
|
||||
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.
|
||||
|
||||
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}")
|
||||
Reference in New Issue
Block a user