fix(8502): 重生图片自动作废旧视频;记录来源图片签名并提示stale;组图/重生视频路径唯一化

This commit is contained in:
Tony Zhang
2025-12-17 22:54:22 +08:00
parent 1e210ffccf
commit c6436da17e
3 changed files with 113 additions and 2 deletions

43
app.py
View File

@@ -11,6 +11,7 @@ from pathlib import Path
import pandas as pd import pandas as pd
from time import perf_counter from time import perf_counter
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
from os import stat
# Import Backend Modules # Import Backend Modules
import config import config
@@ -704,6 +705,9 @@ if st.session_state.view_mode == "workspace":
for s_id, path in results.items(): for s_id, path in results.items():
st.session_state.scene_images[s_id] = path st.session_state.scene_images[s_id] = path
db.save_asset(st.session_state.project_id, s_id, "image", "completed", local_path=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): if len(results) == len(scenes):
st.success("组图生成完成!") st.success("组图生成完成!")
@@ -754,6 +758,9 @@ if st.session_state.view_mode == "workspace":
if img_path: if img_path:
st.session_state.scene_images[scene_id] = 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) 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) progress_bar.progress(done / total_scenes)
@@ -823,6 +830,9 @@ if st.session_state.view_mode == "workspace":
if new_path: if new_path:
st.session_state.scene_images[scene_id] = 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) 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() st.rerun()
if st.button("下一步:生成视频", type="primary"): if st.button("下一步:生成视频", type="primary"):
@@ -965,6 +975,22 @@ if st.session_state.view_mode == "workspace":
meta = (asset or {}).get("metadata") or {} meta = (asset or {}).get("metadata") or {}
video_url = meta.get("video_url") video_url = meta.get("video_url")
if 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.caption("URL 直连预览(不经服务器落盘)")
st.video(video_url) st.video(video_url)
else: else:
@@ -995,7 +1021,19 @@ if st.session_state.view_mode == "workspace":
for _ in range(60): for _ in range(60):
status, url = vid_gen.check_task_status(t_id) status, url = vid_gen.check_task_status(t_id)
if status == "succeeded": 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: if new_path:
st.session_state.scene_videos[scene_id] = 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) 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"): if st.button("🔄 重新生成所有视频", type="secondary"):
# Clear videos and rerun # Clear videos and rerun
st.session_state.scene_videos = {} 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() st.rerun()
with c_act2: with c_act2:

View File

@@ -308,6 +308,59 @@ class DBManager:
finally: finally:
session.close() 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 --- # --- Config/Prompt Operations ---
def get_config(self, key: str, default: Any = None) -> Any: def get_config(self, key: str, default: Any = None) -> Any:

View File

@@ -6,6 +6,7 @@ import logging
import time import time
import requests import requests
import os import os
from os import stat
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
from pathlib import Path from pathlib import Path
@@ -56,6 +57,21 @@ class VideoGenerator:
task_id = self._submit_task(image_url, prompt) task_id = self._submit_task(image_url, prompt)
if task_id: 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 # 立即保存 task_id 到数据库,状态为 processing
db.save_asset( db.save_asset(
project_id=project_id, project_id=project_id,
@@ -63,7 +79,8 @@ class VideoGenerator:
asset_type="video", asset_type="video",
status="processing", status="processing",
task_id=task_id, task_id=task_id,
local_path=None local_path=None,
metadata=source_sig,
) )
return task_id return task_id