Files
wehub-resource-sync 41b710f9c7
CI / Frontend checks (push) Failing after 0s
CI / Backend tests (push) Failing after 1s
I18n Documentation Sync / sync-docs (push) Failing after 0s
chore: import upstream snapshot with attribution
2026-07-13 12:28:40 +08:00

756 lines
37 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
数据同步服务 - 将处理结果同步到数据库
"""
import json
import logging
from typing import Dict, Any, List, Optional
from pathlib import Path
from sqlalchemy.orm import Session
from backend.models.clip import Clip, ClipStatus
from backend.models.collection import Collection, CollectionStatus
from backend.models.project import Project, ProjectStatus, ProjectType
from backend.models.task import Task, TaskStatus, TaskType
from datetime import datetime
logger = logging.getLogger(__name__)
class DataSyncService:
"""数据同步服务"""
def __init__(self, db: Session):
self.db = db
def sync_all_projects_from_filesystem(self, data_dir: Path) -> Dict[str, Any]:
"""从文件系统同步所有项目到数据库"""
try:
logger.info(f"开始从文件系统同步所有项目: {data_dir}")
projects_dir = data_dir / "projects"
if not projects_dir.exists():
logger.warning(f"项目目录不存在: {projects_dir}")
return {"success": False, "error": "项目目录不存在"}
synced_projects = []
failed_projects = []
# 遍历所有项目目录
for project_dir in projects_dir.iterdir():
if project_dir.is_dir() and not project_dir.name.startswith('.'):
project_id = project_dir.name
try:
result = self.sync_project_from_filesystem(project_id, project_dir)
if result["success"]:
synced_projects.append(project_id)
else:
failed_projects.append({"project_id": project_id, "error": result.get("error")})
except Exception as e:
logger.error(f"同步项目 {project_id} 失败: {str(e)}")
failed_projects.append({"project_id": project_id, "error": str(e)})
logger.info(f"同步完成: 成功 {len(synced_projects)} 个, 失败 {len(failed_projects)} 个")
return {
"success": True,
"synced_projects": synced_projects,
"failed_projects": failed_projects,
"total_synced": len(synced_projects),
"total_failed": len(failed_projects)
}
except Exception as e:
logger.error(f"同步所有项目失败: {str(e)}")
return {"success": False, "error": str(e)}
def sync_project_from_filesystem(self, project_id: str, project_dir: Path) -> Dict[str, Any]:
"""从文件系统同步单个项目到数据库"""
try:
logger.info(f"开始同步项目: {project_id}")
# 检查项目是否已存在于数据库
existing_project = self.db.query(Project).filter(Project.id == project_id).first()
if existing_project:
logger.info(f"项目 {project_id} 已存在于数据库,继续同步切片数据")
else:
# 读取项目元数据
project_metadata = self._read_project_metadata(project_dir)
if not project_metadata:
logger.warning(f"项目 {project_id} 没有元数据文件,创建基础项目记录")
project_metadata = {
"project_name": f"项目_{project_id[:8]}",
"created_at": datetime.now().isoformat(),
"status": "pending"
}
# 创建项目记录
project = Project(
id=project_id,
name=project_metadata.get("project_name", f"项目_{project_id[:8]}"),
description=project_metadata.get("description", ""),
project_type=ProjectType.KNOWLEDGE, # 默认类型
status=ProjectStatus.PENDING,
processing_config=project_metadata.get("processing_config", {}),
project_metadata=project_metadata
)
self.db.add(project)
self.db.commit()
self.db.refresh(project)
logger.info(f"项目 {project_id} 同步到数据库成功")
# 同步切片数据
clips_count = self._sync_clips_from_filesystem(project_id, project_dir)
# 同步合集数据
logger.info(f"开始同步项目 {project_id} 的合集数据")
collections_count = self._sync_collections_from_filesystem(project_id, project_dir)
logger.info(f"项目 {project_id} 合集同步完成,同步了 {collections_count} 个合集")
# 检查项目是否已完成处理,更新项目状态
self._update_project_status_if_completed(project_id, project_dir)
return {
"success": True,
"project_id": project_id,
"clips_synced": clips_count,
"collections_synced": collections_count
}
except Exception as e:
logger.error(f"同步项目 {project_id} 失败: {str(e)}")
self.db.rollback()
return {"success": False, "error": str(e)}
def _read_project_metadata(self, project_dir: Path) -> Optional[Dict[str, Any]]:
"""读取项目元数据"""
metadata_files = [
project_dir / "project.json",
project_dir / "metadata.json",
project_dir / "info.json"
]
for metadata_file in metadata_files:
if metadata_file.exists():
try:
with open(metadata_file, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
logger.warning(f"读取元数据文件失败 {metadata_file}: {e}")
return None
def _sync_clips_from_filesystem(self, project_id: str, project_dir: Path) -> int:
"""从文件系统同步切片数据"""
try:
# 查找切片数据文件
clips_files = [
project_dir / "step6_video" / "clips_metadata.json", # 最完整的数据源
project_dir / "step3_all_scored.json", # 优先使用正确的数据源
project_dir / "step4_title" / "step4_title.json",
project_dir / "step4_titles.json",
project_dir / "clips_metadata.json",
project_dir / "metadata" / "clips_metadata.json"
]
clips_data = None
for clips_file in clips_files:
logger.info(f"检查切片文件: {clips_file}")
if clips_file.exists():
try:
with open(clips_file, 'r', encoding='utf-8') as f:
clips_data = json.load(f)
logger.info(f"成功读取切片文件: {clips_file}, 数据长度: {len(clips_data) if isinstance(clips_data, list) else 'not list'}")
break
except Exception as e:
logger.warning(f"读取切片文件失败 {clips_file}: {e}")
else:
logger.info(f"切片文件不存在: {clips_file}")
if not clips_data:
logger.info(f"项目 {project_id} 没有找到切片数据")
return 0
# 确保clips_data是列表
if isinstance(clips_data, dict) and "clips" in clips_data:
clips_data = clips_data["clips"]
elif not isinstance(clips_data, list):
logger.warning(f"项目 {project_id} 切片数据格式不正确")
return 0
synced_count = 0
updated_count = 0
for clip_data in clips_data:
try:
# 检查切片是否已存在
existing_clip = self.db.query(Clip).filter(
Clip.project_id == project_id,
Clip.title == clip_data.get("generated_title", clip_data.get("title", ""))
).first()
if existing_clip:
# 更新现有切片的video_path和tags,强制使用项目内输出目录
clip_id = clip_data.get('id', str(synced_count + 1))
safe_title = clip_data.get('generated_title', clip_data.get('title', clip_data.get('outline', '')))
# 清理文件名,移除特殊字符
safe_title = "".join(c for c in safe_title if c.isalnum() or c in (' ', '-', '_')).rstrip()
safe_title = safe_title.replace(' ', '_')
# 强制使用项目内标准路径
from ..core.path_utils import get_project_directory
project_dir = get_project_directory(project_id)
project_clips_dir = project_dir / "output" / "clips"
project_clips_dir.mkdir(parents=True, exist_ok=True)
project_video_path = project_clips_dir / f"{clip_id}_{safe_title}.mp4"
# 兼容旧的全局输出目录,如果存在则迁移到项目目录
from ..core.path_utils import get_data_directory
legacy_video_path = get_data_directory() / "output" / "clips" / f"{clip_id}_{safe_title}.mp4"
try:
if legacy_video_path.exists() and not project_video_path.exists():
import shutil
shutil.copy2(legacy_video_path, project_video_path)
logger.info(f"迁移旧切片文件到项目目录: {legacy_video_path} -> {project_video_path}")
except Exception as _e:
logger.warning(f"迁移旧切片文件失败: {legacy_video_path} -> {project_video_path}: {_e}")
# 始终使用项目内路径
video_path = str(project_video_path)
logger.info(f"更新切片 {existing_clip.id} 的video_path: {video_path}")
existing_clip.video_path = video_path
if existing_clip.tags is None:
existing_clip.tags = [] # 确保tags是空列表而不是null
updated_count += 1
continue
# 转换时间格式
start_time = self._convert_time_to_seconds(clip_data.get('start_time', '00:00:00'))
end_time = self._convert_time_to_seconds(clip_data.get('end_time', '00:00:00'))
duration = end_time - start_time
# 构建视频文件路径,强制使用项目内目录
clip_id = clip_data.get('id', str(synced_count + 1))
title = clip_data.get('generated_title', clip_data.get('title', clip_data.get('outline', '')))
# 强制使用项目内路径
from ..core.path_utils import get_project_directory, get_data_directory
project_dir = get_project_directory(project_id)
project_clips_dir = project_dir / "output" / "clips"
project_clips_dir.mkdir(parents=True, exist_ok=True)
# 查找实际的文件名(保留特殊字符)
actual_filename = None
for file_path in project_clips_dir.glob(f"{clip_id}_*.mp4"):
actual_filename = file_path.name
break
if actual_filename:
project_video_path = project_clips_dir / actual_filename
else:
# 如果找不到实际文件,使用清理后的文件名作为后备
safe_title = "".join(c for c in title if c.isalnum() or c in (' ', '-', '_')).rstrip()
safe_title = safe_title.replace(' ', '_')
project_video_path = project_clips_dir / f"{clip_id}_{safe_title}.mp4"
# 兼容旧的全局输出目录,如果存在则迁移到项目目录
global_clips_dir = get_data_directory() / "output" / "clips"
if actual_filename:
global_video_path = global_clips_dir / actual_filename
else:
safe_title = "".join(c for c in title if c.isalnum() or c in (' ', '-', '_')).rstrip()
safe_title = safe_title.replace(' ', '_')
global_video_path = global_clips_dir / f"{clip_id}_{safe_title}.mp4"
if global_video_path.exists() and not project_video_path.exists():
import shutil
shutil.copy2(global_video_path, project_video_path)
logger.info(f"将切片文件从全局目录迁移到项目目录: {global_video_path} -> {project_video_path}")
# 始终使用项目内路径
video_path = str(project_video_path)
# 创建切片记录
clip = Clip(
project_id=project_id,
title=clip_data.get('generated_title', clip_data.get('title', clip_data.get('outline', ''))),
description=clip_data.get('recommend_reason', ''),
start_time=start_time,
end_time=end_time,
duration=duration,
score=clip_data.get('final_score', 0.0),
video_path=video_path,
tags=[], # 确保tags是空列表而不是null
clip_metadata=clip_data,
status=ClipStatus.COMPLETED
)
self.db.add(clip)
synced_count += 1
except Exception as e:
logger.error(f"同步切片失败: {e}")
continue
self.db.commit()
logger.info(f"项目 {project_id} 同步了 {synced_count} 个切片,更新了 {updated_count} 个切片")
return synced_count
except Exception as e:
logger.error(f"同步切片数据失败: {str(e)}")
return 0
def _sync_collections_from_filesystem(self, project_id: str, project_dir: Path) -> int:
"""从文件系统同步合集数据"""
try:
# 构建collections目录路径
collections_dir = project_dir / "output" / "collections"
# 查找合集数据文件
collections_files = [
project_dir / "step6_video" / "collections_metadata.json", # 最完整的数据源
project_dir / "step5_clustering" / "step5_clustering.json",
project_dir / "metadata" / "step5_collections.json", # 添加step5_collections.json
project_dir / "collections_metadata.json",
project_dir / "metadata" / "collections_metadata.json"
]
collections_data = None
for collections_file in collections_files:
logger.info(f"检查合集文件: {collections_file}")
if collections_file.exists():
try:
with open(collections_file, 'r', encoding='utf-8') as f:
collections_data = json.load(f)
logger.info(f"成功读取合集文件: {collections_file}, 数据长度: {len(collections_data) if isinstance(collections_data, list) else 'not list'}")
break
except Exception as e:
logger.warning(f"读取合集文件失败 {collections_file}: {e}")
else:
logger.info(f"合集文件不存在: {collections_file}")
if not collections_data:
logger.info(f"项目 {project_id} 没有找到合集数据")
return 0
# 确保collections_data是列表
if isinstance(collections_data, dict) and "collections" in collections_data:
collections_data = collections_data["collections"]
elif not isinstance(collections_data, list):
logger.warning(f"项目 {project_id} 合集数据格式不正确")
return 0
# 读取删除记录文件
deleted_collections_file = project_dir / "deleted_collections.json"
deleted_collections = set()
if deleted_collections_file.exists():
try:
with open(deleted_collections_file, 'r', encoding='utf-8') as f:
deleted_data = json.load(f)
deleted_collections = set(deleted_data.get('deleted_collection_ids', []))
except Exception as e:
logger.warning(f"读取删除记录失败: {e}")
synced_count = 0
for collection_data in collections_data:
try:
collection_id = collection_data.get("id", "")
collection_title = collection_data.get("collection_title", "")
# 检查是否已被删除
if collection_id in deleted_collections:
logger.info(f"合集 {collection_id} 已被删除,跳过同步")
continue
# 检查合集是否已存在
existing_collection = self.db.query(Collection).filter(
Collection.project_id == project_id,
Collection.name == collection_title
).first()
if existing_collection:
# 合集已存在,检查是否需要建立关联关系
collection = existing_collection
logger.info(f"合集 {collection_title} 已存在,检查关联关系")
else:
# 创建新合集
collection = None
# 构建合集视频文件路径,强制使用项目内目录
# 尝试多种可能的文件名格式
possible_filenames = [
f"{collection_id}_{collection_title}.mp4",
f"{collection_title}.mp4",
f"collection_{collection_id}.mp4"
]
from ..core.path_utils import get_project_directory, get_data_directory
project_dir = get_project_directory(project_id)
project_collections_dir = project_dir / "output" / "collections"
project_collections_dir.mkdir(parents=True, exist_ok=True)
video_path = None
# 首先在项目目录中查找
for filename in possible_filenames:
project_video_path = project_collections_dir / filename
if project_video_path.exists():
video_path = str(project_video_path)
break
# 如果项目目录中没找到,尝试全局目录并迁移
if not video_path:
for filename in possible_filenames:
legacy_video_path = get_data_directory() / "output" / "collections" / filename
if legacy_video_path.exists():
# 迁移到项目目录
project_video_path = project_collections_dir / filename
import shutil
shutil.copy2(legacy_video_path, project_video_path)
video_path = str(project_video_path)
logger.info(f"将合集文件从全局目录迁移到项目目录: {legacy_video_path} -> {project_video_path}")
break
# 如果还是没找到,使用项目内路径(文件可能还未生成)
if not video_path:
video_path = str(project_collections_dir / possible_filenames[0])
# 将数字格式的clip_ids转换为UUID格式
original_clip_ids = collection_data.get('clip_ids', [])
uuid_clip_ids = []
# 获取项目中所有切片的映射关系(数字ID -> UUID
clips = self.db.query(Clip).filter(Clip.project_id == project_id).all()
clip_id_mapping = {}
for clip in clips:
# 从clip_metadata中获取原始ID
if clip.clip_metadata and 'id' in clip.clip_metadata:
original_id = str(clip.clip_metadata['id'])
clip_id_mapping[original_id] = clip.id
# 转换clip_ids
for original_id in original_clip_ids:
if str(original_id) in clip_id_mapping:
uuid_clip_ids.append(clip_id_mapping[str(original_id)])
else:
logger.warning(f"找不到切片ID {original_id} 对应的UUID")
# 构建缩略图路径
thumbnail_filename = f"{collection_id}_{collection_title}_thumbnail.jpg"
thumbnail_path = collections_dir / thumbnail_filename
# 如果合集不存在,创建新合集
if not collection:
collection = Collection(
project_id=project_id,
name=collection_title,
description=collection_data.get('collection_summary', ''),
video_path=video_path,
export_path=video_path, # 设置export_path
thumbnail_path=str(thumbnail_path) if thumbnail_path.exists() else None,
collection_metadata={
'clip_ids': uuid_clip_ids, # 使用UUID格式的clip_ids
'original_clip_ids': original_clip_ids, # 保留原始数字ID
'collection_type': 'ai_recommended',
'original_id': collection_id
},
status=CollectionStatus.COMPLETED
)
self.db.add(collection)
self.db.flush() # 确保collection有ID
logger.info(f"创建新合集: {collection.id}")
else:
# 更新现有合集的元数据
if not collection.collection_metadata:
collection.collection_metadata = {}
collection.collection_metadata.update({
'clip_ids': uuid_clip_ids,
'original_clip_ids': original_clip_ids,
'collection_type': 'ai_recommended',
'original_id': collection_id
})
collection.video_path = video_path
collection.export_path = video_path # 设置export_path
logger.info(f"更新现有合集: {collection.id}")
# 建立合集和切片的关联关系
for i, clip_id in enumerate(uuid_clip_ids):
try:
# 检查切片是否存在
clip = self.db.query(Clip).filter(Clip.id == clip_id).first()
if clip:
# 检查关联关系是否已存在
from ..models.collection import clip_collection
existing_relation = self.db.execute(
clip_collection.select().where(
clip_collection.c.clip_id == clip_id,
clip_collection.c.collection_id == collection.id
)
).first()
if not existing_relation:
# 使用关联表插入记录
stmt = clip_collection.insert().values(
clip_id=clip_id,
collection_id=collection.id,
order_index=i
)
self.db.execute(stmt)
logger.info(f"建立合集 {collection.id} 和切片 {clip_id} 的关联关系")
else:
logger.info(f"合集 {collection.id} 和切片 {clip_id} 的关联关系已存在")
else:
logger.warning(f"切片 {clip_id} 不存在,跳过关联")
except Exception as e:
logger.error(f"建立合集和切片关联关系失败: {e}")
synced_count += 1
except Exception as e:
logger.error(f"同步合集失败: {e}")
continue
self.db.commit()
logger.info(f"项目 {project_id} 同步了 {synced_count} 个合集")
return synced_count
except Exception as e:
logger.error(f"同步合集数据失败: {str(e)}")
return 0
def sync_project_data(self, project_id: str, project_dir: Path) -> Dict[str, Any]:
"""同步项目数据到数据库"""
try:
logger.info(f"开始同步项目数据: {project_id}")
# 同步clips数据
clips_count = self._sync_clips(project_id, project_dir)
# 同步collections数据
collections_count = self._sync_collections(project_id, project_dir)
# 更新项目统计信息
self._update_project_stats(project_id, clips_count, collections_count)
logger.info(f"项目数据同步完成: {project_id}, clips: {clips_count}, collections: {collections_count}")
return {
"success": True,
"clips_synced": clips_count,
"collections_synced": collections_count
}
except Exception as e:
logger.error(f"同步项目数据失败: {str(e)}")
raise
def _sync_clips(self, project_id: str, project_dir: Path) -> int:
"""同步clips数据"""
clips_file = project_dir / "step4_titles.json"
if not clips_file.exists():
logger.warning(f"Clips文件不存在: {clips_file}")
return 0
try:
with open(clips_file, 'r', encoding='utf-8') as f:
clips_data = json.load(f)
clips_count = 0
for clip_data in clips_data:
# 检查是否已存在
existing_clip = self.db.query(Clip).filter(
Clip.project_id == project_id,
Clip.title == clip_data.get("generated_title")
).first()
if existing_clip:
logger.info(f"Clip已存在,跳过: {clip_data.get('generated_title')}")
continue
# 创建新的clip记录
clip = Clip(
project_id=project_id,
title=clip_data.get("generated_title", ""),
description=clip_data.get("outline", ""),
start_time=self._parse_time(clip_data.get("start_time", "00:00:00")),
end_time=self._parse_time(clip_data.get("end_time", "00:00:00")),
duration=self._calculate_duration(
clip_data.get("start_time", "00:00:00"),
clip_data.get("end_time", "00:00:00")
),
score=clip_data.get("final_score", 0.0),
status=ClipStatus.COMPLETED,
tags=[],
clip_metadata={
"outline": clip_data.get("outline"),
"content": clip_data.get("content", []),
"recommend_reason": clip_data.get("recommend_reason"),
"chunk_index": clip_data.get("chunk_index"),
"original_id": clip_data.get("id")
}
)
self.db.add(clip)
clips_count += 1
logger.info(f"创建clip: {clip.title}")
self.db.commit()
logger.info(f"同步了 {clips_count} 个clips")
return clips_count
except Exception as e:
logger.error(f"同步clips失败: {str(e)}")
self.db.rollback()
raise
def _sync_collections(self, project_id: str, project_dir: Path) -> int:
"""同步collections数据到数据库"""
collections_file = project_dir / "step5_collections.json"
if not collections_file.exists():
logger.warning(f"Collections文件不存在: {collections_file}")
return 0
try:
# 构建collections目录路径
collections_dir = project_dir / "output" / "collections"
with open(collections_file, 'r', encoding='utf-8') as f:
collections_data = json.load(f)
collections_count = 0
for collection_data in collections_data:
# 检查是否已存在
existing_collection = self.db.query(Collection).filter(
Collection.project_id == project_id,
Collection.name == collection_data.get("collection_title")
).first()
if existing_collection:
logger.info(f"Collection已存在,跳过: {collection_data.get('collection_title')}")
continue
# 构建缩略图路径
collection_id = collection_data.get("id", "")
collection_title = collection_data.get("collection_title", "")
thumbnail_filename = f"{collection_id}_{collection_title}_thumbnail.jpg"
thumbnail_path = collections_dir / thumbnail_filename
# 创建新的collection记录
collection = Collection(
project_id=project_id,
name=collection_data.get("collection_title", ""),
description=collection_data.get("collection_summary", ""),
theme="default",
status=CollectionStatus.COMPLETED,
tags=[],
thumbnail_path=str(thumbnail_path) if thumbnail_path.exists() else None,
collection_metadata={
"clip_ids": collection_data.get("clip_ids", []),
"original_id": collection_data.get("id"),
"collection_type": "ai_recommended" # 标记为AI推荐
}
)
self.db.add(collection)
collections_count += 1
logger.info(f"创建collection: {collection.name}")
self.db.commit()
logger.info(f"同步了 {collections_count} 个collections")
return collections_count
except Exception as e:
logger.error(f"同步collections失败: {str(e)}")
self.db.rollback()
raise
def _update_project_stats(self, project_id: str, clips_count: int, collections_count: int):
"""更新项目统计信息"""
try:
project = self.db.query(Project).filter(Project.id == project_id).first()
if project:
project.total_clips = clips_count
project.total_collections = collections_count
self.db.commit()
logger.info(f"更新项目统计: clips={clips_count}, collections={collections_count}")
except Exception as e:
logger.error(f"更新项目统计失败: {str(e)}")
def _parse_time(self, time_str: str) -> float:
"""解析时间字符串为秒数"""
try:
if ',' in time_str:
time_str = time_str.replace(',', '.')
parts = time_str.split(':')
if len(parts) == 3:
hours = int(parts[0])
minutes = int(parts[1])
seconds = float(parts[2])
return hours * 3600 + minutes * 60 + seconds
else:
return 0.0
except Exception:
return 0.0
def _calculate_duration(self, start_time: str, end_time: str) -> float:
"""计算持续时间"""
start_seconds = self._parse_time(start_time)
end_seconds = self._parse_time(end_time)
return end_seconds - start_seconds
def _convert_time_to_seconds(self, time_str: str) -> int:
"""将时间字符串转换为秒数"""
try:
# 处理格式 "00:00:00,120" 或 "00:00:00.120"
time_str = time_str.replace(',', '.')
parts = time_str.split(':')
hours = int(parts[0])
minutes = int(parts[1])
seconds_parts = parts[2].split('.')
seconds = int(seconds_parts[0])
milliseconds = int(seconds_parts[1]) if len(seconds_parts) > 1 else 0
total_seconds = hours * 3600 + minutes * 60 + seconds + milliseconds / 1000
return int(total_seconds)
except Exception as e:
logger.error(f"时间转换失败: {time_str}, 错误: {e}")
return 0
def _update_project_status_if_completed(self, project_id: str, project_dir: Path):
"""检查项目是否已完成处理,如果是则更新状态为completed"""
try:
# 检查是否有step6_video_output.json文件,这是处理完成的标志
step6_output_file = project_dir / "output" / "step6_video_output.json"
if step6_output_file.exists():
# 获取项目记录
project = self.db.query(Project).filter(Project.id == project_id).first()
if project and project.status != ProjectStatus.COMPLETED:
# 读取step6输出文件获取统计信息
try:
with open(step6_output_file, 'r', encoding='utf-8') as f:
step6_output = json.load(f)
# 更新项目状态和统计信息
project.status = ProjectStatus.COMPLETED
project.total_clips = step6_output.get("clips_count", 0)
project.total_collections = step6_output.get("collections_count", 0)
project.completed_at = datetime.now()
self.db.commit()
logger.info(f"项目 {project_id} 状态已更新为已完成,切片数: {project.total_clips}, 合集数: {project.total_collections}")
except Exception as e:
logger.error(f"读取step6输出文件失败: {e}")
# 即使读取失败,也标记为已完成
project.status = ProjectStatus.COMPLETED
project.completed_at = datetime.now()
self.db.commit()
logger.info(f"项目 {project_id} 状态已更新为已完成(无统计信息)")
except Exception as e:
logger.error(f"更新项目状态失败: {e}")