This commit is contained in:
root
2025-07-12 16:12:06 +08:00
parent 808c143bf8
commit 2b5867342f
22 changed files with 1785 additions and 2 deletions

View File

@@ -0,0 +1,298 @@
"""
项目素材管理服务
"""
import json
import hashlib
from pathlib import Path
from typing import List, Dict, Any, Optional
from datetime import datetime
from python_core.utils.logger import logger
class ProjectMaterialService:
"""项目素材管理服务"""
def __init__(self):
logger.info("ProjectMaterialService 初始化完成")
def import_video_materials(
self,
video_segments: List[Any],
project_id: str,
project_directory: Path,
source_video_path: str,
material_tags: Optional[List[str]] = None
) -> Dict[str, Any]:
"""
导入视频素材到项目
Args:
video_segments: 视频片段列表
project_id: 项目ID
project_directory: 项目目录
source_video_path: 源视频路径
material_tags: 素材标签
Returns:
Dict: 导入结果
"""
try:
# 创建项目素材目录
uncategorized_dir = project_directory / "未分类文件夹"
uncategorized_dir.mkdir(parents=True, exist_ok=True)
# 加载现有的项目素材列表
material_list_file = project_directory / "project_material.json"
existing_materials = self._load_material_list(material_list_file)
# 处理视频片段
new_materials = []
imported_count = 0
skipped_count = 0
for segment in video_segments:
if not segment.success:
continue
try:
result = self._import_single_segment(
segment=segment,
uncategorized_dir=uncategorized_dir,
existing_materials=existing_materials,
project_id=project_id,
source_video_path=source_video_path,
material_tags=material_tags or []
)
if result["imported"]:
new_materials.append(result["material_info"])
imported_count += 1
logger.info(f"✅ 素材已导入: {result['original_filename']} -> {result['target_filename']}")
else:
skipped_count += 1
logger.info(f"📋 素材已存在,跳过: {result['original_filename']}")
except Exception as e:
logger.error(f"❌ 导入素材失败 {segment.output_path.name}: {e}")
skipped_count += 1
# 更新项目素材列表
if new_materials:
existing_materials.extend(new_materials)
self._save_material_list(material_list_file, existing_materials)
logger.info(f"📝 项目素材列表已更新: 新增 {len(new_materials)} 个素材")
return {
"success": True,
"imported_count": imported_count,
"skipped_count": skipped_count,
"total_count": len(video_segments),
"new_materials": new_materials
}
except Exception as e:
logger.error(f"❌ 项目素材导入失败: {e}")
return {
"success": False,
"error": str(e),
"imported_count": 0,
"skipped_count": 0,
"total_count": len(video_segments)
}
def _import_single_segment(
self,
segment: Any,
uncategorized_dir: Path,
existing_materials: List[Dict],
project_id: str,
source_video_path: str,
material_tags: List[str]
) -> Dict[str, Any]:
"""导入单个视频片段"""
# 复制文件到项目目录
import shutil
temp_target_path = uncategorized_dir / f"temp_{segment.output_path.name}"
shutil.copy2(segment.output_path, temp_target_path)
# 计算复制后文件的MD5
file_md5 = self._calculate_file_md5(temp_target_path)
# 检查是否已存在相同MD5的素材
if any(material.get('md5') == file_md5 for material in existing_materials):
# 删除临时文件
temp_target_path.unlink()
return {
"imported": False,
"original_filename": segment.output_path.name,
"reason": "duplicate_md5"
}
# 重命名为最终文件名
target_filename = f"{file_md5}.mp4"
target_path = uncategorized_dir / target_filename
temp_target_path.rename(target_path)
# 创建素材信息
material_info = {
"id": file_md5,
"md5": file_md5,
"original_filename": segment.output_path.name,
"filename": target_filename,
"file_path": str(target_path),
"relative_path": f"未分类文件夹/{target_filename}",
"file_size": segment.file_size,
"duration": segment.duration,
"start_time": segment.start_time,
"end_time": segment.end_time,
"scene_index": segment.scene_index,
"tags": material_tags.copy(),
"created_at": datetime.now().isoformat(),
"use_count": 0,
"source_video": source_video_path,
"project_id": project_id
}
return {
"imported": True,
"material_info": material_info,
"original_filename": segment.output_path.name,
"target_filename": target_filename
}
def _load_material_list(self, material_list_file: Path) -> List[Dict]:
"""加载项目素材列表"""
if not material_list_file.exists():
return []
try:
with open(material_list_file, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
logger.warning(f"⚠️ 读取现有素材列表失败: {e}")
return []
def _save_material_list(self, material_list_file: Path, materials: List[Dict]):
"""保存项目素材列表"""
try:
with open(material_list_file, 'w', encoding='utf-8') as f:
json.dump(materials, f, ensure_ascii=False, indent=2)
except Exception as e:
logger.error(f"❌ 保存项目素材列表失败: {e}")
raise
def _calculate_file_md5(self, file_path: Path) -> str:
"""计算文件MD5值"""
hash_md5 = hashlib.md5()
try:
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 as e:
logger.error(f"❌ 计算MD5失败 {file_path}: {e}")
# 如果计算MD5失败使用文件名和大小作为备用标识
return hashlib.md5(f"{file_path.name}_{file_path.stat().st_size}".encode()).hexdigest()
def get_project_materials(self, project_directory: Path) -> List[Dict]:
"""获取项目素材列表"""
material_list_file = project_directory / "project_material.json"
return self._load_material_list(material_list_file)
def add_material_tags(self, project_directory: Path, material_id: str, tags: List[str]) -> bool:
"""为素材添加标签"""
try:
material_list_file = project_directory / "project_material.json"
materials = self._load_material_list(material_list_file)
for material in materials:
if material.get('id') == material_id:
existing_tags = set(material.get('tags', []))
new_tags = existing_tags.union(set(tags))
material['tags'] = list(new_tags)
self._save_material_list(material_list_file, materials)
logger.info(f"✅ 素材标签已更新: {material_id}")
return True
logger.warning(f"⚠️ 未找到素材: {material_id}")
return False
except Exception as e:
logger.error(f"❌ 添加素材标签失败: {e}")
return False
def remove_material(self, project_directory: Path, material_id: str) -> bool:
"""删除项目素材"""
try:
material_list_file = project_directory / "project_material.json"
materials = self._load_material_list(material_list_file)
# 查找要删除的素材
material_to_remove = None
for i, material in enumerate(materials):
if material.get('id') == material_id:
material_to_remove = materials.pop(i)
break
if material_to_remove:
# 删除文件
file_path = Path(material_to_remove['file_path'])
if file_path.exists():
file_path.unlink()
logger.info(f"🗑️ 已删除素材文件: {file_path.name}")
# 更新素材列表
self._save_material_list(material_list_file, materials)
logger.info(f"✅ 素材已删除: {material_id}")
return True
else:
logger.warning(f"⚠️ 未找到素材: {material_id}")
return False
except Exception as e:
logger.error(f"❌ 删除素材失败: {e}")
return False
def get_material_stats(self, project_directory: Path) -> Dict[str, Any]:
"""获取项目素材统计信息"""
try:
materials = self.get_project_materials(project_directory)
total_count = len(materials)
total_size = sum(material.get('file_size', 0) for material in materials)
total_duration = sum(material.get('duration', 0) for material in materials)
# 按标签统计
tag_stats = {}
for material in materials:
for tag in material.get('tags', []):
tag_stats[tag] = tag_stats.get(tag, 0) + 1
# 按使用次数统计
used_count = sum(1 for material in materials if material.get('use_count', 0) > 0)
unused_count = total_count - used_count
return {
"total_count": total_count,
"total_size": total_size,
"total_duration": total_duration,
"used_count": used_count,
"unused_count": unused_count,
"tag_stats": tag_stats
}
except Exception as e:
logger.error(f"❌ 获取素材统计失败: {e}")
return {
"total_count": 0,
"total_size": 0,
"total_duration": 0,
"used_count": 0,
"unused_count": 0,
"tag_stats": {}
}