first commit
This commit is contained in:
21
python_core/__init__.py
Normal file
21
python_core/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
MixVideo V2 Python Core
|
||||
视频混剪软件核心处理模块
|
||||
|
||||
This module provides the core video and audio processing capabilities
|
||||
for the MixVideo V2 application.
|
||||
"""
|
||||
|
||||
__version__ = "2.0.0"
|
||||
__author__ = "MixVideo Team"
|
||||
|
||||
from .video_processing import VideoProcessor
|
||||
from .audio_processing import AudioProcessor
|
||||
from .services import FileManager, TaskQueue
|
||||
|
||||
__all__ = [
|
||||
"VideoProcessor",
|
||||
"AudioProcessor",
|
||||
"FileManager",
|
||||
"TaskQueue"
|
||||
]
|
||||
20
python_core/audio_processing/__init__.py
Normal file
20
python_core/audio_processing/__init__.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
Audio Processing Module
|
||||
音频处理模块
|
||||
|
||||
Handles audio editing, rhythm detection, and audio effects using Librosa, Pydub, and other audio libraries.
|
||||
"""
|
||||
|
||||
from .core import AudioProcessor
|
||||
from .rhythm_detection import RhythmDetector
|
||||
from .audio_effects import AudioEffects
|
||||
from .voice_separation import VoiceSeparator
|
||||
from .tts import TextToSpeech
|
||||
|
||||
__all__ = [
|
||||
"AudioProcessor",
|
||||
"RhythmDetector",
|
||||
"AudioEffects",
|
||||
"VoiceSeparator",
|
||||
"TextToSpeech"
|
||||
]
|
||||
351
python_core/audio_processing/core.py
Normal file
351
python_core/audio_processing/core.py
Normal file
@@ -0,0 +1,351 @@
|
||||
#!/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
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
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()
|
||||
114
python_core/config.py
Normal file
114
python_core/config.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
Configuration Module
|
||||
配置模块
|
||||
|
||||
Central configuration management for the MixVideo V2 application.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
try:
|
||||
from pydantic_settings import BaseSettings
|
||||
from pydantic import Field
|
||||
except ImportError:
|
||||
# Fallback for older pydantic versions
|
||||
from pydantic import BaseSettings, Field
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Application settings with environment variable support."""
|
||||
|
||||
# Application Info
|
||||
app_name: str = "MixVideo V2"
|
||||
app_version: str = "2.0.0"
|
||||
debug: bool = Field(default=False, env="DEBUG")
|
||||
|
||||
# Paths
|
||||
project_root: Path = Path(__file__).parent.parent
|
||||
temp_dir: Path = Field(default_factory=lambda: Path.home() / ".mixvideo" / "temp")
|
||||
cache_dir: Path = Field(default_factory=lambda: Path.home() / ".mixvideo" / "cache")
|
||||
projects_dir: Path = Field(default_factory=lambda: Path.home() / "MixVideo Projects")
|
||||
|
||||
# Video Processing
|
||||
max_video_resolution: str = "1920x1080"
|
||||
default_fps: int = 30
|
||||
default_video_codec: str = "libx264"
|
||||
default_audio_codec: str = "aac"
|
||||
|
||||
# Audio Processing
|
||||
default_sample_rate: int = 44100
|
||||
default_audio_bitrate: str = "192k"
|
||||
|
||||
# Performance
|
||||
max_workers: int = Field(default=4, env="MAX_WORKERS")
|
||||
chunk_size: int = 1024 * 1024 # 1MB chunks
|
||||
|
||||
# Redis (for task queue)
|
||||
redis_host: str = Field(default="localhost", env="REDIS_HOST")
|
||||
redis_port: int = Field(default=6379, env="REDIS_PORT")
|
||||
redis_db: int = Field(default=0, env="REDIS_DB")
|
||||
|
||||
# Logging
|
||||
log_level: str = Field(default="INFO", env="LOG_LEVEL")
|
||||
log_file: str = "mixvideo.log"
|
||||
|
||||
# File Formats
|
||||
supported_video_formats: list = [".mp4", ".avi", ".mov", ".mkv", ".wmv", ".flv"]
|
||||
supported_audio_formats: list = [".mp3", ".wav", ".aac", ".flac", ".ogg"]
|
||||
supported_image_formats: list = [".jpg", ".jpeg", ".png", ".bmp", ".tiff"]
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
env_file_encoding = "utf-8"
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
# Ensure directories exist
|
||||
self.temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.projects_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
# Global settings instance
|
||||
settings = Settings()
|
||||
|
||||
|
||||
class Config:
|
||||
"""Configuration helper class."""
|
||||
|
||||
@staticmethod
|
||||
def get_temp_path(filename: str = None) -> Path:
|
||||
"""Get temporary file path."""
|
||||
if filename:
|
||||
return settings.temp_dir / filename
|
||||
return settings.temp_dir
|
||||
|
||||
@staticmethod
|
||||
def get_cache_path(filename: str = None) -> Path:
|
||||
"""Get cache file path."""
|
||||
if filename:
|
||||
return settings.cache_dir / filename
|
||||
return settings.cache_dir
|
||||
|
||||
@staticmethod
|
||||
def get_project_path(project_name: str = None) -> Path:
|
||||
"""Get project directory path."""
|
||||
if project_name:
|
||||
return settings.projects_dir / project_name
|
||||
return settings.projects_dir
|
||||
|
||||
@staticmethod
|
||||
def is_supported_video(file_path: str) -> bool:
|
||||
"""Check if file is a supported video format."""
|
||||
return Path(file_path).suffix.lower() in settings.supported_video_formats
|
||||
|
||||
@staticmethod
|
||||
def is_supported_audio(file_path: str) -> bool:
|
||||
"""Check if file is a supported audio format."""
|
||||
return Path(file_path).suffix.lower() in settings.supported_audio_formats
|
||||
|
||||
@staticmethod
|
||||
def is_supported_image(file_path: str) -> bool:
|
||||
"""Check if file is a supported image format."""
|
||||
return Path(file_path).suffix.lower() in settings.supported_image_formats
|
||||
67
python_core/requirements.txt
Normal file
67
python_core/requirements.txt
Normal file
@@ -0,0 +1,67 @@
|
||||
# Core Video Processing
|
||||
moviepy>=1.0.3
|
||||
ffmpeg-python>=0.2.0
|
||||
opencv-python>=4.8.0
|
||||
PySceneDetect>=0.6.2
|
||||
|
||||
# Audio Processing
|
||||
librosa>=0.10.0
|
||||
pydub>=0.25.1
|
||||
pyaudio>=0.2.11
|
||||
spleeter>=2.3.2
|
||||
|
||||
# Audio Analysis & Generation
|
||||
audiolazy>=0.7
|
||||
madmom>=0.16.1
|
||||
music21>=8.1.0
|
||||
pysynth>=0.9.3
|
||||
magenta>=2.1.4
|
||||
|
||||
# AI & Machine Learning
|
||||
transformers>=4.30.0
|
||||
torch>=2.0.0
|
||||
torchvision>=0.15.0
|
||||
torchaudio>=2.0.0
|
||||
|
||||
# Automation & Control
|
||||
pyautogui>=0.9.54
|
||||
pynput>=1.7.6
|
||||
|
||||
# Text-to-Speech
|
||||
gTTS>=2.3.2
|
||||
pyttsx3>=2.90
|
||||
|
||||
# Task Queue & Distributed Processing
|
||||
celery>=5.3.0
|
||||
redis>=4.5.0
|
||||
|
||||
# Database & Search
|
||||
elasticsearch>=8.8.0
|
||||
|
||||
# Utilities
|
||||
numpy>=1.24.0
|
||||
scipy>=1.10.0
|
||||
matplotlib>=3.7.0
|
||||
pillow>=10.0.0
|
||||
requests>=2.31.0
|
||||
python-dotenv>=1.0.0
|
||||
|
||||
# Development & Testing
|
||||
pytest>=7.4.0
|
||||
pytest-asyncio>=0.21.0
|
||||
black>=23.0.0
|
||||
flake8>=6.0.0
|
||||
|
||||
# GUI Framework (if needed for standalone Python GUI)
|
||||
tkinter-tooltip>=2.0.0
|
||||
|
||||
# File Format Support
|
||||
python-magic>=0.4.27
|
||||
mutagen>=1.46.0
|
||||
|
||||
# Logging & Monitoring
|
||||
loguru>=0.7.0
|
||||
|
||||
# Configuration
|
||||
pydantic>=2.0.0
|
||||
pyyaml>=6.0
|
||||
18
python_core/services/__init__.py
Normal file
18
python_core/services/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Services Module
|
||||
服务层模块
|
||||
|
||||
Provides background services, file management, and task queue functionality.
|
||||
"""
|
||||
|
||||
from .file_manager import FileManager
|
||||
from .task_queue import TaskQueue
|
||||
from .project_manager import ProjectManager
|
||||
from .cache_manager import CacheManager
|
||||
|
||||
__all__ = [
|
||||
"FileManager",
|
||||
"TaskQueue",
|
||||
"ProjectManager",
|
||||
"CacheManager"
|
||||
]
|
||||
383
python_core/services/file_manager.py
Normal file
383
python_core/services/file_manager.py
Normal file
@@ -0,0 +1,383 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
File Manager Service
|
||||
文件管理服务
|
||||
|
||||
Handles file operations, media import, and asset management.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List, Optional, Union
|
||||
import hashlib
|
||||
import mimetypes
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from config import settings
|
||||
from utils import setup_logger, validate_video_file, validate_audio_file, validate_image_file, get_file_info
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
class FileManager:
|
||||
"""File management service."""
|
||||
|
||||
def __init__(self):
|
||||
self.temp_dir = settings.temp_dir
|
||||
self.cache_dir = settings.cache_dir
|
||||
self.projects_dir = settings.projects_dir
|
||||
|
||||
def import_file(self, source_path: str, project_path: str, copy_file: bool = True) -> Dict[str, Any]:
|
||||
"""
|
||||
Import a media file into a project.
|
||||
|
||||
Args:
|
||||
source_path: Source file path
|
||||
project_path: Project directory path
|
||||
copy_file: Whether to copy file to project assets
|
||||
|
||||
Returns:
|
||||
Dictionary with import result
|
||||
"""
|
||||
try:
|
||||
source = Path(source_path)
|
||||
if not source.exists():
|
||||
raise FileNotFoundError(f"Source file not found: {source_path}")
|
||||
|
||||
# Validate file type
|
||||
file_type = self._get_file_type(source_path)
|
||||
if file_type == "unknown":
|
||||
raise ValueError(f"Unsupported file type: {source.suffix}")
|
||||
|
||||
# Get file info
|
||||
file_info = get_file_info(source_path)
|
||||
if "error" in file_info:
|
||||
raise ValueError(f"Failed to get file info: {file_info['error']}")
|
||||
|
||||
# Determine destination
|
||||
project_dir = Path(project_path)
|
||||
assets_dir = project_dir / "assets"
|
||||
assets_dir.mkdir(exist_ok=True)
|
||||
|
||||
if copy_file:
|
||||
# Copy file to project assets
|
||||
dest_path = assets_dir / source.name
|
||||
|
||||
# Handle name conflicts
|
||||
counter = 1
|
||||
while dest_path.exists():
|
||||
stem = source.stem
|
||||
suffix = source.suffix
|
||||
dest_path = assets_dir / f"{stem}_{counter}{suffix}"
|
||||
counter += 1
|
||||
|
||||
shutil.copy2(source, dest_path)
|
||||
final_path = str(dest_path)
|
||||
logger.info(f"Copied file to: {final_path}")
|
||||
else:
|
||||
# Use original path
|
||||
final_path = source_path
|
||||
logger.info(f"Linked file: {final_path}")
|
||||
|
||||
# Generate file hash for deduplication
|
||||
file_hash = self._calculate_file_hash(final_path)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"file_path": final_path,
|
||||
"file_type": file_type,
|
||||
"file_info": file_info,
|
||||
"file_hash": file_hash,
|
||||
"imported_at": file_info.get("modified", 0)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to import file: {str(e)}")
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def export_file(self, source_path: str, dest_path: str,
|
||||
move_file: bool = False) -> Dict[str, Any]:
|
||||
"""
|
||||
Export a file from project to external location.
|
||||
|
||||
Args:
|
||||
source_path: Source file path
|
||||
dest_path: Destination file path
|
||||
move_file: Whether to move instead of copy
|
||||
|
||||
Returns:
|
||||
Dictionary with export result
|
||||
"""
|
||||
try:
|
||||
source = Path(source_path)
|
||||
dest = Path(dest_path)
|
||||
|
||||
if not source.exists():
|
||||
raise FileNotFoundError(f"Source file not found: {source_path}")
|
||||
|
||||
# Ensure destination directory exists
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Handle name conflicts
|
||||
if dest.exists():
|
||||
counter = 1
|
||||
while dest.exists():
|
||||
stem = dest.stem
|
||||
suffix = dest.suffix
|
||||
dest = dest.parent / f"{stem}_{counter}{suffix}"
|
||||
counter += 1
|
||||
|
||||
if move_file:
|
||||
shutil.move(source, dest)
|
||||
logger.info(f"Moved file to: {dest}")
|
||||
else:
|
||||
shutil.copy2(source, dest)
|
||||
logger.info(f"Copied file to: {dest}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"dest_path": str(dest),
|
||||
"operation": "move" if move_file else "copy"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to export file: {str(e)}")
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def list_project_assets(self, project_path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
List all assets in a project.
|
||||
|
||||
Args:
|
||||
project_path: Project directory path
|
||||
|
||||
Returns:
|
||||
Dictionary with asset list
|
||||
"""
|
||||
try:
|
||||
project_dir = Path(project_path)
|
||||
assets_dir = project_dir / "assets"
|
||||
|
||||
if not assets_dir.exists():
|
||||
return {
|
||||
"status": "success",
|
||||
"assets": [],
|
||||
"count": 0
|
||||
}
|
||||
|
||||
assets = []
|
||||
for file_path in assets_dir.rglob("*"):
|
||||
if file_path.is_file():
|
||||
file_type = self._get_file_type(str(file_path))
|
||||
if file_type != "unknown":
|
||||
file_info = get_file_info(str(file_path))
|
||||
|
||||
assets.append({
|
||||
"name": file_path.name,
|
||||
"path": str(file_path),
|
||||
"relative_path": str(file_path.relative_to(project_dir)),
|
||||
"type": file_type,
|
||||
"size": file_info.get("size", 0),
|
||||
"size_mb": file_info.get("size_mb", 0),
|
||||
"modified": file_info.get("modified", 0),
|
||||
"duration": file_info.get("duration"),
|
||||
"width": file_info.get("width"),
|
||||
"height": file_info.get("height"),
|
||||
"file_hash": self._calculate_file_hash(str(file_path))
|
||||
})
|
||||
|
||||
# Sort by name
|
||||
assets.sort(key=lambda x: x["name"].lower())
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"assets": assets,
|
||||
"count": len(assets)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list project assets: {str(e)}")
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def create_temp_file(self, suffix: str = "", prefix: str = "mixvideo_") -> str:
|
||||
"""
|
||||
Create a temporary file.
|
||||
|
||||
Args:
|
||||
suffix: File suffix/extension
|
||||
prefix: File prefix
|
||||
|
||||
Returns:
|
||||
Path to temporary file
|
||||
"""
|
||||
import tempfile
|
||||
|
||||
fd, temp_path = tempfile.mkstemp(suffix=suffix, prefix=prefix, dir=self.temp_dir)
|
||||
os.close(fd) # Close file descriptor
|
||||
|
||||
logger.debug(f"Created temp file: {temp_path}")
|
||||
return temp_path
|
||||
|
||||
def cleanup_temp_files(self, max_age_hours: int = 24) -> Dict[str, Any]:
|
||||
"""
|
||||
Clean up old temporary files.
|
||||
|
||||
Args:
|
||||
max_age_hours: Maximum age of files to keep
|
||||
|
||||
Returns:
|
||||
Dictionary with cleanup result
|
||||
"""
|
||||
try:
|
||||
from utils import clean_temp_files
|
||||
|
||||
clean_temp_files(self.temp_dir, max_age_hours)
|
||||
clean_temp_files(self.cache_dir, max_age_hours)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Cleaned temp files older than {max_age_hours} hours"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to cleanup temp files: {str(e)}")
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def get_disk_usage(self, path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Get disk usage information for a path.
|
||||
|
||||
Args:
|
||||
path: Directory path
|
||||
|
||||
Returns:
|
||||
Dictionary with disk usage info
|
||||
"""
|
||||
try:
|
||||
path_obj = Path(path)
|
||||
if not path_obj.exists():
|
||||
raise FileNotFoundError(f"Path not found: {path}")
|
||||
|
||||
# Get disk usage
|
||||
usage = shutil.disk_usage(path)
|
||||
|
||||
# Calculate directory size if it's a directory
|
||||
if path_obj.is_dir():
|
||||
dir_size = sum(f.stat().st_size for f in path_obj.rglob('*') if f.is_file())
|
||||
else:
|
||||
dir_size = path_obj.stat().st_size
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"path": path,
|
||||
"total_space": usage.total,
|
||||
"used_space": usage.used,
|
||||
"free_space": usage.free,
|
||||
"directory_size": dir_size,
|
||||
"usage_percent": (usage.used / usage.total) * 100
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get disk usage: {str(e)}")
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def _get_file_type(self, file_path: str) -> str:
|
||||
"""
|
||||
Determine file type based on extension and validation.
|
||||
|
||||
Args:
|
||||
file_path: Path to file
|
||||
|
||||
Returns:
|
||||
File type: 'video', 'audio', 'image', or 'unknown'
|
||||
"""
|
||||
if validate_video_file(file_path):
|
||||
return "video"
|
||||
elif validate_audio_file(file_path):
|
||||
return "audio"
|
||||
elif validate_image_file(file_path):
|
||||
return "image"
|
||||
else:
|
||||
return "unknown"
|
||||
|
||||
def _calculate_file_hash(self, file_path: str) -> str:
|
||||
"""
|
||||
Calculate MD5 hash of a file.
|
||||
|
||||
Args:
|
||||
file_path: Path to file
|
||||
|
||||
Returns:
|
||||
MD5 hash string
|
||||
"""
|
||||
try:
|
||||
hash_md5 = hashlib.md5()
|
||||
with open(file_path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(4096), b""):
|
||||
hash_md5.update(chunk)
|
||||
return hash_md5.hexdigest()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def find_duplicates(self, project_path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Find duplicate files in a project based on hash.
|
||||
|
||||
Args:
|
||||
project_path: Project directory path
|
||||
|
||||
Returns:
|
||||
Dictionary with duplicate files
|
||||
"""
|
||||
try:
|
||||
assets_result = self.list_project_assets(project_path)
|
||||
if assets_result["status"] != "success":
|
||||
return assets_result
|
||||
|
||||
# Group files by hash
|
||||
hash_groups = {}
|
||||
for asset in assets_result["assets"]:
|
||||
file_hash = asset["file_hash"]
|
||||
if file_hash:
|
||||
if file_hash not in hash_groups:
|
||||
hash_groups[file_hash] = []
|
||||
hash_groups[file_hash].append(asset)
|
||||
|
||||
# Find duplicates (groups with more than one file)
|
||||
duplicates = {hash_val: files for hash_val, files in hash_groups.items() if len(files) > 1}
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"duplicates": duplicates,
|
||||
"duplicate_count": sum(len(files) - 1 for files in duplicates.values()),
|
||||
"total_duplicate_size": sum(
|
||||
sum(file["size"] for file in files[1:]) # Skip first file in each group
|
||||
for files in duplicates.values()
|
||||
)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to find duplicates: {str(e)}")
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
}
|
||||
427
python_core/services/project_manager.py
Normal file
427
python_core/services/project_manager.py
Normal file
@@ -0,0 +1,427 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Project Manager Service
|
||||
项目管理服务
|
||||
|
||||
Handles project creation, loading, saving, and management.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from config import settings
|
||||
from utils import setup_logger, ensure_directory, get_unique_filename
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
class ProjectManager:
|
||||
"""Project management service."""
|
||||
|
||||
def __init__(self):
|
||||
self.projects_dir = settings.projects_dir
|
||||
self.temp_dir = settings.temp_dir
|
||||
|
||||
def create_project(self, name: str, description: str = "") -> Dict[str, Any]:
|
||||
"""
|
||||
Create a new project.
|
||||
|
||||
Args:
|
||||
name: Project name
|
||||
description: Project description
|
||||
|
||||
Returns:
|
||||
Dictionary with project information
|
||||
"""
|
||||
try:
|
||||
# Create project directory
|
||||
project_path = self.projects_dir / name
|
||||
ensure_directory(project_path)
|
||||
|
||||
# Create project structure
|
||||
(project_path / "assets").mkdir(exist_ok=True)
|
||||
(project_path / "exports").mkdir(exist_ok=True)
|
||||
(project_path / "temp").mkdir(exist_ok=True)
|
||||
|
||||
# Create project info
|
||||
project_info = {
|
||||
"id": str(uuid.uuid4()),
|
||||
"name": name,
|
||||
"description": description,
|
||||
"path": str(project_path),
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"modified_at": datetime.now().isoformat(),
|
||||
"version": "1.0",
|
||||
"settings": {
|
||||
"resolution": settings.max_video_resolution,
|
||||
"fps": settings.default_fps,
|
||||
"video_codec": settings.default_video_codec,
|
||||
"audio_codec": settings.default_audio_codec
|
||||
},
|
||||
"video_tracks": [],
|
||||
"audio_tracks": [],
|
||||
"timeline": {
|
||||
"duration": 0.0,
|
||||
"zoom": 1.0,
|
||||
"position": 0.0
|
||||
}
|
||||
}
|
||||
|
||||
# Save project file
|
||||
project_file = project_path / f"{name}.mixvideo"
|
||||
with open(project_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(project_info, f, indent=2, ensure_ascii=False)
|
||||
|
||||
logger.info(f"Created project: {name} at {project_path}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"project": project_info
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create project: {str(e)}")
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def load_project(self, project_path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Load an existing project.
|
||||
|
||||
Args:
|
||||
project_path: Path to project file or directory
|
||||
|
||||
Returns:
|
||||
Dictionary with project information
|
||||
"""
|
||||
try:
|
||||
path = Path(project_path)
|
||||
|
||||
# If it's a directory, look for .mixvideo file
|
||||
if path.is_dir():
|
||||
mixvideo_files = list(path.glob("*.mixvideo"))
|
||||
if not mixvideo_files:
|
||||
raise FileNotFoundError(f"No .mixvideo file found in {path}")
|
||||
project_file = mixvideo_files[0]
|
||||
else:
|
||||
project_file = path
|
||||
|
||||
if not project_file.exists():
|
||||
raise FileNotFoundError(f"Project file not found: {project_file}")
|
||||
|
||||
# Load project data
|
||||
with open(project_file, 'r', encoding='utf-8') as f:
|
||||
project_info = json.load(f)
|
||||
|
||||
# Update modified time
|
||||
project_info["modified_at"] = datetime.now().isoformat()
|
||||
|
||||
logger.info(f"Loaded project: {project_info['name']}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"project": project_info
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load project: {str(e)}")
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def save_project(self, project_info: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Save project to file.
|
||||
|
||||
Args:
|
||||
project_info: Project information dictionary
|
||||
|
||||
Returns:
|
||||
Dictionary with save result
|
||||
"""
|
||||
try:
|
||||
project_path = Path(project_info["path"])
|
||||
project_name = project_info["name"]
|
||||
|
||||
# Update modified time
|
||||
project_info["modified_at"] = datetime.now().isoformat()
|
||||
|
||||
# Save project file
|
||||
project_file = project_path / f"{project_name}.mixvideo"
|
||||
with open(project_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(project_info, f, indent=2, ensure_ascii=False)
|
||||
|
||||
logger.info(f"Saved project: {project_name}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Project saved to {project_file}"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save project: {str(e)}")
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def list_projects(self) -> Dict[str, Any]:
|
||||
"""
|
||||
List all projects in the projects directory.
|
||||
|
||||
Returns:
|
||||
Dictionary with list of projects
|
||||
"""
|
||||
try:
|
||||
projects = []
|
||||
|
||||
for project_file in self.projects_dir.glob("**/*.mixvideo"):
|
||||
try:
|
||||
with open(project_file, 'r', encoding='utf-8') as f:
|
||||
project_info = json.load(f)
|
||||
|
||||
# Add basic project info
|
||||
projects.append({
|
||||
"id": project_info.get("id"),
|
||||
"name": project_info.get("name"),
|
||||
"description": project_info.get("description", ""),
|
||||
"path": str(project_file.parent),
|
||||
"created_at": project_info.get("created_at"),
|
||||
"modified_at": project_info.get("modified_at"),
|
||||
"file_size": project_file.stat().st_size
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to read project file {project_file}: {str(e)}")
|
||||
continue
|
||||
|
||||
# Sort by modified time (newest first)
|
||||
projects.sort(key=lambda x: x.get("modified_at", ""), reverse=True)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"projects": projects,
|
||||
"count": len(projects)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list projects: {str(e)}")
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def delete_project(self, project_path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Delete a project.
|
||||
|
||||
Args:
|
||||
project_path: Path to project directory
|
||||
|
||||
Returns:
|
||||
Dictionary with deletion result
|
||||
"""
|
||||
try:
|
||||
import shutil
|
||||
|
||||
path = Path(project_path)
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"Project path not found: {path}")
|
||||
|
||||
if path.is_dir():
|
||||
shutil.rmtree(path)
|
||||
else:
|
||||
path.unlink()
|
||||
|
||||
logger.info(f"Deleted project: {path}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Project deleted: {path}"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete project: {str(e)}")
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def add_video_track(self, project_info: Dict[str, Any], file_path: str,
|
||||
start_time: float = 0.0, duration: Optional[float] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Add a video track to the project.
|
||||
|
||||
Args:
|
||||
project_info: Project information
|
||||
file_path: Path to video file
|
||||
start_time: Start time in timeline
|
||||
duration: Duration (auto-detect if None)
|
||||
|
||||
Returns:
|
||||
Updated project information
|
||||
"""
|
||||
try:
|
||||
from utils import get_file_info
|
||||
|
||||
file_info = get_file_info(file_path)
|
||||
if "error" in file_info:
|
||||
raise ValueError(f"Invalid video file: {file_info['error']}")
|
||||
|
||||
track_id = str(uuid.uuid4())
|
||||
video_track = {
|
||||
"id": track_id,
|
||||
"name": Path(file_path).stem,
|
||||
"file_path": file_path,
|
||||
"start_time": start_time,
|
||||
"duration": duration or file_info.get("duration", 0.0),
|
||||
"file_info": file_info,
|
||||
"effects": [],
|
||||
"volume": 1.0,
|
||||
"enabled": True
|
||||
}
|
||||
|
||||
project_info["video_tracks"].append(video_track)
|
||||
project_info["modified_at"] = datetime.now().isoformat()
|
||||
|
||||
logger.info(f"Added video track: {video_track['name']}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"project": project_info,
|
||||
"track_id": track_id
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to add video track: {str(e)}")
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def add_audio_track(self, project_info: Dict[str, Any], file_path: str,
|
||||
start_time: float = 0.0, duration: Optional[float] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Add an audio track to the project.
|
||||
|
||||
Args:
|
||||
project_info: Project information
|
||||
file_path: Path to audio file
|
||||
start_time: Start time in timeline
|
||||
duration: Duration (auto-detect if None)
|
||||
|
||||
Returns:
|
||||
Updated project information
|
||||
"""
|
||||
try:
|
||||
from utils import get_file_info
|
||||
|
||||
file_info = get_file_info(file_path)
|
||||
if "error" in file_info:
|
||||
raise ValueError(f"Invalid audio file: {file_info['error']}")
|
||||
|
||||
track_id = str(uuid.uuid4())
|
||||
audio_track = {
|
||||
"id": track_id,
|
||||
"name": Path(file_path).stem,
|
||||
"file_path": file_path,
|
||||
"start_time": start_time,
|
||||
"duration": duration or file_info.get("duration", 0.0),
|
||||
"file_info": file_info,
|
||||
"effects": [],
|
||||
"volume": 1.0,
|
||||
"enabled": True
|
||||
}
|
||||
|
||||
project_info["audio_tracks"].append(audio_track)
|
||||
project_info["modified_at"] = datetime.now().isoformat()
|
||||
|
||||
logger.info(f"Added audio track: {audio_track['name']}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"project": project_info,
|
||||
"track_id": track_id
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to add audio track: {str(e)}")
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
"""Command line interface for project management."""
|
||||
parser = argparse.ArgumentParser(description="Project Manager")
|
||||
parser.add_argument("--action", required=True,
|
||||
choices=["create", "load", "save", "list", "delete", "get_info"],
|
||||
help="Action to perform")
|
||||
parser.add_argument("--name", help="Project name")
|
||||
parser.add_argument("--path", help="Project path")
|
||||
parser.add_argument("--data", help="Project data as JSON string")
|
||||
parser.add_argument("--description", help="Project description")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
manager = ProjectManager()
|
||||
|
||||
if args.action == "create":
|
||||
if not args.name:
|
||||
raise ValueError("Project name is required for create action")
|
||||
result = manager.create_project(args.name, args.description or "")
|
||||
|
||||
elif args.action == "load":
|
||||
if not args.path:
|
||||
raise ValueError("Project path is required for load action")
|
||||
result = manager.load_project(args.path)
|
||||
|
||||
elif args.action == "save":
|
||||
if not args.data:
|
||||
raise ValueError("Project data is required for save action")
|
||||
project_data = json.loads(args.data)
|
||||
result = manager.save_project(project_data)
|
||||
|
||||
elif args.action == "list":
|
||||
result = manager.list_projects()
|
||||
|
||||
elif args.action == "delete":
|
||||
if not args.path:
|
||||
raise ValueError("Project path is required for delete action")
|
||||
result = manager.delete_project(args.path)
|
||||
|
||||
elif args.action == "get_info":
|
||||
if not args.path:
|
||||
raise ValueError("Project path is required for get_info action")
|
||||
result = manager.load_project(args.path)
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unknown action: {args.action}")
|
||||
|
||||
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()
|
||||
23
python_core/utils/__init__.py
Normal file
23
python_core/utils/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
Utilities Module
|
||||
工具模块
|
||||
|
||||
Common utilities and helper functions used across the application.
|
||||
"""
|
||||
|
||||
from .logger import setup_logger
|
||||
from .validators import validate_video_file, validate_audio_file, validate_image_file
|
||||
from .helpers import format_duration, get_file_info, ensure_directory, get_unique_filename, format_file_size, clean_temp_files
|
||||
|
||||
__all__ = [
|
||||
"setup_logger",
|
||||
"validate_video_file",
|
||||
"validate_audio_file",
|
||||
"validate_image_file",
|
||||
"format_duration",
|
||||
"get_file_info",
|
||||
"ensure_directory",
|
||||
"get_unique_filename",
|
||||
"format_file_size",
|
||||
"clean_temp_files"
|
||||
]
|
||||
195
python_core/utils/helpers.py
Normal file
195
python_core/utils/helpers.py
Normal file
@@ -0,0 +1,195 @@
|
||||
"""
|
||||
Helper utilities for MixVideo V2
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Union
|
||||
import ffmpeg
|
||||
|
||||
|
||||
def format_duration(seconds: float) -> str:
|
||||
"""
|
||||
Format duration in seconds to human-readable string.
|
||||
|
||||
Args:
|
||||
seconds: Duration in seconds
|
||||
|
||||
Returns:
|
||||
Formatted duration string (e.g., "1:23:45")
|
||||
"""
|
||||
hours = int(seconds // 3600)
|
||||
minutes = int((seconds % 3600) // 60)
|
||||
secs = int(seconds % 60)
|
||||
|
||||
if hours > 0:
|
||||
return f"{hours}:{minutes:02d}:{secs:02d}"
|
||||
else:
|
||||
return f"{minutes}:{secs:02d}"
|
||||
|
||||
|
||||
def get_file_info(file_path: Union[str, Path]) -> Dict[str, Any]:
|
||||
"""
|
||||
Get basic file information.
|
||||
|
||||
Args:
|
||||
file_path: Path to file
|
||||
|
||||
Returns:
|
||||
Dictionary with file information
|
||||
"""
|
||||
try:
|
||||
path = Path(file_path)
|
||||
|
||||
if not path.exists():
|
||||
return {"error": "File not found"}
|
||||
|
||||
stat = path.stat()
|
||||
|
||||
info = {
|
||||
"name": path.name,
|
||||
"size": stat.st_size,
|
||||
"size_mb": round(stat.st_size / (1024 * 1024), 2),
|
||||
"modified": stat.st_mtime,
|
||||
"extension": path.suffix.lower(),
|
||||
"is_file": path.is_file(),
|
||||
"is_directory": path.is_dir()
|
||||
}
|
||||
|
||||
# Try to get media info if it's a media file
|
||||
if path.suffix.lower() in ['.mp4', '.avi', '.mov', '.mkv', '.wmv', '.flv', '.mp3', '.wav', '.aac', '.flac', '.ogg']:
|
||||
try:
|
||||
probe = ffmpeg.probe(str(path))
|
||||
format_info = probe.get('format', {})
|
||||
|
||||
info.update({
|
||||
"duration": float(format_info.get('duration', 0)),
|
||||
"duration_formatted": format_duration(float(format_info.get('duration', 0))),
|
||||
"bitrate": int(format_info.get('bit_rate', 0)),
|
||||
"format_name": format_info.get('format_name', ''),
|
||||
})
|
||||
|
||||
# Video specific info
|
||||
video_streams = [s for s in probe.get('streams', []) if s.get('codec_type') == 'video']
|
||||
if video_streams:
|
||||
video_stream = video_streams[0]
|
||||
info.update({
|
||||
"width": int(video_stream.get('width', 0)),
|
||||
"height": int(video_stream.get('height', 0)),
|
||||
"fps": eval(video_stream.get('r_frame_rate', '0/1')),
|
||||
"video_codec": video_stream.get('codec_name', ''),
|
||||
})
|
||||
|
||||
# Audio specific info
|
||||
audio_streams = [s for s in probe.get('streams', []) if s.get('codec_type') == 'audio']
|
||||
if audio_streams:
|
||||
audio_stream = audio_streams[0]
|
||||
info.update({
|
||||
"sample_rate": int(audio_stream.get('sample_rate', 0)),
|
||||
"channels": int(audio_stream.get('channels', 0)),
|
||||
"audio_codec": audio_stream.get('codec_name', ''),
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
info["media_info_error"] = str(e)
|
||||
|
||||
return info
|
||||
|
||||
except Exception as e:
|
||||
return {"error": str(e)}
|
||||
|
||||
|
||||
def ensure_directory(directory: Union[str, Path]) -> Path:
|
||||
"""
|
||||
Ensure directory exists, create if it doesn't.
|
||||
|
||||
Args:
|
||||
directory: Directory path
|
||||
|
||||
Returns:
|
||||
Path object of the directory
|
||||
"""
|
||||
path = Path(directory)
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
def get_unique_filename(file_path: Union[str, Path]) -> Path:
|
||||
"""
|
||||
Get a unique filename by appending a number if file already exists.
|
||||
|
||||
Args:
|
||||
file_path: Original file path
|
||||
|
||||
Returns:
|
||||
Unique file path
|
||||
"""
|
||||
path = Path(file_path)
|
||||
|
||||
if not path.exists():
|
||||
return path
|
||||
|
||||
counter = 1
|
||||
while True:
|
||||
stem = path.stem
|
||||
suffix = path.suffix
|
||||
parent = path.parent
|
||||
|
||||
new_name = f"{stem}_{counter}{suffix}"
|
||||
new_path = parent / new_name
|
||||
|
||||
if not new_path.exists():
|
||||
return new_path
|
||||
|
||||
counter += 1
|
||||
|
||||
|
||||
def format_file_size(size_bytes: int) -> str:
|
||||
"""
|
||||
Format file size in bytes to human-readable string.
|
||||
|
||||
Args:
|
||||
size_bytes: Size in bytes
|
||||
|
||||
Returns:
|
||||
Formatted size string (e.g., "1.5 MB")
|
||||
"""
|
||||
if size_bytes == 0:
|
||||
return "0 B"
|
||||
|
||||
size_names = ["B", "KB", "MB", "GB", "TB"]
|
||||
i = 0
|
||||
size = float(size_bytes)
|
||||
|
||||
while size >= 1024.0 and i < len(size_names) - 1:
|
||||
size /= 1024.0
|
||||
i += 1
|
||||
|
||||
return f"{size:.1f} {size_names[i]}"
|
||||
|
||||
|
||||
def clean_temp_files(temp_dir: Union[str, Path], max_age_hours: int = 24):
|
||||
"""
|
||||
Clean old temporary files.
|
||||
|
||||
Args:
|
||||
temp_dir: Temporary directory path
|
||||
max_age_hours: Maximum age of files to keep in hours
|
||||
"""
|
||||
import time
|
||||
|
||||
temp_path = Path(temp_dir)
|
||||
if not temp_path.exists():
|
||||
return
|
||||
|
||||
current_time = time.time()
|
||||
max_age_seconds = max_age_hours * 3600
|
||||
|
||||
for file_path in temp_path.rglob('*'):
|
||||
if file_path.is_file():
|
||||
file_age = current_time - file_path.stat().st_mtime
|
||||
if file_age > max_age_seconds:
|
||||
try:
|
||||
file_path.unlink()
|
||||
except Exception:
|
||||
pass # Ignore errors when deleting files
|
||||
48
python_core/utils/logger.py
Normal file
48
python_core/utils/logger.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
Logging utilities for MixVideo V2
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from loguru import logger
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from config import settings
|
||||
|
||||
|
||||
def setup_logger(name: str = None):
|
||||
"""
|
||||
Set up logger with appropriate configuration.
|
||||
|
||||
Args:
|
||||
name: Logger name (optional)
|
||||
|
||||
Returns:
|
||||
Configured logger instance
|
||||
"""
|
||||
# Remove default handler
|
||||
logger.remove()
|
||||
|
||||
# Console handler
|
||||
logger.add(
|
||||
sys.stderr,
|
||||
level=settings.log_level,
|
||||
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
|
||||
colorize=True
|
||||
)
|
||||
|
||||
# File handler
|
||||
log_file = settings.temp_dir / settings.log_file
|
||||
logger.add(
|
||||
log_file,
|
||||
level=settings.log_level,
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
|
||||
rotation="10 MB",
|
||||
retention="7 days",
|
||||
compression="zip"
|
||||
)
|
||||
|
||||
return logger
|
||||
117
python_core/utils/validators.py
Normal file
117
python_core/utils/validators.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
File validation utilities for MixVideo V2
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from config import settings
|
||||
|
||||
|
||||
def validate_video_file(file_path: Union[str, Path]) -> bool:
|
||||
"""
|
||||
Validate if file is a supported video format.
|
||||
|
||||
Args:
|
||||
file_path: Path to video file
|
||||
|
||||
Returns:
|
||||
True if valid video file, False otherwise
|
||||
"""
|
||||
try:
|
||||
path = Path(file_path)
|
||||
|
||||
# Check if file exists
|
||||
if not path.exists():
|
||||
return False
|
||||
|
||||
# Check if it's a file (not directory)
|
||||
if not path.is_file():
|
||||
return False
|
||||
|
||||
# Check file extension
|
||||
if path.suffix.lower() not in settings.supported_video_formats:
|
||||
return False
|
||||
|
||||
# Check file size (not empty)
|
||||
if path.stat().st_size == 0:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def validate_audio_file(file_path: Union[str, Path]) -> bool:
|
||||
"""
|
||||
Validate if file is a supported audio format.
|
||||
|
||||
Args:
|
||||
file_path: Path to audio file
|
||||
|
||||
Returns:
|
||||
True if valid audio file, False otherwise
|
||||
"""
|
||||
try:
|
||||
path = Path(file_path)
|
||||
|
||||
# Check if file exists
|
||||
if not path.exists():
|
||||
return False
|
||||
|
||||
# Check if it's a file (not directory)
|
||||
if not path.is_file():
|
||||
return False
|
||||
|
||||
# Check file extension
|
||||
if path.suffix.lower() not in settings.supported_audio_formats:
|
||||
return False
|
||||
|
||||
# Check file size (not empty)
|
||||
if path.stat().st_size == 0:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def validate_image_file(file_path: Union[str, Path]) -> bool:
|
||||
"""
|
||||
Validate if file is a supported image format.
|
||||
|
||||
Args:
|
||||
file_path: Path to image file
|
||||
|
||||
Returns:
|
||||
True if valid image file, False otherwise
|
||||
"""
|
||||
try:
|
||||
path = Path(file_path)
|
||||
|
||||
# Check if file exists
|
||||
if not path.exists():
|
||||
return False
|
||||
|
||||
# Check if it's a file (not directory)
|
||||
if not path.is_file():
|
||||
return False
|
||||
|
||||
# Check file extension
|
||||
if path.suffix.lower() not in settings.supported_image_formats:
|
||||
return False
|
||||
|
||||
# Check file size (not empty)
|
||||
if path.stat().st_size == 0:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
return False
|
||||
18
python_core/video_processing/__init__.py
Normal file
18
python_core/video_processing/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Video Processing Module
|
||||
视频处理核心模块
|
||||
|
||||
Handles video editing, effects, and transformations using MoviePy, FFmpeg, and OpenCV.
|
||||
"""
|
||||
|
||||
from .core import VideoProcessor
|
||||
from .effects import EffectsProcessor
|
||||
from .scene_detection import SceneDetector
|
||||
from .format_converter import FormatConverter
|
||||
|
||||
__all__ = [
|
||||
"VideoProcessor",
|
||||
"EffectsProcessor",
|
||||
"SceneDetector",
|
||||
"FormatConverter"
|
||||
]
|
||||
243
python_core/video_processing/core.py
Normal file
243
python_core/video_processing/core.py
Normal file
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Video Processing Core Module
|
||||
视频处理核心模块
|
||||
|
||||
This module provides the main video processing functionality using MoviePy and FFmpeg.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
import moviepy.editor as mp
|
||||
from moviepy.video.fx import resize, crop, rotate
|
||||
from moviepy.audio.fx import volumex
|
||||
import ffmpeg
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from config import settings
|
||||
from utils import setup_logger, validate_video_file
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
class VideoProcessor:
|
||||
"""Main video processing class."""
|
||||
|
||||
def __init__(self):
|
||||
self.temp_dir = settings.temp_dir
|
||||
self.cache_dir = settings.cache_dir
|
||||
|
||||
def process_video(self, input_path: str, output_path: str, operation: str, parameters: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Process video with specified operation and parameters.
|
||||
|
||||
Args:
|
||||
input_path: Path to input video file
|
||||
output_path: Path to output video file
|
||||
operation: Type of operation to perform
|
||||
parameters: Operation-specific parameters
|
||||
|
||||
Returns:
|
||||
Dictionary with processing results
|
||||
"""
|
||||
try:
|
||||
if not validate_video_file(input_path):
|
||||
raise ValueError(f"Invalid video file: {input_path}")
|
||||
|
||||
logger.info(f"Processing video: {operation} on {input_path}")
|
||||
|
||||
# Load video
|
||||
video = mp.VideoFileClip(input_path)
|
||||
|
||||
# Apply operation based on type
|
||||
if operation == "trim":
|
||||
result_video = self._trim_video(video, parameters)
|
||||
elif operation == "resize":
|
||||
result_video = self._resize_video(video, parameters)
|
||||
elif operation == "crop":
|
||||
result_video = self._crop_video(video, parameters)
|
||||
elif operation == "rotate":
|
||||
result_video = self._rotate_video(video, parameters)
|
||||
elif operation == "adjust_brightness":
|
||||
result_video = self._adjust_brightness(video, parameters)
|
||||
elif operation == "adjust_contrast":
|
||||
result_video = self._adjust_contrast(video, parameters)
|
||||
elif operation == "add_text":
|
||||
result_video = self._add_text(video, parameters)
|
||||
elif operation == "merge":
|
||||
result_video = self._merge_videos(parameters)
|
||||
else:
|
||||
raise ValueError(f"Unknown operation: {operation}")
|
||||
|
||||
# Write output
|
||||
result_video.write_videofile(
|
||||
output_path,
|
||||
codec=settings.default_video_codec,
|
||||
audio_codec=settings.default_audio_codec,
|
||||
fps=parameters.get("fps", settings.default_fps)
|
||||
)
|
||||
|
||||
# Clean up
|
||||
video.close()
|
||||
result_video.close()
|
||||
|
||||
# Get output info
|
||||
output_info = self._get_video_info(output_path)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"output_path": output_path,
|
||||
"info": output_info
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Video processing failed: {str(e)}")
|
||||
return {
|
||||
"status": "error",
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def _trim_video(self, video: mp.VideoFileClip, params: Dict[str, Any]) -> mp.VideoFileClip:
|
||||
"""Trim video to specified start and end times."""
|
||||
start_time = params.get("start_time", 0)
|
||||
end_time = params.get("end_time", video.duration)
|
||||
return video.subclip(start_time, end_time)
|
||||
|
||||
def _resize_video(self, video: mp.VideoFileClip, params: Dict[str, Any]) -> mp.VideoFileClip:
|
||||
"""Resize video to specified dimensions."""
|
||||
width = params.get("width")
|
||||
height = params.get("height")
|
||||
|
||||
if width and height:
|
||||
return video.fx(resize, newsize=(width, height))
|
||||
elif width:
|
||||
return video.fx(resize, width=width)
|
||||
elif height:
|
||||
return video.fx(resize, height=height)
|
||||
else:
|
||||
raise ValueError("Width or height must be specified for resize operation")
|
||||
|
||||
def _crop_video(self, video: mp.VideoFileClip, params: Dict[str, Any]) -> mp.VideoFileClip:
|
||||
"""Crop video to specified region."""
|
||||
x1 = params.get("x1", 0)
|
||||
y1 = params.get("y1", 0)
|
||||
x2 = params.get("x2", video.w)
|
||||
y2 = params.get("y2", video.h)
|
||||
return video.fx(crop, x1=x1, y1=y1, x2=x2, y2=y2)
|
||||
|
||||
def _rotate_video(self, video: mp.VideoFileClip, params: Dict[str, Any]) -> mp.VideoFileClip:
|
||||
"""Rotate video by specified angle."""
|
||||
angle = params.get("angle", 0)
|
||||
return video.fx(rotate, angle)
|
||||
|
||||
def _adjust_brightness(self, video: mp.VideoFileClip, params: Dict[str, Any]) -> mp.VideoFileClip:
|
||||
"""Adjust video brightness."""
|
||||
factor = params.get("factor", 1.0)
|
||||
|
||||
def brightness_effect(get_frame, t):
|
||||
frame = get_frame(t)
|
||||
return np.clip(frame * factor, 0, 255).astype(np.uint8)
|
||||
|
||||
return video.fl(brightness_effect)
|
||||
|
||||
def _adjust_contrast(self, video: mp.VideoFileClip, params: Dict[str, Any]) -> mp.VideoFileClip:
|
||||
"""Adjust video contrast."""
|
||||
factor = params.get("factor", 1.0)
|
||||
|
||||
def contrast_effect(get_frame, t):
|
||||
frame = get_frame(t)
|
||||
return np.clip(128 + factor * (frame - 128), 0, 255).astype(np.uint8)
|
||||
|
||||
return video.fl(contrast_effect)
|
||||
|
||||
def _add_text(self, video: mp.VideoFileClip, params: Dict[str, Any]) -> mp.VideoFileClip:
|
||||
"""Add text overlay to video."""
|
||||
text = params.get("text", "")
|
||||
fontsize = params.get("fontsize", 50)
|
||||
color = params.get("color", "white")
|
||||
position = params.get("position", ("center", "bottom"))
|
||||
duration = params.get("duration", video.duration)
|
||||
|
||||
txt_clip = mp.TextClip(text, fontsize=fontsize, color=color).set_position(position).set_duration(duration)
|
||||
return mp.CompositeVideoClip([video, txt_clip])
|
||||
|
||||
def _merge_videos(self, params: Dict[str, Any]) -> mp.VideoFileClip:
|
||||
"""Merge multiple videos."""
|
||||
video_paths = params.get("video_paths", [])
|
||||
if len(video_paths) < 2:
|
||||
raise ValueError("At least 2 videos required for merge operation")
|
||||
|
||||
clips = [mp.VideoFileClip(path) for path in video_paths]
|
||||
return mp.concatenate_videoclips(clips)
|
||||
|
||||
def _get_video_info(self, video_path: str) -> Dict[str, Any]:
|
||||
"""Get video file information."""
|
||||
try:
|
||||
probe = ffmpeg.probe(video_path)
|
||||
video_stream = next((stream for stream in probe['streams'] if stream['codec_type'] == 'video'), None)
|
||||
audio_stream = next((stream for stream in probe['streams'] if stream['codec_type'] == 'audio'), None)
|
||||
|
||||
info = {
|
||||
"duration": float(probe['format']['duration']),
|
||||
"size": int(probe['format']['size']),
|
||||
"bitrate": int(probe['format']['bit_rate']),
|
||||
}
|
||||
|
||||
if video_stream:
|
||||
info.update({
|
||||
"width": int(video_stream['width']),
|
||||
"height": int(video_stream['height']),
|
||||
"fps": eval(video_stream['r_frame_rate']),
|
||||
"video_codec": video_stream['codec_name']
|
||||
})
|
||||
|
||||
if audio_stream:
|
||||
info.update({
|
||||
"audio_codec": audio_stream['codec_name'],
|
||||
"sample_rate": int(audio_stream['sample_rate']),
|
||||
"channels": int(audio_stream['channels'])
|
||||
})
|
||||
|
||||
return info
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get video info: {str(e)}")
|
||||
return {}
|
||||
|
||||
|
||||
def main():
|
||||
"""Command line interface for video processing."""
|
||||
parser = argparse.ArgumentParser(description="Video Processing Core")
|
||||
parser.add_argument("--input", required=True, help="Input video file path")
|
||||
parser.add_argument("--output", required=True, help="Output video file path")
|
||||
parser.add_argument("--operation", required=True, help="Operation to perform")
|
||||
parser.add_argument("--parameters", required=True, help="Operation parameters as JSON string")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
parameters = json.loads(args.parameters)
|
||||
processor = VideoProcessor()
|
||||
result = processor.process_video(args.input, args.output, args.operation, parameters)
|
||||
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()
|
||||
Reference in New Issue
Block a user