diff --git a/apps/api/app/api/routes/templates_editor/_fallback.py b/apps/api/app/api/routes/templates_editor/_fallback.py index deafea778..1b7974780 100755 --- a/apps/api/app/api/routes/templates_editor/_fallback.py +++ b/apps/api/app/api/routes/templates_editor/_fallback.py @@ -161,43 +161,74 @@ def _auto_fallback_auto_material_mode( clips_without_asset: list, asset_library_repo: Any, asset_repo: Any, + user_id: str = "", ) -> None: - """自动兜底 4: 项目有视频素材库时自动选素材""" + """自动兜底 4: 自动选素材分配给无素材片段 + + 查找策略(按优先级): + 1. plan 有 project_id → 从项目素材库查找 + 2. plan 无 project_id 但有 user_id → 从用户上传的素材中查找 + """ if not clips_without_asset: return - if not plan_check.project_id: + + ready_videos: list = [] + source_desc = "" + + # 策略 1: 通过 project_id 查找项目素材库 + if plan_check.project_id: + libs = asset_library_repo.find_by_project(plan_check.project_id) + video_lib = None + for lib in libs: + lib_kind = lib.kind.value if hasattr(lib.kind, "value") else lib.kind + if lib_kind == "video": + video_lib = lib + break + if video_lib: + assets = asset_repo.find_by_library(video_lib.id) + ready_videos = [ + a + for a in assets + if (a.status.value if hasattr(a.status, "value") else a.status) == "ready" + and a.mime_type + and a.mime_type.startswith("video") + ] + source_desc = f"素材库 {video_lib.name}" + + # 策略 2: 通过 user_id 查找用户上传的素材 + if not ready_videos and user_id and hasattr(asset_repo, "find_ready_videos_by_user"): + logger.info( + "模板编辑器自动兜底4: plan=%s project_id 为空,尝试通过 user_id=%s 查找素材", + plan_id, + user_id, + ) + ready_videos = asset_repo.find_ready_videos_by_user(user_id) + source_desc = f"用户上传 (user_id={user_id[:8]}...)" + + if not ready_videos: + logger.warning( + "模板编辑器自动兜底4: plan=%s 未找到可用素材 (project_id=%s, user_id=%s)", + plan_id, + plan_check.project_id or "(empty)", + user_id[:8] + "..." if user_id else "(empty)", + ) return logger.info( - "模板编辑器自动兜底4: plan=%s 自动选素材分配给 %d 个无素材片段", + "模板编辑器自动兜底4: plan=%s 自动选素材分配给 %d 个无素材片段 (来源: %s, 共 %d 个)", plan_id, len(clips_without_asset), + source_desc, + len(ready_videos), + ) + random.shuffle(ready_videos) + for i, clip in enumerate(clips_without_asset): + asset = ready_videos[i % len(ready_videos)] + svc.assign_asset(clip.id, asset.id) + logger.info( + "模板编辑器自动兜底4: plan=%s 从 %s 分配了 %d 个素材给 %d 个片段", + plan_id, + source_desc, + len(ready_videos), + len(clips_without_asset), ) - libs = asset_library_repo.find_by_project(plan_check.project_id) - video_lib = None - for lib in libs: - lib_kind = lib.kind.value if hasattr(lib.kind, "value") else lib.kind - if lib_kind == "video": - video_lib = lib - break - - if video_lib: - assets = asset_repo.find_by_library(video_lib.id) - ready_videos = [ - a - for a in assets - if (a.status.value if hasattr(a.status, "value") else a.status) == "ready" - and a.mime_type - and a.mime_type.startswith("video") - ] - if ready_videos: - random.shuffle(ready_videos) - for i, clip in enumerate(clips_without_asset): - asset = ready_videos[i % len(ready_videos)] - svc.assign_asset(clip.id, asset.id) - logger.info( - "模板编辑器自动兜底4: plan=%s 从素材库 %s 分配了 %d 个素材", - plan_id, - video_lib.name, - len(ready_videos), - ) diff --git a/apps/api/app/api/routes/templates_editor/generation.py b/apps/api/app/api/routes/templates_editor/generation.py index c9bda4ce1..57d1aec14 100755 --- a/apps/api/app/api/routes/templates_editor/generation.py +++ b/apps/api/app/api/routes/templates_editor/generation.py @@ -71,7 +71,8 @@ def generate_editor_draft( _auto_fallback_copy_template_clips(plan_svc, plan_id, plan_check, db) clips_without_asset = _auto_fallback_assign_assets(plan_svc, plan_id, plan_check) _auto_fallback_auto_material_mode( - plan_svc, plan_id, plan_check, clips_without_asset, asset_library_repo, asset_repo + plan_svc, plan_id, plan_check, clips_without_asset, asset_library_repo, asset_repo, + user_id=str(current_user.user.id), ) # 检查是否可生成(含最后防线自动修复 + 诊断日志) diff --git a/packages/adapters/sqlalchemy_impl/asset_repository.py b/packages/adapters/sqlalchemy_impl/asset_repository.py index a5abe8bed..0c4077c14 100755 --- a/packages/adapters/sqlalchemy_impl/asset_repository.py +++ b/packages/adapters/sqlalchemy_impl/asset_repository.py @@ -284,6 +284,24 @@ class SQLAlchemyAssetRepository: ) return int(result or 0) + def find_ready_videos_by_user( + self, + user_id: str, + *, + limit: int = 50, + ) -> list[Asset]: + """查找用户上传的所有就绪视频素材。""" + query = self.session.query(AssetModel).filter( + AssetModel.uploaded_by_user_id == user_id, + AssetModel.status == "ready", + AssetModel.file_type == "video", + ) + query = query.order_by(AssetModel.created_at.desc()) + if limit > 0: + query = query.limit(limit) + models = query.all() + return [self._to_domain(m) for m in models] + def search_candidates( self, project_id: str,