From c6436da17e98bff451def279ea1927d78e7d82bb Mon Sep 17 00:00:00 2001 From: Tony Zhang Date: Wed, 17 Dec 2025 22:54:22 +0800 Subject: [PATCH] =?UTF-8?q?fix(8502):=20=E9=87=8D=E7=94=9F=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E8=87=AA=E5=8A=A8=E4=BD=9C=E5=BA=9F=E6=97=A7=E8=A7=86?= =?UTF-8?q?=E9=A2=91=EF=BC=9B=E8=AE=B0=E5=BD=95=E6=9D=A5=E6=BA=90=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E7=AD=BE=E5=90=8D=E5=B9=B6=E6=8F=90=E7=A4=BAstale?= =?UTF-8?q?=EF=BC=9B=E7=BB=84=E5=9B=BE/=E9=87=8D=E7=94=9F=E8=A7=86?= =?UTF-8?q?=E9=A2=91=E8=B7=AF=E5=BE=84=E5=94=AF=E4=B8=80=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 43 ++++++++++++++++++++++++++++++++++- modules/db_manager.py | 53 +++++++++++++++++++++++++++++++++++++++++++ modules/video_gen.py | 19 +++++++++++++++- 3 files changed, 113 insertions(+), 2 deletions(-) diff --git a/app.py b/app.py index d2c0ecf..9e06d10 100644 --- a/app.py +++ b/app.py @@ -11,6 +11,7 @@ from pathlib import Path import pandas as pd from time import perf_counter from concurrent.futures import ThreadPoolExecutor, as_completed +from os import stat # Import Backend Modules import config @@ -704,6 +705,9 @@ if st.session_state.view_mode == "workspace": for s_id, path in results.items(): st.session_state.scene_images[s_id] = path db.save_asset(st.session_state.project_id, s_id, "image", "completed", local_path=path) + # Invalidate stale video for this scene (group image regen also changes image) + db.clear_asset(st.session_state.project_id, s_id, "video", status="pending") + st.session_state.scene_videos.pop(s_id, None) if len(results) == len(scenes): st.success("组图生成完成!") @@ -754,6 +758,9 @@ if st.session_state.view_mode == "workspace": if img_path: st.session_state.scene_images[scene_id] = img_path db.save_asset(st.session_state.project_id, scene_id, "image", "completed", local_path=img_path) + # Invalidate stale video for this scene (image changed => old video is wrong) + db.clear_asset(st.session_state.project_id, scene_id, "video", status="pending") + st.session_state.scene_videos.pop(scene_id, None) progress_bar.progress(done / total_scenes) @@ -823,6 +830,9 @@ if st.session_state.view_mode == "workspace": if new_path: st.session_state.scene_images[scene_id] = new_path db.save_asset(st.session_state.project_id, scene_id, "image", "completed", local_path=new_path) + # Invalidate stale video for this scene + db.clear_asset(st.session_state.project_id, scene_id, "video", status="pending") + st.session_state.scene_videos.pop(scene_id, None) st.rerun() if st.button("下一步:生成视频", type="primary"): @@ -965,6 +975,22 @@ if st.session_state.view_mode == "workspace": meta = (asset or {}).get("metadata") or {} video_url = meta.get("video_url") if video_url: + # Detect stale mapping: if source image signature differs, warn and avoid misleading preview + stale = False + try: + cur_img = st.session_state.scene_images.get(scene_id) + if cur_img and os.path.exists(cur_img): + st_img = stat(cur_img) + cur_size = int(getattr(st_img, "st_size", 0) or 0) + cur_mtime = float(getattr(st_img, "st_mtime", 0.0) or 0.0) + src_size = meta.get("source_image_size") + src_mtime = meta.get("source_image_mtime") + if (src_size and cur_size and int(src_size) != cur_size) or (src_mtime and cur_mtime and abs(float(src_mtime) - cur_mtime) > 1e-3): + stale = True + except Exception: + stale = False + if stale: + st.warning("检测到该视频可能基于旧图片生成(图片已更新)。请点击“提交图生视频任务”重新生成,以避免主体不一致。") st.caption("URL 直连预览(不经服务器落盘)") st.video(video_url) else: @@ -995,7 +1021,19 @@ if st.session_state.view_mode == "workspace": for _ in range(60): status, url = vid_gen.check_task_status(t_id) if status == "succeeded": - new_path = vid_gen._download_video(url, f"scene_{scene_id}_video_{int(time.time())}.mp4") + # Use per-project unique name to avoid cross-project overwrite + out_name = path_utils.unique_filename( + prefix="scene_video", + ext="mp4", + project_id=st.session_state.project_id, + scene_id=scene_id, + extra=(t_id[-8:] if isinstance(t_id, str) else None), + ) + new_path = vid_gen._download_video( + url, + out_name, + output_dir=path_utils.project_videos_dir(st.session_state.project_id), + ) if new_path: st.session_state.scene_videos[scene_id] = new_path db.save_asset(st.session_state.project_id, scene_id, "video", "completed", local_path=new_path, task_id=t_id) @@ -1015,6 +1053,9 @@ if st.session_state.view_mode == "workspace": if st.button("🔄 重新生成所有视频", type="secondary"): # Clear videos and rerun st.session_state.scene_videos = {} + # Also clear DB video assets to avoid stale URL preview + if st.session_state.project_id: + db.clear_assets(st.session_state.project_id, "video", status="pending") st.rerun() with c_act2: diff --git a/modules/db_manager.py b/modules/db_manager.py index dcf7b97..2c43804 100644 --- a/modules/db_manager.py +++ b/modules/db_manager.py @@ -308,6 +308,59 @@ class DBManager: finally: session.close() + def clear_asset(self, project_id: str, scene_id: int, asset_type: str, *, status: str = "pending") -> None: + """ + Clear an asset record (keep the row, but remove paths/task/metadata). + Used to invalidate stale videos after images are regenerated. + """ + session = self._get_session() + try: + asset = session.query(SceneAsset).filter_by( + project_id=project_id, + scene_id=scene_id, + asset_type=asset_type, + ).first() + if not asset: + return + asset.status = status + asset.local_path = None + asset.remote_url = None + asset.task_id = None + asset.metadata_json = "{}" + asset.updated_at = time.time() + session.commit() + except Exception as e: + session.rollback() + logger.error(f"Error clearing asset: {e}") + finally: + session.close() + + def clear_assets(self, project_id: str, asset_type: str, *, status: str = "pending") -> int: + """ + Clear all assets of a type for a project. Returns number of rows affected. + """ + session = self._get_session() + try: + q = session.query(SceneAsset).filter_by(project_id=project_id, asset_type=asset_type) + rows = q.all() + if not rows: + return 0 + for asset in rows: + asset.status = status + asset.local_path = None + asset.remote_url = None + asset.task_id = None + asset.metadata_json = "{}" + asset.updated_at = time.time() + session.commit() + return len(rows) + except Exception as e: + session.rollback() + logger.error(f"Error clearing assets: {e}") + return 0 + finally: + session.close() + # --- Config/Prompt Operations --- def get_config(self, key: str, default: Any = None) -> Any: diff --git a/modules/video_gen.py b/modules/video_gen.py index 7f725cb..fb58d5d 100644 --- a/modules/video_gen.py +++ b/modules/video_gen.py @@ -6,6 +6,7 @@ import logging import time import requests import os +from os import stat from typing import Dict, Any, List, Optional from pathlib import Path @@ -56,6 +57,21 @@ class VideoGenerator: task_id = self._submit_task(image_url, prompt) if task_id: + try: + st = stat(image_path) + source_sig = { + "source_image_local_path": image_path, + "source_image_size": int(getattr(st, "st_size", 0) or 0), + "source_image_mtime": float(getattr(st, "st_mtime", 0.0) or 0.0), + "source_image_r2_url": image_url, + "submitted_at": time.time(), + } + except Exception: + source_sig = { + "source_image_local_path": image_path, + "source_image_r2_url": image_url, + "submitted_at": time.time(), + } # 立即保存 task_id 到数据库,状态为 processing db.save_asset( project_id=project_id, @@ -63,7 +79,8 @@ class VideoGenerator: asset_type="video", status="processing", task_id=task_id, - local_path=None + local_path=None, + metadata=source_sig, ) return task_id