Fix S3 streaming upload + detect white/gray watermarks
Docker Build and Push / build-and-push (cuda, 11.8) (push) Has been skipped
Docker Build and Push / build-and-push (cuda, 12.6) (push) Has been skipped
Docker Build and Push / build-and-push (cuda, 12.8) (push) Has been skipped
Docker Build and Push / check-secrets (push) Successful in 4s
Docker Build and Push / build-and-push (cpu, latest) (push) Has been skipped
Docker Build and Push / build-and-push (directml, latest) (push) Has been skipped
Build Windows CUDA 11.8 / build (push) Has been cancelled
Build Windows CUDA 12.6 / build (push) Has been cancelled
Build Windows CUDA 12.8 / build (push) Has been cancelled
Build Windows DirectML / build (push) Has been cancelled
Build Windows CPU / build (push) Has been cancelled
Docker Build and Push / build-and-push (cuda, 11.8) (push) Has been skipped
Docker Build and Push / build-and-push (cuda, 12.6) (push) Has been skipped
Docker Build and Push / build-and-push (cuda, 12.8) (push) Has been skipped
Docker Build and Push / check-secrets (push) Successful in 4s
Docker Build and Push / build-and-push (cpu, latest) (push) Has been skipped
Docker Build and Push / build-and-push (directml, latest) (push) Has been skipped
Build Windows CUDA 11.8 / build (push) Has been cancelled
Build Windows CUDA 12.6 / build (push) Has been cancelled
Build Windows CUDA 12.8 / build (push) Has been cancelled
Build Windows DirectML / build (push) Has been cancelled
Build Windows CPU / build (push) Has been cancelled
S3 upload: - Replace read_bytes() (loads entire file into memory) with streaming SHA256 computation (8MB chunks) + httpx streaming PUT with Content-Length header - Add detailed logging for each upload step to diagnose future failures Watermark detection (smart_detect.py): - Add Path B: brightness-diff detection for low-saturation watermarks like white/gray text (e.g. 'sjhs.pw'). Compares mean brightness in first 10s vs the rest of the video. Regions that are brighter in early frames AND located in frame corners (top-left, top-right, etc.) are flagged. - Merge Path A (saturation-based) + Path B (brightness-diff) results - Fall back to global std method only if both paths find nothing Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 4.5
parent
8b583a4b0e
commit
fabde9cf83
@@ -76,51 +76,58 @@ def detect_static_regions(video_path: str, sample_frames: int = 30, threshold: f
|
||||
# 分析时使用小尺寸坐标空间
|
||||
w, h = SW, SH
|
||||
|
||||
if len(logo_gray_frames) < 2:
|
||||
print(f"[SAM2-detect] logo帧不足,回退到时序std法", flush=True)
|
||||
# ── 路径A:彩色水印(高饱和度)检测 ──────────────────────────────────
|
||||
sat_mask = np.zeros((h, w), dtype=np.uint8)
|
||||
if len(logo_gray_frames) >= 2 and nlogo_bgr_frames:
|
||||
sat_acc = np.zeros((h, w), dtype=np.float32)
|
||||
for bgr in logo_bgr_frames:
|
||||
sat_acc += cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV)[:, :, 1].astype(np.float32) / 255.0
|
||||
sat_mean_logo = sat_acc / len(logo_bgr_frames)
|
||||
|
||||
sat_acc_nl = np.zeros((h, w), dtype=np.float32)
|
||||
for bgr in nlogo_bgr_frames[:6]:
|
||||
sat_acc_nl += cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV)[:, :, 1].astype(np.float32) / 255.0
|
||||
sat_mean_nlogo = sat_acc_nl / min(len(nlogo_bgr_frames), 6)
|
||||
sat_diff = sat_mean_logo - sat_mean_nlogo
|
||||
|
||||
sat_thresh = min(0.6, np.percentile(sat_mean_logo, 92))
|
||||
diff_thresh = max(0.25, sat_diff.mean() + sat_diff.std())
|
||||
sat_mask = ((sat_mean_logo >= sat_thresh) & (sat_diff >= diff_thresh)).astype(np.uint8) * 255
|
||||
print(f"[SAM2-detect] 彩色路径: sat_thresh={sat_thresh:.3f} diff_thresh={diff_thresh:.3f} 候选={np.sum(sat_mask>0)}", flush=True)
|
||||
|
||||
# ── 路径B:白色/灰色水印(亮度差分)检测 ────────────────────────────────
|
||||
# "sjhs.pw" 类白色文字水印:饱和度≈0,但亮度比背景高,且只出现在少数帧
|
||||
# 策略:把前 N 秒的帧与后续帧做亮度差分,找只出现在片头的高亮区域
|
||||
bright_mask = np.zeros((h, w), dtype=np.uint8)
|
||||
early_cutoff = min(len(gray_frames), max(5, int(10 * fps / step))) # 前10秒
|
||||
if early_cutoff >= 2 and len(gray_frames) > early_cutoff:
|
||||
early_stack = np.stack(gray_frames[:early_cutoff], axis=0)
|
||||
rest_stack = np.stack(gray_frames[early_cutoff:], axis=0)
|
||||
early_mean = early_stack.mean(axis=0)
|
||||
rest_mean = rest_stack.mean(axis=0)
|
||||
# 片头比后续亮得多 → 水印像素(值越大越亮)
|
||||
bright_diff = early_mean - rest_mean
|
||||
# 阈值:差值超过 20 灰度级的区域
|
||||
bright_mask_raw = (bright_diff > 20).astype(np.uint8) * 255
|
||||
# 只保留四角区域(水印通常在角落),避免误报视频内容
|
||||
corner_mask = np.zeros((h, w), dtype=np.uint8)
|
||||
ch, cw = h // 4, w // 4 # 角落占画面 25%
|
||||
corner_mask[:ch, :cw] = 1 # 左上
|
||||
corner_mask[:ch, -cw:] = 1 # 右上
|
||||
corner_mask[-ch:, :cw] = 1 # 左下
|
||||
corner_mask[-ch:, -cw:] = 1 # 右下
|
||||
bright_mask = cv2.bitwise_and(bright_mask_raw, corner_mask * 255)
|
||||
print(f"[SAM2-detect] 亮度差分路径: bright_diff_max={bright_diff.max():.1f} 角落候选={np.sum(bright_mask>0)}", flush=True)
|
||||
|
||||
# ── 合并两条路径 ──────────────────────────────────────────────────────
|
||||
static_mask = cv2.bitwise_or(sat_mask, bright_mask)
|
||||
if np.sum(static_mask) == 0:
|
||||
# 两条路径都没找到,回退到全局时序std法
|
||||
print(f"[SAM2-detect] 两条路径均无结果,回退到时序std法", flush=True)
|
||||
stack = np.stack(gray_frames, axis=0)
|
||||
std_map = np.std(stack, axis=0)
|
||||
static_mask = (std_map < 8.0).astype(np.uint8) * 255
|
||||
else:
|
||||
# 在logo帧中统计每像素平均饱和度
|
||||
sat_acc = np.zeros((h, w), dtype=np.float32)
|
||||
for bgr in logo_bgr_frames:
|
||||
hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV)
|
||||
sat_acc += (hsv[:, :, 1].astype(np.float32) / 255.0)
|
||||
sat_mean_logo = sat_acc / len(logo_bgr_frames)
|
||||
|
||||
if nlogo_bgr_frames:
|
||||
# ── 有无logo帧对比:准确检测logo区域 ───────────────────────────
|
||||
# Logo像素: logo帧高饱和 + 无logo帧低饱和(差值大)
|
||||
sat_acc_nl = np.zeros((h, w), dtype=np.float32)
|
||||
for bgr in nlogo_bgr_frames[:6]:
|
||||
hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV)
|
||||
sat_acc_nl += (hsv[:, :, 1].astype(np.float32) / 255.0)
|
||||
sat_mean_nlogo = sat_acc_nl / min(len(nlogo_bgr_frames), 6)
|
||||
sat_diff = sat_mean_logo - sat_mean_nlogo
|
||||
|
||||
sat_thresh = min(0.6, np.percentile(sat_mean_logo, 92))
|
||||
diff_thresh = max(0.25, sat_diff.mean() + sat_diff.std())
|
||||
logo_pixel_mask = ((sat_mean_logo >= sat_thresh) & (sat_diff >= diff_thresh)).astype(np.uint8) * 255
|
||||
|
||||
print(f"[SAM2-detect] logo帧平均饱和: max={sat_mean_logo.max():.3f}", flush=True)
|
||||
print(f"[SAM2-detect] 饱和差值: max={sat_diff.max():.3f} mean={sat_diff.mean():.3f}", flush=True)
|
||||
print(f"[SAM2-detect] sat_thresh={sat_thresh:.3f} diff_thresh={diff_thresh:.3f}, 候选={np.sum(logo_pixel_mask > 0)}", flush=True)
|
||||
else:
|
||||
# ── 无非logo帧(短视频/预览:logo全程可见)──────────────────────
|
||||
# 只在帧的边缘区域(顶部30%)寻找高饱和logo
|
||||
# 避免把视频中央的自然内容误报为logo
|
||||
border_mask = np.zeros((h, w), dtype=np.uint8)
|
||||
border_mask[:h * 3 // 10, :] = 1 # 顶部 30%
|
||||
|
||||
# 取最高饱和的4%像素(极鲜艳 = logo文字/图形)
|
||||
sat_thresh = max(0.6, np.percentile(sat_mean_logo, 96))
|
||||
logo_pixel_mask = ((sat_mean_logo >= sat_thresh).astype(np.uint8) * border_mask * 255)
|
||||
|
||||
print(f"[SAM2-detect] 无非logo帧,限定边缘区域搜索", flush=True)
|
||||
print(f"[SAM2-detect] sat_thresh={sat_thresh:.3f}, 候选={np.sum(logo_pixel_mask > 0)}", flush=True)
|
||||
|
||||
static_mask = logo_pixel_mask
|
||||
print(f"[SAM2-detect] 合并后候选像素数={np.sum(static_mask>0)}", flush=True)
|
||||
|
||||
# 仅排除纯黑区域
|
||||
mean_gray = np.mean(np.stack(gray_frames, axis=0), axis=0)
|
||||
|
||||
+32
-9
@@ -834,17 +834,26 @@ class VSRTaskWorker:
|
||||
logger.info(f"[vsr_worker] 任务 #{platform_task_id} 上报结果 status={status}")
|
||||
|
||||
async def _upload_result_video(self, local_path: str) -> str | None:
|
||||
"""处理完成后将输出视频上传到平台,返回 play_url(S3 地址)。"""
|
||||
"""处理完成后将输出视频上传到平台,返回 play_url(S3 地址)。流式上传,不一次性读入内存。"""
|
||||
import hashlib as _hl
|
||||
try:
|
||||
data = Path(local_path).read_bytes()
|
||||
sha256 = _hl.sha256(data).hexdigest()
|
||||
filename = Path(local_path).name
|
||||
file_path = Path(local_path)
|
||||
file_size = file_path.stat().st_size
|
||||
filename = file_path.name
|
||||
|
||||
# 流式计算 SHA256,避免 read_bytes() 把几百 MB 全加载进内存
|
||||
h = _hl.sha256()
|
||||
with open(local_path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(8 * 1024 * 1024), b""):
|
||||
h.update(chunk)
|
||||
sha256 = h.hexdigest()
|
||||
logger.info(f"[vsr_worker] 文件 {filename} size={file_size//1048576}MB sha256={sha256[:12]}…")
|
||||
|
||||
sign_resp = await self._signed_post(
|
||||
"/openapi/file/upload/get_upload_sign",
|
||||
{
|
||||
"sha256": sha256,
|
||||
"size": len(data),
|
||||
"size": file_size,
|
||||
"filename": filename,
|
||||
"content_type": "video/mp4",
|
||||
"file_type": "video",
|
||||
@@ -853,24 +862,36 @@ class VSRTaskWorker:
|
||||
},
|
||||
)
|
||||
if sign_resp is None:
|
||||
logger.warning("[vsr_worker] get_upload_sign 返回空,跳过上传")
|
||||
return None
|
||||
action = sign_resp.get("action", "")
|
||||
if action == "skip":
|
||||
logger.info(f"[vsr_worker] 文件已存在,直接返回 play_url")
|
||||
return sign_resp.get("play_url")
|
||||
presigned_url = sign_resp.get("presigned_url", "")
|
||||
object_key = sign_resp.get("object_key", "")
|
||||
if not presigned_url or not object_key:
|
||||
logger.warning(f"[vsr_worker] upload sign 未返回 presigned_url/object_key: {sign_resp}")
|
||||
return None
|
||||
put_resp = await self._client.put(presigned_url, content=data,
|
||||
headers={"Content-Type": "video/mp4"})
|
||||
|
||||
# 流式 PUT 上传,不把文件加载到内存
|
||||
logger.info(f"[vsr_worker] 开始流式上传 {file_size//1048576}MB → S3…")
|
||||
async with httpx.AsyncClient(timeout=7200) as upload_client:
|
||||
with open(local_path, "rb") as f:
|
||||
put_resp = await upload_client.put(
|
||||
presigned_url,
|
||||
content=f,
|
||||
headers={"Content-Type": "video/mp4", "Content-Length": str(file_size)},
|
||||
)
|
||||
put_resp.raise_for_status()
|
||||
logger.info(f"[vsr_worker] S3 上传完成,注册资产…")
|
||||
|
||||
reg_resp = await self._signed_post(
|
||||
"/openapi/file/assets/register",
|
||||
{
|
||||
"sha256": sha256,
|
||||
"object_key": object_key,
|
||||
"size": len(data),
|
||||
"size": file_size,
|
||||
"filename": filename,
|
||||
"content_type": "video/mp4",
|
||||
"file_type": "video",
|
||||
@@ -878,7 +899,9 @@ class VSRTaskWorker:
|
||||
"no_transcode": True,
|
||||
},
|
||||
)
|
||||
return reg_resp.get("play_url") if reg_resp else None
|
||||
play_url = reg_resp.get("play_url") if reg_resp else None
|
||||
logger.info(f"[vsr_worker] 资产注册完成 play_url={play_url}")
|
||||
return play_url
|
||||
except Exception as e:
|
||||
logger.warning(f"[vsr_worker] 上传结果视频失败: {e}")
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user