Files
root 1ff49a3c26 refactor: 统一使用相对导入,规范 Python 包结构
🏗️ **Python 包结构规范化**:

1. **导入方式统一**:
   - 移除所有 sys.path.append() hack 方式
   - 统一使用相对导入 (from ..config import settings)
   - 符合 Python 包管理最佳实践

2. **包结构简化**:
   - 简化 python_core/__init__.py,移除复杂依赖
   - 避免包初始化时的循环导入问题
   - 清理不必要的 try-except 导入逻辑

3. **模块运行方式**:
   - 支持标准的模块运行: python -m python_core.ai_video.video_generator
   - Rust 代码使用 -m 参数调用 Python 模块
   - 相对导入在模块运行时正常工作

4. **涉及文件修改**:
   - python_core/__init__.py: 简化包初始化
   - python_core/ai_video/video_generator.py: 相对导入
   - python_core/ai_video/cloud_storage.py: 移除 fallback 逻辑
   - python_core/ai_video/api_client.py: 统一相对导入
   - python_core/video_processing/core.py: 相对导入
   - python_core/audio_processing/core.py: 相对导入
   - python_core/utils/logger.py: 相对导入
   - python_core/services/*.py: 统一相对导入
   - src-tauri/src/commands/ai_video.rs: 使用模块运行方式

5. **代码质量提升**:
   - 移除重复的 sys.path 操作
   - 清理冗余的 try-except 导入
   - 统一的错误处理方式
   - 更清晰的模块依赖关系

 **改进效果**:
- 符合 Python 最佳实践 ✓
- 代码结构更清晰 ✓
- 易于维护和测试 ✓
- 消除 hack 式路径操作 ✓
- 支持标准模块运行 ✓

现在整个 Python 包结构规范且易于维护!
2025-07-10 14:47:32 +08:00

351 lines
12 KiB
Python

#!/usr/bin/env python3
"""
Audio Processing Core Module
音频处理核心模块
This module provides audio processing functionality using Librosa, Pydub, and other audio libraries.
"""
import argparse
import json
import sys
from pathlib import Path
from typing import Dict, Any, List, Tuple
import numpy as np
from pydub import AudioSegment
import librosa
import librosa.display
import matplotlib.pyplot as plt
import sys
import os
from ..config import settings
from ..utils import setup_logger, validate_audio_file
logger = setup_logger(__name__)
class AudioProcessor:
"""Main audio processing class."""
def __init__(self):
self.temp_dir = settings.temp_dir
self.cache_dir = settings.cache_dir
self.sample_rate = settings.default_sample_rate
def analyze_audio(self, file_path: str, analysis_type: str) -> Dict[str, Any]:
"""
Analyze audio file with specified analysis type.
Args:
file_path: Path to audio file
analysis_type: Type of analysis to perform
Returns:
Dictionary with analysis results
"""
try:
if not validate_audio_file(file_path):
raise ValueError(f"Invalid audio file: {file_path}")
logger.info(f"Analyzing audio: {analysis_type} on {file_path}")
# Load audio
y, sr = librosa.load(file_path, sr=self.sample_rate)
if analysis_type == "rhythm":
result = self._analyze_rhythm(y, sr)
elif analysis_type == "spectral":
result = self._analyze_spectral(y, sr)
elif analysis_type == "tempo":
result = self._analyze_tempo(y, sr)
elif analysis_type == "pitch":
result = self._analyze_pitch(y, sr)
elif analysis_type == "energy":
result = self._analyze_energy(y, sr)
elif analysis_type == "mfcc":
result = self._analyze_mfcc(y, sr)
else:
raise ValueError(f"Unknown analysis type: {analysis_type}")
return {
"status": "success",
"file_path": file_path,
"analysis_type": analysis_type,
"sample_rate": sr,
"duration": len(y) / sr,
"result": result
}
except Exception as e:
logger.error(f"Audio analysis failed: {str(e)}")
return {
"status": "error",
"error": str(e)
}
def _analyze_rhythm(self, y: np.ndarray, sr: int) -> Dict[str, Any]:
"""Analyze rhythm and beat tracking."""
# Beat tracking
tempo, beats = librosa.beat.beat_track(y=y, sr=sr)
beat_times = librosa.frames_to_time(beats, sr=sr)
# Onset detection
onset_frames = librosa.onset.onset_detect(y=y, sr=sr)
onset_times = librosa.frames_to_time(onset_frames, sr=sr)
# Rhythm patterns
beat_intervals = np.diff(beat_times)
rhythm_stability = 1.0 - np.std(beat_intervals) / np.mean(beat_intervals)
return {
"tempo": float(tempo),
"beat_count": len(beats),
"beat_times": beat_times.tolist(),
"onset_count": len(onset_frames),
"onset_times": onset_times.tolist(),
"rhythm_stability": float(rhythm_stability),
"average_beat_interval": float(np.mean(beat_intervals))
}
def _analyze_spectral(self, y: np.ndarray, sr: int) -> Dict[str, Any]:
"""Analyze spectral features."""
# Spectral centroid
spectral_centroids = librosa.feature.spectral_centroid(y=y, sr=sr)[0]
# Spectral rolloff
spectral_rolloff = librosa.feature.spectral_rolloff(y=y, sr=sr)[0]
# Spectral bandwidth
spectral_bandwidth = librosa.feature.spectral_bandwidth(y=y, sr=sr)[0]
# Zero crossing rate
zcr = librosa.feature.zero_crossing_rate(y)[0]
return {
"spectral_centroid_mean": float(np.mean(spectral_centroids)),
"spectral_centroid_std": float(np.std(spectral_centroids)),
"spectral_rolloff_mean": float(np.mean(spectral_rolloff)),
"spectral_rolloff_std": float(np.std(spectral_rolloff)),
"spectral_bandwidth_mean": float(np.mean(spectral_bandwidth)),
"spectral_bandwidth_std": float(np.std(spectral_bandwidth)),
"zero_crossing_rate_mean": float(np.mean(zcr)),
"zero_crossing_rate_std": float(np.std(zcr))
}
def _analyze_tempo(self, y: np.ndarray, sr: int) -> Dict[str, Any]:
"""Analyze tempo and timing."""
# Tempo estimation
tempo, beats = librosa.beat.beat_track(y=y, sr=sr)
# Dynamic tempo analysis
hop_length = 512
tempo_dynamic = librosa.beat.tempo(
onset_envelope=librosa.onset.onset_strength(y=y, sr=sr),
sr=sr,
hop_length=hop_length
)
return {
"tempo": float(tempo),
"tempo_confidence": float(np.std(tempo_dynamic)),
"tempo_stability": float(1.0 - np.std(tempo_dynamic) / np.mean(tempo_dynamic)) if np.mean(tempo_dynamic) > 0 else 0.0,
"beat_count": len(beats)
}
def _analyze_pitch(self, y: np.ndarray, sr: int) -> Dict[str, Any]:
"""Analyze pitch and harmonic content."""
# Pitch tracking using piptrack
pitches, magnitudes = librosa.piptrack(y=y, sr=sr)
# Extract fundamental frequency
pitch_values = []
for t in range(pitches.shape[1]):
index = magnitudes[:, t].argmax()
pitch = pitches[index, t]
if pitch > 0:
pitch_values.append(pitch)
if pitch_values:
pitch_mean = np.mean(pitch_values)
pitch_std = np.std(pitch_values)
pitch_range = np.max(pitch_values) - np.min(pitch_values)
else:
pitch_mean = pitch_std = pitch_range = 0.0
# Harmonic-percussive separation
y_harmonic, y_percussive = librosa.effects.hpss(y)
harmonic_ratio = np.mean(np.abs(y_harmonic)) / (np.mean(np.abs(y)) + 1e-8)
return {
"pitch_mean": float(pitch_mean),
"pitch_std": float(pitch_std),
"pitch_range": float(pitch_range),
"pitch_count": len(pitch_values),
"harmonic_ratio": float(harmonic_ratio)
}
def _analyze_energy(self, y: np.ndarray, sr: int) -> Dict[str, Any]:
"""Analyze energy and dynamics."""
# RMS energy
rms = librosa.feature.rms(y=y)[0]
# Short-time energy
frame_length = 2048
hop_length = 512
energy = []
for i in range(0, len(y) - frame_length, hop_length):
frame = y[i:i + frame_length]
energy.append(np.sum(frame ** 2))
energy = np.array(energy)
# Dynamic range
dynamic_range = np.max(rms) - np.min(rms)
return {
"rms_mean": float(np.mean(rms)),
"rms_std": float(np.std(rms)),
"rms_max": float(np.max(rms)),
"rms_min": float(np.min(rms)),
"energy_mean": float(np.mean(energy)),
"energy_std": float(np.std(energy)),
"dynamic_range": float(dynamic_range)
}
def _analyze_mfcc(self, y: np.ndarray, sr: int) -> Dict[str, Any]:
"""Analyze MFCC (Mel-frequency cepstral coefficients)."""
# Extract MFCC features
mfccs = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13)
# Statistical features for each MFCC coefficient
mfcc_features = {}
for i in range(mfccs.shape[0]):
mfcc_features[f"mfcc_{i+1}_mean"] = float(np.mean(mfccs[i]))
mfcc_features[f"mfcc_{i+1}_std"] = float(np.std(mfccs[i]))
return mfcc_features
def process_audio(self, input_path: str, output_path: str, operation: str, parameters: Dict[str, Any]) -> Dict[str, Any]:
"""
Process audio with specified operation and parameters.
Args:
input_path: Path to input audio file
output_path: Path to output audio file
operation: Type of operation to perform
parameters: Operation-specific parameters
Returns:
Dictionary with processing results
"""
try:
if not validate_audio_file(input_path):
raise ValueError(f"Invalid audio file: {input_path}")
logger.info(f"Processing audio: {operation} on {input_path}")
# Load audio with pydub for processing
audio = AudioSegment.from_file(input_path)
if operation == "trim":
result_audio = self._trim_audio(audio, parameters)
elif operation == "volume":
result_audio = self._adjust_volume(audio, parameters)
elif operation == "fade":
result_audio = self._apply_fade(audio, parameters)
elif operation == "normalize":
result_audio = self._normalize_audio(audio, parameters)
elif operation == "merge":
result_audio = self._merge_audio(parameters)
else:
raise ValueError(f"Unknown operation: {operation}")
# Export result
result_audio.export(output_path, format="wav")
return {
"status": "success",
"output_path": output_path,
"duration": len(result_audio) / 1000.0,
"channels": result_audio.channels,
"sample_rate": result_audio.frame_rate
}
except Exception as e:
logger.error(f"Audio processing failed: {str(e)}")
return {
"status": "error",
"error": str(e)
}
def _trim_audio(self, audio: AudioSegment, params: Dict[str, Any]) -> AudioSegment:
"""Trim audio to specified start and end times."""
start_ms = int(params.get("start_time", 0) * 1000)
end_ms = int(params.get("end_time", len(audio) / 1000) * 1000)
return audio[start_ms:end_ms]
def _adjust_volume(self, audio: AudioSegment, params: Dict[str, Any]) -> AudioSegment:
"""Adjust audio volume."""
volume_change = params.get("volume_db", 0)
return audio + volume_change
def _apply_fade(self, audio: AudioSegment, params: Dict[str, Any]) -> AudioSegment:
"""Apply fade in/out effects."""
fade_in_ms = int(params.get("fade_in", 0) * 1000)
fade_out_ms = int(params.get("fade_out", 0) * 1000)
result = audio
if fade_in_ms > 0:
result = result.fade_in(fade_in_ms)
if fade_out_ms > 0:
result = result.fade_out(fade_out_ms)
return result
def _normalize_audio(self, audio: AudioSegment, params: Dict[str, Any]) -> AudioSegment:
"""Normalize audio to specified level."""
target_dBFS = params.get("target_db", -20.0)
return audio.normalize().apply_gain(target_dBFS - audio.dBFS)
def _merge_audio(self, params: Dict[str, Any]) -> AudioSegment:
"""Merge multiple audio files."""
audio_paths = params.get("audio_paths", [])
if len(audio_paths) < 2:
raise ValueError("At least 2 audio files required for merge operation")
result = AudioSegment.from_file(audio_paths[0])
for path in audio_paths[1:]:
audio = AudioSegment.from_file(path)
result = result + audio
return result
def main():
"""Command line interface for audio processing."""
parser = argparse.ArgumentParser(description="Audio Processing Core")
parser.add_argument("--file", required=True, help="Audio file path")
parser.add_argument("--analysis", required=True, help="Analysis type to perform")
args = parser.parse_args()
try:
processor = AudioProcessor()
result = processor.analyze_audio(args.file, args.analysis)
print(json.dumps(result))
except Exception as e:
error_result = {
"status": "error",
"error": str(e)
}
print(json.dumps(error_result))
sys.exit(1)
if __name__ == "__main__":
main()