Files
autoclip/backend/services/pipeline_adapter.py
T
2025-08-21 01:05:23 +08:00

675 lines
28 KiB
Python

"""Pipeline适配器服务 - 将shared/pipeline中的处理逻辑集成到新架构"""
import json
import logging
import asyncio
import sys
from typing import List, Dict, Any, Optional, Callable
from pathlib import Path
from datetime import datetime
# 导入新架构的模型和服务
from models.project import Project, ProjectStatus
from models.task import Task, TaskStatus
from core.database import get_db
from core.progress_manager import get_progress_manager
from core.config import get_project_root, get_data_directory, get_output_directory
from sqlalchemy.orm import Session
logger = logging.getLogger(__name__)
# 添加shared目录到Python路径
project_root = get_project_root()
shared_path = project_root / "shared"
if str(shared_path) not in sys.path:
sys.path.insert(0, str(shared_path))
# 导入shared/pipeline中的处理步骤
try:
from pipeline.step1_outline import run_step1_outline
from pipeline.step2_timeline import run_step2_timeline
from pipeline.step3_scoring import run_step3_scoring
from pipeline.step4_title import run_step4_title
from pipeline.step5_clustering import run_step5_clustering
from pipeline.step6_video import run_step6_video
logger.info("流水线模块导入成功")
except ImportError as e:
logger.warning(f"无法导入流水线模块: {e}")
# 定义占位符函数
def run_step1_outline(**kwargs):
logger.warning("流水线模块未正确导入,使用占位符函数")
return {"status": "skipped", "message": "流水线模块未正确导入"}
def run_step2_timeline(**kwargs):
logger.warning("流水线模块未正确导入,使用占位符函数")
return {"status": "skipped", "message": "流水线模块未正确导入"}
def run_step3_scoring(**kwargs):
logger.warning("流水线模块未正确导入,使用占位符函数")
return {"status": "skipped", "message": "流水线模块未正确导入"}
def run_step4_title(**kwargs):
logger.warning("流水线模块未正确导入,使用占位符函数")
return {"status": "skipped", "message": "流水线模块未正确导入"}
def run_step5_clustering(**kwargs):
logger.warning("流水线模块未正确导入,使用占位符函数")
return {"status": "skipped", "message": "流水线模块未正确导入"}
def run_step6_video(**kwargs):
logger.warning("流水线模块未正确导入,使用占位符函数")
return {"status": "skipped", "message": "流水线模块未正确导入"}
class PipelineAdapter:
"""Pipeline适配器,将shared/pipeline中的处理逻辑集成到新架构"""
def __init__(self, db: Session, task_id: Optional[str] = None, project_id: Optional[str] = None):
self.db = db
self.task_id = task_id
self.project_id = project_id
self.progress_manager = get_progress_manager(db)
# 获取项目根目录和输出目录
self.project_root = get_project_root()
self.data_dir = get_data_directory()
self.output_dir = get_output_directory()
# 初始化存储服务
if project_id:
from .storage_service import StorageService
self.storage_service = StorageService(project_id)
else:
self.storage_service = None
logger.info(f"Pipeline适配器初始化完成: project_root={self.project_root}, data_dir={self.data_dir}, output_dir={self.output_dir}")
def process_project_sync(self, project_id: str, input_video_path: str, input_srt_path: str) -> Dict[str, Any]:
"""
同步处理项目 - 执行完整的6步处理流程
Args:
project_id: 项目ID
input_video_path: 输入视频路径
input_srt_path: 输入SRT路径
Returns:
处理结果
"""
logger.info(f"开始处理项目: {project_id}")
try:
# 更新项目状态为处理中
self._update_project_status(project_id, ProjectStatus.PROCESSING)
# 准备项目目录
project_dir = self.data_dir / "projects" / project_id
project_dir.mkdir(parents=True, exist_ok=True)
# 准备输出目录
output_dir = self.output_dir / "metadata" / f"step1_chunks"
output_dir.mkdir(parents=True, exist_ok=True)
# 步骤1: 大纲提取
logger.info("开始步骤1: 大纲提取")
self._update_progress(project_id, 16, "大纲提取")
step1_result = self._run_step1_outline(project_id, input_srt_path, project_dir)
# 步骤2: 时间定位
logger.info("开始步骤2: 时间定位")
self._update_progress(project_id, 33, "时间定位")
step2_result = self._run_step2_timeline(project_id, project_dir)
# 步骤3: 内容评分
logger.info("开始步骤3: 内容评分")
self._update_progress(project_id, 50, "内容评分")
step3_result = self._run_step3_scoring(project_id, project_dir)
# 步骤4: 标题生成
logger.info("开始步骤4: 标题生成")
self._update_progress(project_id, 66, "标题生成")
step4_result = self._run_step4_title(project_id, project_dir)
# 步骤5: 主题聚类
logger.info("开始步骤5: 主题聚类")
self._update_progress(project_id, 83, "主题聚类")
step5_result = self._run_step5_clustering(project_id, project_dir)
# 步骤6: 视频切割
logger.info("开始步骤6: 视频切割")
self._update_progress(project_id, 100, "视频切割")
step6_result = self._run_step6_video(project_id, input_video_path, project_dir)
# 更新项目状态为完成
self._update_project_status(project_id, ProjectStatus.COMPLETED)
# 返回完整结果
result = {
"project_id": project_id,
"status": "completed",
"steps": {
"step1": step1_result,
"step2": step2_result,
"step3": step3_result,
"step4": step4_result,
"step5": step5_result,
"step6": step6_result
},
"message": "项目处理完成"
}
logger.info(f"项目处理完成: {project_id}")
return result
except Exception as e:
error_msg = f"项目处理失败: {str(e)}"
logger.error(error_msg)
# 更新项目状态为失败
self._update_project_status(project_id, ProjectStatus.FAILED)
raise Exception(error_msg)
def _run_step1_outline(self, project_id: str, srt_path: str, project_dir: Path) -> Dict[str, Any]:
"""运行步骤1: 大纲提取"""
try:
output_dir = project_dir / "step1_outline"
output_dir.mkdir(parents=True, exist_ok=True)
# 验证SRT文件路径
srt_file_path = Path(srt_path) if srt_path and srt_path.strip() else None
if not srt_file_path or not srt_file_path.exists():
logger.warning(f"SRT文件不存在或路径无效: {srt_path}")
# 返回空结果,让后续步骤处理
result = []
else:
result = run_step1_outline(
srt_path=srt_file_path,
output_path=output_dir / "step1_outline.json",
metadata_dir=project_dir
)
logger.info(f"步骤1完成: {result}")
return result
except Exception as e:
logger.error(f"步骤1失败: {str(e)}")
return {"status": "failed", "error": str(e)}
def _run_step2_timeline(self, project_id: str, project_dir: Path) -> Dict[str, Any]:
"""运行步骤2: 时间定位"""
try:
step1_output = project_dir / "step1_outline" / "step1_outline.json"
output_dir = project_dir / "step2_timeline"
output_dir.mkdir(parents=True, exist_ok=True)
result = run_step2_timeline(
outline_path=step1_output,
output_path=output_dir / "step2_timeline.json",
metadata_dir=project_dir
)
logger.info(f"步骤2完成: {result}")
return result
except Exception as e:
logger.error(f"步骤2失败: {str(e)}")
return {"status": "failed", "error": str(e)}
def _run_step3_scoring(self, project_id: str, project_dir: Path) -> Dict[str, Any]:
"""运行步骤3: 内容评分"""
try:
step2_output = project_dir / "step2_timeline" / "step2_timeline.json"
output_dir = project_dir / "step3_scoring"
output_dir.mkdir(parents=True, exist_ok=True)
result = run_step3_scoring(
timeline_path=step2_output,
output_path=output_dir / "step3_scoring.json",
metadata_dir=project_dir
)
logger.info(f"步骤3完成: {result}")
return result
except Exception as e:
logger.error(f"步骤3失败: {str(e)}")
return {"status": "failed", "error": str(e)}
def _run_step4_title(self, project_id: str, project_dir: Path) -> Dict[str, Any]:
"""运行步骤4: 标题生成"""
try:
step3_output = project_dir / "step3_scoring" / "step3_scoring.json"
output_dir = project_dir / "step4_title"
output_dir.mkdir(parents=True, exist_ok=True)
result = run_step4_title(
high_score_clips_path=step3_output,
output_path=output_dir / "step4_title.json",
metadata_dir=str(project_dir)
)
logger.info(f"步骤4完成: {result}")
return result
except Exception as e:
logger.error(f"步骤4失败: {str(e)}")
return {"status": "failed", "error": str(e)}
def _run_step5_clustering(self, project_id: str, project_dir: Path) -> Dict[str, Any]:
"""运行步骤5: 主题聚类"""
try:
step4_output = project_dir / "step4_title" / "step4_title.json"
output_dir = project_dir / "step5_clustering"
output_dir.mkdir(parents=True, exist_ok=True)
result = run_step5_clustering(
clips_with_titles_path=step4_output,
output_path=output_dir / "step5_clustering.json",
metadata_dir=str(project_dir)
)
logger.info(f"步骤5完成: {result}")
return result
except Exception as e:
logger.error(f"步骤5失败: {str(e)}")
return {"status": "failed", "error": str(e)}
def _run_step6_video(self, project_id: str, input_video_path: str, project_dir: Path) -> Dict[str, Any]:
"""运行步骤6: 视频切割"""
try:
step4_output = project_dir / "step4_title" / "step4_title.json"
step5_output = project_dir / "step5_clustering" / "step5_clustering.json"
output_dir = project_dir / "step6_video"
output_dir.mkdir(parents=True, exist_ok=True)
# 准备视频输出目录
clips_dir = self.output_dir / "clips"
collections_dir = self.output_dir / "collections"
clips_dir.mkdir(parents=True, exist_ok=True)
collections_dir.mkdir(parents=True, exist_ok=True)
result = run_step6_video(
clips_with_titles_path=step4_output,
collections_path=step5_output,
input_video=Path(input_video_path),
output_dir=output_dir,
clips_dir=str(clips_dir),
collections_dir=str(collections_dir),
metadata_dir=str(project_dir)
)
# 将数据保存到数据库
if result.get('clips_generated', 0) > 0:
self._save_clips_to_database(project_id, step4_output)
if result.get('collections_generated', 0) > 0:
self._save_collections_to_database(project_id, step5_output)
logger.info(f"步骤6完成: {result}")
return result
except Exception as e:
logger.error(f"步骤6失败: {str(e)}")
return {"status": "failed", "error": str(e)}
def _update_progress(self, project_id: str, progress: int, current_step: str):
"""更新处理进度"""
if self.progress_manager and self.task_id:
# 使用正确的方法名和参数
import asyncio
try:
# 计算当前步骤
step_mapping = {
"大纲提取": 1,
"时间定位": 2,
"内容评分": 3,
"标题生成": 4,
"主题聚类": 5,
"视频切割": 6
}
current_step_num = step_mapping.get(current_step, 1)
# 创建事件循环并运行异步方法
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(
self.progress_manager.update_task_progress(
task_id=self.task_id, # 不转换为int,保持UUID字符串
current_step=current_step_num,
total_steps=6,
step_name=current_step,
progress=progress
)
)
loop.close()
except Exception as e:
logger.warning(f"更新进度失败: {e}")
# 直接更新数据库中的任务
if self.task_id:
task = self.db.query(Task).filter(Task.id == self.task_id).first()
if task:
task.progress = progress
task.current_step = current_step
self.db.commit()
def _update_project_status(self, project_id: str, status: ProjectStatus):
"""更新项目状态"""
project = self.db.query(Project).filter(Project.id == project_id).first()
if project:
project.status = status
if status == ProjectStatus.COMPLETED:
project.completed_at = datetime.now()
self.db.commit()
def _save_clips_to_database(self, project_id: str, clips_file: Path):
"""将切片数据保存到数据库(分离存储模式)"""
try:
import json
from models.clip import Clip
from datetime import datetime
import uuid
# 确保存储服务可用
if not self.storage_service:
self.storage_service = StorageService(project_id)
# 读取切片数据
with open(clips_file, 'r', encoding='utf-8') as f:
clips_data = json.load(f)
# 保存到数据库(只保存元数据)
for clip_data in clips_data:
# 转换时间格式
start_time = self._convert_time_to_seconds(clip_data['start_time'])
end_time = self._convert_time_to_seconds(clip_data['end_time'])
duration = end_time - start_time
# 生成切片ID
clip_id = str(uuid.uuid4())
# 保存切片文件到文件系统
video_path = self.storage_service.save_clip_file(clip_data, clip_id)
# 保存完整数据到文件系统
metadata_path = self.storage_service.save_metadata(clip_data, f"clip_{clip_id}")
# 创建切片记录(只保存元数据和路径引用)
clip = Clip(
id=clip_id,
project_id=project_id,
title=clip_data.get('generated_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, # 只存储路径引用
clip_metadata={
'metadata_file': metadata_path, # 完整数据文件路径
'clip_id': clip_id,
'created_at': datetime.now().isoformat(),
# 添加完整的元数据字段
'recommend_reason': clip_data.get('recommend_reason', ''),
'outline': clip_data.get('outline', ''),
'content': clip_data.get('content', []),
'chunk_index': clip_data.get('chunk_index', 0),
'generated_title': clip_data.get('generated_title', '')
},
status='completed'
)
self.db.add(clip)
self.db.commit()
logger.info(f"成功保存 {len(clips_data)} 个切片(分离存储模式)")
except Exception as e:
logger.error(f"保存切片到数据库失败: {str(e)}")
self.db.rollback()
def _save_collections_to_database(self, project_id: str, collections_file: Path):
"""将合集数据保存到数据库(分离存储模式)"""
try:
import json
from models.collection import Collection
from datetime import datetime
import uuid
# 确保存储服务可用
if not self.storage_service:
self.storage_service = StorageService(project_id)
# 读取合集数据
with open(collections_file, 'r', encoding='utf-8') as f:
collections_data = json.load(f)
# 保存到数据库(只保存元数据)
for collection_data in collections_data:
# 生成合集ID
collection_id = str(uuid.uuid4())
# 保存合集文件到文件系统
export_path = self.storage_service.save_collection_file(collection_data, collection_id)
# 保存完整数据到文件系统
metadata_path = self.storage_service.save_metadata(collection_data, f"collection_{collection_id}")
# 创建合集记录(只保存元数据和路径引用)
collection = Collection(
id=collection_id,
project_id=project_id,
name=collection_data.get('collection_title', ''),
description=collection_data.get('collection_summary', ''),
export_path=str(self.output_dir / "collections" / f"{collection_data.get('collection_title', '')}.mp4"), # 使用正确的路径
collection_metadata={
'metadata_file': metadata_path, # 完整数据文件路径
'clip_ids': collection_data.get('clip_ids', []),
'collection_type': 'ai_recommended',
'collection_id': collection_id,
'created_at': datetime.now().isoformat(),
# 添加完整的元数据字段
'collection_title': collection_data.get('collection_title', ''),
'collection_summary': collection_data.get('collection_summary', '')
}
)
self.db.add(collection)
self.db.commit()
logger.info(f"成功保存 {len(collections_data)} 个合集(分离存储模式)")
except Exception as e:
logger.error(f"保存合集到数据库失败: {str(e)}")
self.db.rollback()
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
# 为ProcessingOrchestrator添加适配器方法
def adapt_step1_outline(self, srt_path: str) -> Dict[str, Any]:
"""适配步骤1参数"""
project_dir = self.data_dir / "projects" / self.project_id
output_dir = project_dir / "step1_outline"
output_dir.mkdir(parents=True, exist_ok=True)
return {
"srt_path": Path(srt_path),
"metadata_dir": project_dir,
"output_path": output_dir / "step1_outline.json"
}
def adapt_step2_timeline(self) -> Dict[str, Any]:
"""适配步骤2参数"""
project_dir = self.data_dir / "projects" / self.project_id
step1_output = project_dir / "step1_outline" / "step1_outline.json"
output_dir = project_dir / "step2_timeline"
output_dir.mkdir(parents=True, exist_ok=True)
return {
"outline_path": step1_output,
"output_path": output_dir / "step2_timeline.json",
"metadata_dir": project_dir
}
def adapt_step3_scoring(self) -> Dict[str, Any]:
"""适配步骤3参数"""
project_dir = self.data_dir / "projects" / self.project_id
step2_output = project_dir / "step2_timeline" / "step2_timeline.json"
output_dir = project_dir / "step3_scoring"
output_dir.mkdir(parents=True, exist_ok=True)
return {
"timeline_path": step2_output,
"output_path": output_dir / "step3_scoring.json",
"metadata_dir": project_dir
}
def adapt_step4_title(self) -> Dict[str, Any]:
"""适配步骤4参数"""
project_dir = self.data_dir / "projects" / self.project_id
step3_output = project_dir / "step3_scoring" / "step3_scoring.json"
output_dir = project_dir / "step4_title"
output_dir.mkdir(parents=True, exist_ok=True)
return {
"high_score_clips_path": step3_output,
"output_path": output_dir / "step4_title.json",
"metadata_dir": project_dir
}
def adapt_step5_clustering(self) -> Dict[str, Any]:
"""适配步骤5参数"""
project_dir = self.data_dir / "projects" / self.project_id
step4_output = project_dir / "step4_title" / "step4_title.json"
output_dir = project_dir / "step5_clustering"
output_dir.mkdir(parents=True, exist_ok=True)
return {
"clips_with_titles_path": step4_output,
"output_path": output_dir / "step5_clustering.json",
"metadata_dir": project_dir
}
def adapt_step6_video(self) -> Dict[str, Any]:
"""适配步骤6参数"""
project_dir = self.data_dir / "projects" / self.project_id
step5_output = project_dir / "step5_clustering" / "step5_clustering.json"
step4_output = project_dir / "step4_title" / "step4_title.json"
output_dir = project_dir / "step6_video"
output_dir.mkdir(parents=True, exist_ok=True)
# 准备视频输出目录
clips_dir = self.output_dir / "clips"
collections_dir = self.output_dir / "collections"
clips_dir.mkdir(parents=True, exist_ok=True)
collections_dir.mkdir(parents=True, exist_ok=True)
return {
"clips_with_titles_path": step4_output,
"collections_path": step5_output,
"input_video": project_dir / "raw" / "input.mp4",
"output_dir": output_dir,
"clips_dir": str(clips_dir),
"collections_dir": str(collections_dir),
"metadata_dir": str(project_dir)
}
def prepare_step_environment(self, step_name: str):
"""准备步骤执行环境"""
logger.info(f"准备步骤环境: {step_name}")
# 这里可以添加步骤特定的环境准备逻辑
pass
def validate_pipeline_prerequisites(self) -> List[str]:
"""
验证流水线前置条件
Returns:
错误信息列表,空列表表示验证通过
"""
errors = []
try:
# 检查项目目录是否存在
if not hasattr(self, 'project_id') or not self.project_id:
errors.append("项目ID未设置")
return errors
# 检查项目目录结构
project_dir = self.data_dir / "projects" / self.project_id
if not project_dir.exists():
errors.append(f"项目目录不存在: {project_dir}")
# 检查raw目录
raw_dir = project_dir / "raw"
if not raw_dir.exists():
errors.append(f"项目raw目录不存在: {raw_dir}")
# 检查视频文件
video_files = list(raw_dir.glob("*.mp4")) if raw_dir.exists() else []
if not video_files:
errors.append("未找到视频文件(.mp4)")
# 检查字幕文件
srt_files = list(raw_dir.glob("*.srt")) if raw_dir.exists() else []
if not srt_files:
errors.append("未找到字幕文件(.srt)")
# 检查输出目录权限
output_dir = self.output_dir
if not output_dir.exists():
try:
output_dir.mkdir(parents=True, exist_ok=True)
except Exception as e:
errors.append(f"无法创建输出目录: {e}")
# 检查API密钥配置
import os
import json
# 首先检查环境变量
api_key = os.getenv('DASHSCOPE_API_KEY') or os.getenv('OPENAI_API_KEY')
# 如果环境变量中没有,尝试从配置文件读取
if not api_key:
try:
settings_path = self.data_dir / "settings.json"
if settings_path.exists():
with open(settings_path, 'r', encoding='utf-8') as f:
settings = json.load(f)
api_key = settings.get('dashscope_api_key') or settings.get('openai_api_key')
except Exception as e:
logger.warning(f"读取配置文件失败: {e}")
if not api_key:
errors.append("未配置API密钥(DASHSCOPE_API_KEY或OPENAI_API_KEY)")
logger.info(f"流水线前置条件验证完成,发现 {len(errors)} 个错误")
return errors
except Exception as e:
logger.error(f"验证流水线前置条件时发生错误: {e}")
errors.append(f"验证过程发生错误: {e}")
return errors
def create_pipeline_adapter_sync(db: Session, task_id: Optional[str] = None, project_id: Optional[str] = None) -> PipelineAdapter:
"""创建同步Pipeline适配器"""
return PipelineAdapter(db, task_id, project_id)