31 Commits
Author SHA1 Message Date
liuchenandClaude Sonnet 4.6 a1eb5bb28e Fix watermark detection + S3 upload + NVENC color + task download on S3 failure
Docker Build and Push / check-secrets (push) Successful in 11s
Docker Build and Push / build-and-push (cpu, latest) (push) Has been skipped
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 / build-and-push (directml, latest) (push) Has been skipped
Build Windows CPU / build (push) Has been cancelled
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
- smart_detect.py: rewrite detect_static_regions using saturation-signature method
  (high-sat logo frames vs no-logo frames diff) to handle animated/periodic watermarks
  that break temporal-std approach; merge top-region boxes; add print debugging
- web_app.py: fix S3 upload (async chunked streaming, error visible in task log);
  s3_failed flag keeps task downloadable even when S3 upload fails; add s3_failed
  column to DB; download/stream endpoints allow s3_failed tasks
- web/index.html: show play/download buttons for s3_failed tasks; task sort by
  created_at desc; watermark mode label update; isLocalDone() helper
- video_io.py: add -vf format=yuv420p before h264_nvenc to fix green video

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-06-28 00:20:44 +08:00
liuchenandClaude Opus 4.8 c65ec20119 Rework watermark detection + remove STTN modes
Docker Build and Push / check-secrets (push) Successful in 5s
Docker Build and Push / build-and-push (directml, latest) (push) Has been skipped
Docker Build and Push / build-and-push (cuda, 12.8) (push) Has been skipped
Docker Build and Push / build-and-push (cpu, latest) (push) Has been skipped
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
Build Windows CUDA 12.8 / build (push) Has been cancelled
Build Windows DirectML / build (push) Has been cancelled
Build Windows CUDA 11.8 / build (push) Has been cancelled
Build Windows CPU / build (push) Has been cancelled
Build Windows CUDA 12.6 / build (push) Has been cancelled
Detection (smart_detect.py) — two complementary paths, both feed per-frame masks:
1. Faint/semi-transparent & scrolling text (e.g. "sjhs.pw"):
   - per-frame white top-hat extracts thin bright strokes (OCR can't see these)
   - connected-component count (>=3) keeps text, rejects solid bright objects
   - track-linking across frames keeps stable-width persistent tracks,
     drops erratic body/skin highlights
2. Colorful opaque logos (e.g. "黑料网 heiliao.com"):
   - high-saturation (>150) AND low temporal-variance → static logo,
     rejects moving skin and muted background art
   - corner-snapping extends box to frame edge for full logo coverage

Key fixes:
- per-frame dict uses 1-based frame numbers to match the inpaint pipeline
  (was 0-based → masks never applied → nothing removed)
- persistent watermarks require LaMa spatial inpaint; STTN temporal inpaint
  has the watermark in its own reference frames so it can't remove them

Remove STTN modes entirely:
- INPAINT_MODE_MAP drops sttn-auto/sttn-det; unknown modes fall back to LaMa
- frontend algorithm selector: LaMa (default) / ProPainter / OpenCV
- platform-task & TaskCreate default inpaint_mode = lama

Validated: both "sjhs.pw" (scrolling text) and "黑料网" (color logo)
fully removed on preview with LaMa, no content damage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 17:59:41 +08:00
liuchenandClaude Haiku 4.5 fabde9cf83 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
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>
2026-06-27 17:06:18 +08:00
liuchenandClaude Haiku 4.5 8b583a4b0e Fix smart_detect: correct pos index, remove O(n²) loop, clean up
Docker Build and Push / build-and-push (cpu, latest) (push) Has been skipped
Docker Build and Push / build-and-push (cuda, 11.8) (push) Has been skipped
Docker Build and Push / check-secrets (push) Successful in 5s
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 / build-and-push (directml, latest) (push) Has been skipped
Build Windows CPU / build (push) Has been cancelled
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
- actual_pos_s[] tracks real timestamps per frame (was using positions[i]
  which breaks when frames fail to read)
- Replace O(n²) np.array_equal logo frame matching with simple bool list
- Remove unused pos_set variable
- Fix docstring still mentioning '8s tail' after removing it
- All variables consistent: logo_gray_frames replaces logo_frames_gray

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-27 16:16:48 +08:00
liuchenandClaude Haiku 4.5 5cdc727207 SAM2: sample every second for the entire video, no exceptions
Docker Build and Push / check-secrets (push) Successful in 9s
Docker Build and Push / build-and-push (cpu, latest) (push) Has been skipped
Docker Build and Push / build-and-push (cuda, 12.8) (push) Has been skipped
Docker Build and Push / build-and-push (directml, latest) (push) Has been skipped
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
Build Windows CPU / build (push) Has been cancelled
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
User requirement: correctness first, slow is acceptable.
- Remove the 120s dense + 8s tail split
- Full video sampled at 1 frame/second uniformly
- Analysis still uses 1/4 resolution for speed
- Coordinate scale restored to original resolution at the end

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-27 16:14:53 +08:00
liuchenandClaude Haiku 4.5 9094b3c888 Fix SAM2 missing early watermarks: always sample first 15 seconds
Docker Build and Push / build-and-push (cuda, 12.6) (push) Has been skipped
Docker Build and Push / check-secrets (push) Failing after 3s
Docker Build and Push / build-and-push (cpu, latest) (push) Has been skipped
Docker Build and Push / build-and-push (cuda, 11.8) (push) Has been skipped
Docker Build and Push / build-and-push (cuda, 12.8) (push) Has been skipped
Docker Build and Push / build-and-push (directml, latest) (push) Has been skipped
Build Windows CPU / build (push) Has been cancelled
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
For long videos (e.g. 1100s), uniform sampling started at ~23s, completely
missing watermarks that appear at 1s, 2s, 4s. Fix: always include dense
samples at 0.5, 1, 1.5, 2, 3, 4, 5, 7, 10, 15 seconds before the uniform
distribution, then deduplicate. Watermarks in the first 15s are now caught.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-27 16:11:29 +08:00
liuchenandClaude Haiku 4.5 b772ab3def Add HH:MM:SS timestamp to each log line in detail and log modals
Docker Build and Push / build-and-push (cuda, 12.6) (push) Has been skipped
Docker Build and Push / build-and-push (cuda, 11.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
Docker Build and Push / build-and-push (cuda, 12.8) (push) Has been skipped
Build Windows CPU / build (push) Has been cancelled
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
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-27 15:54:32 +08:00
liuchenandClaude Haiku 4.5 f21f844f0f Fix NVENC transcode: use system ffmpeg instead of bundled one
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 (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 / build-and-push (directml, latest) (push) Has been skipped
Build Windows CPU / build (push) Has been cancelled
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
Bundled ffmpeg at backend/ffmpeg/linux_x64/ffmpeg has no NVENC support.
System /usr/bin/ffmpeg does. Switch _ensure_h264 to use shutil.which('ffmpeg')
which resolves to /usr/bin/ffmpeg. Also log the actual NVENC error message
so future failures are visible in the task logs.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-27 15:52:21 +08:00
liuchenandClaude Haiku 4.5 0b2a3ec6e3 Auto-transcode AV1/HEVC videos to H.264 before processing
Docker Build and Push / check-secrets (push) Successful in 5s
Docker Build and Push / build-and-push (cuda, 12.8) (push) Has been skipped
Docker Build and Push / build-and-push (directml, latest) (push) Has been skipped
Docker Build and Push / build-and-push (cuda, 11.8) (push) Has been skipped
Docker Build and Push / build-and-push (cpu, latest) (push) Has been skipped
Docker Build and Push / build-and-push (cuda, 12.6) (push) Has been skipped
Build Windows CPU / build (push) Has been cancelled
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
Root cause: FramePrefetcher._read_loop exits after cap.read() returns False
(AV1 decode failure), putting only one (False, None) sentinel in the queue.
If the caller reads past that, queue.get() blocks forever → deadlock.

Fix: _ensure_h264() checks if OpenCV can read the first frame. If not,
runs FFmpeg to transcode to H.264 NVENC (falls back to libx264 if NVENC
fails). Uses cached converted file on repeated attempts. Processing then
runs on the H.264 version which OpenCV can decode normally.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-27 15:48:12 +08:00
liuchenandClaude Haiku 4.5 8dc1276a7c Show animated pulse bar when processing but progress still 0%
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 (cuda, 11.8) (push) Has been skipped
Docker Build and Push / build-and-push (cuda, 12.8) (push) Has been skipped
Docker Build and Push / build-and-push (directml, latest) (push) Has been skipped
Docker Build and Push / build-and-push (cuda, 12.6) (push) Has been skipped
Build Windows CPU / build (push) Has been cancelled
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
For large videos (e.g. 27508 frames), the first 275 frames only amount
to ~1% which rounds to 0 in integer percentage — so detect_pct stays 0
for a while even though the task is actively running. Instead of showing
static zero bars, display a pulsing indeterminate bar with an explanation
message so the user knows the task is alive.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-27 15:42:20 +08:00
liuchenandClaude Haiku 4.5 b4b9837dde Fix platform worker task not appearing in list
Docker Build and Push / build-and-push (cuda, 11.8) (push) Has been skipped
Docker Build and Push / check-secrets (push) Successful in 5s
Docker Build and Push / build-and-push (cpu, latest) (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 / build-and-push (directml, latest) (push) Has been skipped
Build Windows CPU / build (push) Has been cancelled
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
Two root causes:
1. Task was created in memory but not broadcast to frontend until the first
   update_task() call. If an exception happened before that, the task was
   invisible forever. Fix: broadcast immediately after task creation.
2. run_in_executor had no try/except — any exception was silently swallowed
   by asyncio.create_task with no log. Fix: wrap in try/except/finally,
   log exception via logger.exception, mark task as error.
Also: get_event_loop() → get_running_loop() (correct API for async context).

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-27 15:30:52 +08:00
liuchenandClaude Haiku 4.5 b81f6afd0f Fix detail modal progress bars not updating
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 CPU / build (push) Has been cancelled
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
Split openDetail (full render once) from renderDetail (partial update only).
- openDetail: renders static content + placeholder divs, loads logs once
- renderDetail (called on task_update): only patches status badge, phase bars,
  duration, error, action buttons — does NOT replace innerHTML or reload logs
This eliminates the flickering and actually makes progress bars respond to
WebSocket updates.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-27 15:21:35 +08:00
liuchenandClaude Haiku 4.5 da119826f5 Reduce sidebar width by 25% (264px → 198px)
Docker Build and Push / check-secrets (push) Failing after 3s
Docker Build and Push / build-and-push (cpu, latest) (push) Has been skipped
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 / build-and-push (directml, latest) (push) Has been skipped
Build Windows CPU / build (push) Has been cancelled
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
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-27 15:13:54 +08:00
liuchenandClaude Haiku 4.5 ea1784a10f Fix detail modal: live progress + logs from DB
Docker Build and Push / check-secrets (push) Failing after 4s
Docker Build and Push / build-and-push (cpu, latest) (push) Has been skipped
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 (directml, latest) (push) Has been skipped
Docker Build and Push / build-and-push (cuda, 12.8) (push) Has been skipped
Build Windows CPU / build (push) Has been cancelled
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
- Track S.detailTaskId; on task_update via WebSocket, re-render detail
  content if that task's modal is open (progress bars now update live)
- Detail logs: async load from DB via /api/tasks/{id}/logs, then append
  real-time WebSocket logs on top (de-duplicated)
- Remove log button from task list (logs now in detail modal)
- Clear S.detailTaskId on modal close

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-27 15:12:56 +08:00
liuchenandClaude Haiku 4.5 27b4a2c1bf Fix stuck 'processing' tasks after restart
Docker Build and Push / check-secrets (push) Failing after 4s
Docker Build and Push / build-and-push (cpu, latest) (push) Has been skipped
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 / build-and-push (directml, latest) (push) Has been skipped
Build Windows CPU / build (push) Has been cancelled
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
On startup, tasks in downloading/processing state are interrupted (not
completed). Previously they were re-queued, causing the same task to be
processed multiple times after each restart.

Now: interrupted tasks are immediately marked as error with message
'服务重启,任务中断'. Only 'pending' tasks are re-queued.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-27 15:10:07 +08:00
liuchenandClaude Haiku 4.5 4d2fee2b1b Remove download button from task list (moved to detail modal)
Docker Build and Push / build-and-push (cuda, 12.8) (push) Has been skipped
Docker Build and Push / check-secrets (push) Successful in 5s
Docker Build and Push / build-and-push (cpu, latest) (push) Has been skipped
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 (directml, latest) (push) Has been skipped
Build Windows CPU / build (push) Has been cancelled
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
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-27 15:05:03 +08:00
liuchenandClaude Haiku 4.5 50c6cf92ba Show full datetime (Y-M-D H:M:S) in task list and detail
Docker Build and Push / check-secrets (push) Failing after 4s
Docker Build and Push / build-and-push (cpu, latest) (push) Has been skipped
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 / build-and-push (directml, latest) (push) Has been skipped
Build Windows CPU / build (push) Has been cancelled
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
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-27 15:00:36 +08:00
ClaudeandClaude Sonnet 4.6 073a2a4fba fix: worker 对接平台 watermark-task API
Docker Build and Push / build-and-push (cuda, 12.8) (push) Has been skipped
Docker Build and Push / check-secrets (push) Failing after 4s
Docker Build and Push / build-and-push (cpu, latest) (push) Has been skipped
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 (directml, latest) (push) Has been skipped
Build Windows CPU / build (push) Has been cancelled
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
- claim/progress/result 路径改为 /openapi/watermark-task/*
- 认领字段 video_url → media_url(兼容旧字段)
- result status: passed→done,字段 output_path→output_url,error→error_message
- 新增 _upload_result_video:处理完后上传结果到平台(no_transcode 直传),获取 play_url 回传

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-06-26 23:55:56 -07:00
liuchenandClaude Haiku 4.5 76f5d1c857 Fix timestamps missing after restart + align detail modal
Docker Build and Push / check-secrets (push) Successful in 6s
Docker Build and Push / build-and-push (cpu, latest) (push) Has been skipped
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 / build-and-push (directml, latest) (push) Has been skipped
Build Windows CPU / build (push) Has been cancelled
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
- Add started_at / completed_at columns to tasks table (were missing)
- Migration adds columns to existing DB without data loss
- _save_task_to_db now persists created_at / started_at / completed_at
- _load_tasks_from_db restores all three timestamp fields on startup
- Detail modal shows same created/started/elapsed layout as table row
  with identical fmtTime / fmtDur helpers, live elapsed for in-progress tasks

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-27 14:48:58 +08:00
liuchenandClaude Haiku 4.5 d5468ec958 Set smart/watermark mode as default detection mode
Docker Build and Push / check-secrets (push) Failing after 3s
Docker Build and Push / build-and-push (cpu, latest) (push) Has been skipped
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 / build-and-push (directml, latest) (push) Has been skipped
Build Windows CPU / build (push) Has been cancelled
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
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-27 14:42:34 +08:00
liuchenandClaude Haiku 4.5 a56a861d57 Add original video playback in task detail modal
Docker Build and Push / check-secrets (push) Failing after 3s
Docker Build and Push / build-and-push (cpu, latest) (push) Has been skipped
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 / build-and-push (directml, latest) (push) Has been skipped
Build Windows CPU / build (push) Has been cancelled
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
- New API endpoint GET /api/tasks/{id}/source-stream to stream cached source file
- Detail modal shows '▶ 原视频' button (always, when cache exists) and
  '▶ 处理结果' button (only when done)
- playVideo() accepts type param ('source'|'output'), sets title and hides
  download button for source files

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-27 14:37:27 +08:00
liuchenandClaude Haiku 4.5 abd5d0ce47 Fix worker logs not appearing + align settings form
Docker Build and Push / check-secrets (push) Successful in 5s
Docker Build and Push / build-and-push (cpu, latest) (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, 11.8) (push) Has been skipped
Docker Build and Push / build-and-push (cuda, 12.8) (push) Has been skipped
Docker Build and Push / build-and-push (directml, latest) (push) Has been skipped
Build Windows CPU / build (push) Has been cancelled
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
- Add _WorkerLogHandler that bridges Python logging → WorkerLogBuffer
  (previously only StreamHandler was used, logs went to stdout but not
  the buffer the frontend polls)
- Rewrite settings form: proper form-grid layout, form submit handler,
  App Secret show/hide toggle, hints below number fields, footer save button
- workerStatus shows configured/unconfigured text instead of badge

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-27 14:34:07 +08:00
liuchenandClaude Haiku 4.5 e512a842e4 Align Worker logs page with df-autotest design
Docker Build and Push / check-secrets (push) Successful in 22s
Docker Build and Push / build-and-push (cpu, latest) (push) Has been skipped
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 / build-and-push (directml, latest) (push) Has been skipped
Build Windows CPU / build (push) Has been cancelled
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
- Extract Worker logs as separate nav page (page-worker-logs)
- Dark terminal style (#0b1320 bg, level-based colors: DEBUG/INFO/WARN/ERROR)
- Auto-poll every 2s via setInterval, starts on page enter, stops on leave
- Pause/Resume and Clear buttons, live line count
- Auto-scroll only when already at bottom
- Remove old embedded log panel from Settings page

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-27 14:29:39 +08:00
liuchenandClaude Haiku 4.5 273f03ee9f Fix modal close buttons and add task duration column
Docker Build and Push / build-and-push (directml, latest) (push) Has been skipped
Docker Build and Push / check-secrets (push) Successful in 9s
Docker Build and Push / build-and-push (cpu, latest) (push) Has been skipped
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
Build Windows CPU / build (push) Has been cancelled
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
- Fix JS crash: remove stale consoleBody scroll listener (element removed)
  This was causing all nav/modal event bindings to silently fail
- Replace all data-close delegation with direct onclick handlers
- Add 耗时 column to task list (shows elapsed time, live for in-progress tasks)
- Persist task logs to SQLite task_logs table; add GET /api/tasks/{id}/logs
- Load .env via python-dotenv; remove platform env var fallbacks (UI-only)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-27 14:16:29 +08:00
liuchenandClaude Haiku 4.5 b2dfbce962 Refactor VSR web: align UI with df-autotest, add auth and platform worker
Docker Build and Push / check-secrets (push) Failing after 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
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
Build Windows CPU / build (push) Has been cancelled
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
- Redesign web/index.html to match df-autotest light theme (sidebar layout,
  same token system, badges, panels, modals, toast notifications)
- Add simple cookie-based auth (ADMIN_PASSWORD env via .env)
- Add platform Worker (poll external platform for VSR tasks, sign requests
  with HMAC same as df-autotest)
- Add Settings page for platform connection config (DB-stored, no env vars)
- Add worker log API with incremental polling
- Remove bottom console strip; add per-task log modal with SQLite persistence
- Task table: add created_at / started_at columns, move progress to detail modal
- Fix modal close buttons (direct onclick, remove unreliable data-close delegation)
- Load .env via python-dotenv on startup

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-27 14:03:50 +08:00
liuchenandClaude Haiku 4.5 693d394f55 Add GPU support and multi-threaded subtitle detection
Docker Build and Push / check-secrets (push) Successful in 30s
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 / build-and-push (directml, latest) (push) Has been skipped
Docker Build and Push / build-and-push (cpu, latest) (push) Has been skipped
Docker Build and Push / build-and-push (cuda, 11.8) (push) Has been skipped
Build Windows CPU / build (push) Has been cancelled
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
- Add gpu_id parameter to SubtitleDetect for GPU acceleration
- Implement multi-threaded frame scanning with thread-safe CUDA context
- Add smart_detect module and web application components
- Update .gitignore to exclude web cache

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-27 13:27:26 +08:00
天涯古巷 5b780e3186 Document solution for Mac 'bad CPU type' error
Added solution for Mac version error related to CPU type.
2026-04-11 20:18:49 +08:00
天涯古巷 577b64adad Add Trendshift badge to README
Added a badge linking to the Trendshift repository.
2026-04-10 23:30:45 +08:00
天涯古巷 c63f6b52da Update README.md 2026-04-10 23:30:00 +08:00
天涯古巷 b6644f3374 Add Trendshift badge to README 2026-04-10 23:12:45 +08:00
天涯古巷 c189f8368e Add subtitle extraction feature and documentation link
Added a feature to extract original subtitles and link to video-subtitle-extractor.
2026-04-09 23:26:58 +08:00
10 changed files with 3725 additions and 69 deletions
+13
View File
@@ -0,0 +1,13 @@
# VSR Web 配置示例 — 复制为 .env 后修改
# 参考 df-autotest-platform 的 .env 结构
# 管理员密码(留空则不需要登录)
ADMIN_PASSWORD=
# 存储目录(默认 ./web_cache)
# VSR_STORAGE=./web_cache
# 服务监听端口(默认 8001)
# PORT=8001
# Platform Worker 连接信息在「系统设置」页面配置,无需在此设置
+2
View File
@@ -375,3 +375,5 @@ test*_no_sub*.mp4
/backend/tools/train/release_model/
inference.onnx
/config/config.json
web_cache/
.env
+8
View File
@@ -3,6 +3,9 @@
<div align="center">
<img src="design/icon_1024.PNG" alt="VSR Logo" width="128" height="128">
</div>
<div align="center">
<a href="https://trendshift.io/repositories/9120" target="_blank"><img src="https://trendshift.io/api/badge/repositories/9120" alt="YaoFANGUK%2Fvideo-subtitle-remover | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</div>
## 项目简介
@@ -15,6 +18,7 @@ Video-subtitle-remover (VSR) 是一款基于AI技术,将视频中的硬字幕
主要实现了以下功能:
- **无损分辨率**将视频中的硬字幕去除,生成去除字幕后的文件
- 通过超强AI算法模型,对去除字幕文本的区域进行填充(非相邻像素填充与马赛克去除)
- 提取原视频字幕,可配合:[video-subtitle-extractor (VSE)](https://github.com/YaoFANGUK/video-subtitle-extractor)
- 支持自定义字幕位置,仅去除定义位置中的字幕(传入位置)
- 支持全视频自动去除所有文本(不传入位置)
- 支持多选图片批量去除水印文本
@@ -260,6 +264,10 @@ LAMA_SUPER_FAST = False # 保证效果
解决方案:升级7-zip解压程序到最新版本
4. Mac版本运行报错:Error "bad CPU type in executable"
解决方案:打开控制台输入`softwareupdate --install-rosetta` 安装rosetta
## 赞助
+3
View File
@@ -3,6 +3,9 @@
<div align="center">
<img src="design/icon_1024.PNG" alt="VSR Logo" width="128" height="128">
</div>
<div align="center">
<a href="https://trendshift.io/repositories/9120" target="_blank"><img src="https://trendshift.io/api/badge/repositories/9120" alt="YaoFANGUK%2Fvideo-subtitle-remover | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</div>
## Project Introduction
+507
View File
@@ -0,0 +1,507 @@
"""
智能水印检测:通过帧差分找静态区域,用SAM2精确分割。
"""
import sys
import os
import numpy as np
import cv2
SAM2_PATH = "/opt/sam2"
SAM2_CKPT = "/opt/sam2/checkpoints/sam2.1_hiera_tiny.pt"
SAM2_CFG = "configs/sam2.1/sam2.1_hiera_t.yaml"
def detect_static_regions(video_path: str, sample_frames: int = 30, threshold: float = 20.0,
max_scan_seconds: float = None):
"""
饱和度签名 + 跨帧对比法检测水印/Logo。
采样策略:全程每秒一帧,分析时缩放到 1/4 分辨率提速,坐标还原回原始尺寸。
"""
cap = cv2.VideoCapture(video_path)
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
fps = cap.get(cv2.CAP_PROP_FPS) or 30.0
orig_frame_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
orig_frame_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
duration_s = total / fps
# 全程每秒采一帧
step = max(1, int(fps))
positions = list(range(0, total, step))
# 分析用 1/4 分辨率加速
SW, SH = max(1, orig_frame_w // 4), max(1, orig_frame_h // 4)
bgr_frames = [] # 小尺寸帧,用于分析
gray_frames = []
actual_pos_s = [] # 每帧对应的时间戳(秒)
for pos in positions:
cap.set(cv2.CAP_PROP_POS_FRAMES, pos)
ret, frame = cap.read()
if not ret:
continue
small = cv2.resize(frame, (SW, SH), interpolation=cv2.INTER_AREA)
bgr_frames.append(small)
gray_frames.append(cv2.cvtColor(small, cv2.COLOR_BGR2GRAY).astype(np.float32))
actual_pos_s.append(pos / fps)
cap.release()
print(f"[SAM2-detect] 采样 {len(bgr_frames)} 帧(全程每秒一帧,共{duration_s:.1f}s,分析尺寸{SW}x{SH})", flush=True)
if len(bgr_frames) < 4:
print(f"[SAM2-detect] 帧数不足4帧,返回空", flush=True)
return []
# ── 阶段1:高饱和度识别含logo帧 ─────────────────────────────────────
top_h = SH // 5
logo_sat_thresh = 200
logo_min_pixels = top_h * SW * 0.003
is_logo = []
for i, bgr in enumerate(bgr_frames):
hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV)
high_sat = int(np.sum(hsv[:top_h, :, 1] > logo_sat_thresh))
has_logo = high_sat >= logo_min_pixels
is_logo.append(has_logo)
mark = '✓' if has_logo else '✗'
print(f"[SAM2-detect] {mark} {actual_pos_s[i]:.1f}s: 高饱和={high_sat}px", flush=True)
logo_bgr_frames = [bgr_frames[i] for i, v in enumerate(is_logo) if v]
nlogo_bgr_frames = [bgr_frames[i] for i, v in enumerate(is_logo) if not v]
logo_gray_frames = [gray_frames[i] for i, v in enumerate(is_logo) if v]
print(f"[SAM2-detect] logo含帧: {len(logo_bgr_frames)}/{len(bgr_frames)}", flush=True)
# 分析时使用小尺寸坐标空间
w, h = SW, SH
# ── 路径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
print(f"[SAM2-detect] 合并后候选像素数={np.sum(static_mask>0)}", flush=True)
# 仅排除纯黑区域
mean_gray = np.mean(np.stack(gray_frames, axis=0), axis=0)
content_mask = (mean_gray > 8).astype(np.uint8) * 255
static_mask = cv2.bitwise_and(static_mask, content_mask)
print(f"[SAM2-detect] 过滤纯黑后候选像素数={np.sum(static_mask > 0)}", flush=True)
# 形态学处理:去噪 + 膨胀
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
static_mask = cv2.morphologyEx(static_mask, cv2.MORPH_OPEN, kernel)
static_mask = cv2.dilate(static_mask, kernel, iterations=2)
# 找轮廓
contours, _ = cv2.findContours(static_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
regions = []
min_area = (w * h) * 0.0005 # 至少占画面 0.05%
max_area = (w * h) * 0.25 # 最多占画面 25%
print(f"[SAM2-detect] 找到 {len(contours)} 个轮廓,min_area={min_area:.0f} max_area={max_area:.0f}", flush=True)
for cnt in contours:
area = cv2.contourArea(cnt)
x, y, bw, bh = cv2.boundingRect(cnt)
ratio = bw / bh if bh > 0 else 0
if min_area < area < max_area:
if 0.1 < ratio < 20:
regions.append((x, y, x + bw, y + bh))
print(f"[SAM2-detect] ✓ 区域 ({x},{y})-({x+bw},{y+bh}) 面积={area:.0f} 比例={ratio:.2f}", flush=True)
else:
print(f"[SAM2-detect] ✗ 比例过滤: ({x},{y})-({x+bw},{y+bh}) 面积={area:.0f} 比例={ratio:.2f}", flush=True)
else:
print(f"[SAM2-detect] ✗ 面积过滤: ({x},{y})-({x+bw},{y+bh}) 面积={area:.0f}", flush=True)
# 若检测到多个位于帧顶部的区域,合并成一个大框覆盖整个logo
# logo通常有高饱和的亮色区域(检测到)和低饱和的暗色区域(未检测到)
# 合并+适当扩展可确保完整覆盖
if len(regions) >= 2:
top_regions = [r for r in regions if r[1] < h * 0.3] # 只合并帧顶30%内的区域
if len(top_regions) >= 2:
x1 = max(0, min(r[0] for r in top_regions) - 30)
y1 = max(0, min(r[1] for r in top_regions) - 15)
x2 = min(w, max(r[2] for r in top_regions) + 30)
y2 = min(h, max(r[3] for r in top_regions) + 20)
other = [r for r in regions if r[1] >= h * 0.3]
merged = [(x1, y1, x2, y2)] + other
print(f"[SAM2-detect] 合并顶部 {len(top_regions)} 个区域为 ({x1},{y1})-({x2},{y2})", flush=True)
regions = merged
# ── 坐标从分析尺寸(SW×SH)还原到原始分辨率 ──────────────────────────
scale_x = orig_frame_w / SW
scale_y = orig_frame_h / SH
regions = [(int(x1*scale_x), int(y1*scale_y), int(x2*scale_x), int(y2*scale_y))
for x1, y1, x2, y2 in regions]
print(f"[SAM2-detect] detect_static_regions 返回 {len(regions)} 个区域(已还原到{orig_frame_w}x{orig_frame_h})", flush=True)
return regions
def refine_with_sam2(image: np.ndarray, boxes: list, gpu_id: int = 0):
"""
用 SAM2 对检测到的区域精确分割,返回合并后的 mask。
image: BGR numpy array
boxes: [(x1,y1,x2,y2), ...]
返回 binary mask (H, W) uint8
"""
if not boxes:
return None
try:
if SAM2_PATH not in sys.path:
sys.path.insert(0, SAM2_PATH)
import torch
from sam2.build_sam import build_sam2
from sam2.sam2_image_predictor import SAM2ImagePredictor
device = f"cuda:{gpu_id}" if torch.cuda.is_available() else "cpu"
model = build_sam2(SAM2_CFG, SAM2_CKPT, device=device)
predictor = SAM2ImagePredictor(model)
rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
predictor.set_image(rgb)
h, w = image.shape[:2]
combined_mask = np.zeros((h, w), dtype=np.uint8)
for (x1, y1, x2, y2) in boxes:
box = np.array([x1, y1, x2, y2])
masks, scores, _ = predictor.predict(
box=box[None],
multimask_output=False,
)
if masks is not None and len(masks) > 0:
m = masks[0].astype(np.uint8) * 255
combined_mask = cv2.bitwise_or(combined_mask, m)
return combined_mask
except Exception as e:
print(f"[SAM2] 分割失败: {e}", flush=True)
return None
def _frame_text_candidates(gray, tophat_kernel, conn_kernel, th_val=22):
"""单帧:white top-hat 提取细笔画亮文字 → 候选文字框 (xmin,xmax,ymin,ymax)。
top-hat 只响应比周围亮、且比核(15px)细的结构 → 文字笔画命中,大块皮肤/丝袜不命中。
再用「连通组件数」区分文字(多个字母,>=3)和实心亮物(卡扣/内衣,1-2块)。"""
tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, tophat_kernel)
th = cv2.threshold(tophat, th_val, 255, cv2.THRESH_BINARY)[1]
closed = cv2.morphologyEx(th, cv2.MORPH_CLOSE, conn_kernel, iterations=2)
cnts, _ = cv2.findContours(closed, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
boxes = []
for c in cnts:
x, y, bw, bh = cv2.boundingRect(c)
area = bw * bh
ar = bw / max(bh, 1)
# 文字特征:宽>高、有一定宽度、不至于太小
if not (area > 350 and 1.3 < ar < 15 and bw > 35 and bh > 6):
continue
# 框内细笔画(未闭合的原始 top-hat)的独立组件数:文字=多字母(>=3),实心物=1-2
roi = th[y:y+bh, x:x+bw]
n, _, stats, _ = cv2.connectedComponentsWithStats(roi, connectivity=8)
comp = sum(1 for i in range(1, n) if stats[i, cv2.CC_STAT_AREA] >= 8)
if comp < 3:
continue
boxes.append((x, x + bw, y, y + bh)) # (xmin,xmax,ymin,ymax)
return boxes
def detect_color_logo(video_path: str, sample_n: int = 40):
"""检测彩色不透明 logo 水印(如「黑料网」橙色横幅)。
判据:高饱和(>150) AND 时序低方差(静止) → 排除会动的皮肤、柔和的背景画。
返回 [(xmin,xmax,ymin,ymax), ...] 静止框(应用到所有帧)。"""
cap = cv2.VideoCapture(video_path)
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
positions = np.linspace(0, total - 1, min(sample_n, total)).astype(int)
gray_stack, sat_acc, cnt = [], np.zeros((h, w), np.float32), 0
for p in positions:
cap.set(cv2.CAP_PROP_POS_FRAMES, int(p))
ret, f = cap.read()
if not ret:
continue
gray_stack.append(cv2.cvtColor(f, cv2.COLOR_BGR2GRAY).astype(np.float32))
sat_acc += cv2.cvtColor(f, cv2.COLOR_BGR2HSV)[:, :, 1].astype(np.float32)
cnt += 1
cap.release()
if cnt < 4:
return []
std = np.std(np.stack(gray_stack), axis=0)
sat = sat_acc / cnt
mask = ((sat > 150) & (std < 12)).astype(np.uint8) * 255
k = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 15))
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, k, iterations=3)
mask = cv2.dilate(mask, k, iterations=1)
cnts, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
raw = []
for c in cnts:
x, y, bw, bh = cv2.boundingRect(c)
if bw * bh > 2000:
raw.append((x, y, x + bw, y + bh))
if not raw:
return []
# 合并相互靠近的区域(同一 logo 的不同彩色部件)
merged = []
used = [False] * len(raw)
for i in range(len(raw)):
if used[i]:
continue
x1, y1, x2, y2 = raw[i]
used[i] = True
changed = True
while changed:
changed = False
for j in range(len(raw)):
if used[j]:
continue
bx1, by1, bx2, by2 = raw[j]
# 间距 < 80px 视为同一 logo
if (bx1 < x2 + 80 and bx2 > x1 - 80 and by1 < y2 + 80 and by2 > y1 - 80):
x1, y1 = min(x1, bx1), min(y1, by1)
x2, y2 = max(x2, bx2), max(y2, by2)
used[j] = True
changed = True
# pad 扩展确保覆盖整个 logo(含低饱和的白字/黑字部分)
pad = 18
x1, y1 = max(0, x1 - pad), max(0, y1 - pad)
x2, y2 = min(w, x2 + pad), min(h, y2 + pad)
# 角落吸附:logo 通常贴边,若框已靠近某边(<160px)则延伸到该边,
# 确保覆盖低饱和的边缘文字(如「黑料网」的黑字)
if x1 < 160:
x1 = 0
if x2 > w - 160:
x2 = w
if y1 < 160:
y1 = 0
if y2 > h - 160:
y2 = h
merged.append((x1, x2, y1, y2))
print(f"[ColorLogo] 检测到 {len(merged)} 个彩色 logo: {merged}", flush=True)
return merged
def detect_watermark_per_frame(video_path: str, sample_step: int = None, pad: int = 6):
"""逐帧检测半透明/移动文字水印,返回 {frame_no: [(xmin,xmax,ymin,ymax), ...]}。
适用于 OCR 检测不到的淡色水印(白字、半透明)以及垂直滚动水印。
采样:每秒一帧(cap.set 跳帧,长视频也快)。
判据:基于轨迹自身(绝对出现秒数 + 出现时段内密度),不依赖视频总长,
因此「只在部分时段出现的滚动水印」「贯穿全程偶有漏检的固定水印」都能被保留。
"""
cap = cv2.VideoCapture(video_path)
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
tophat_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15))
conn_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (25, 9))
# 阶段1:每秒一帧采样(cap.set 跳帧;frame_no 用 1-based 与 inpaint 管线一致)
step = max(1, int(round(fps))) # 每秒对应的帧步长
sample_frames = list(range(0, total, step))
frame_boxes = {} # sample_idx -> [box]
sidx_to_fno = {} # sample_idx -> 1-based 帧号
for si, pos in enumerate(sample_frames):
cap.set(cv2.CAP_PROP_POS_FRAMES, pos)
ret, frame = cap.read()
if not ret:
continue
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
frame_boxes[si] = _frame_text_candidates(gray, tophat_kernel, conn_kernel)
sidx_to_fno[si] = pos + 1
cap.release()
n_sampled = len(frame_boxes)
if n_sampled == 0:
return {}
# 阶段2:轨迹连接(按「采样序号」相邻,间断容忍 3 个采样点=3秒)
sampled_sidx = sorted(frame_boxes.keys())
def cx_of(b): return (b[0] + b[1]) / 2
def cy_of(b): return (b[2] + b[3]) / 2
def wid_of(b): return b[1] - b[0]
GAP_TOL = 5 # 允许漏检 5 个采样点(5秒,滚动水印经过浅背景会暂时漏检)
tracks = [] # {"boxes": {sidx:box}, "last_si":, "last_box":}
for si in sampled_sidx:
for b in frame_boxes[si]:
best, best_d = None, 1e9
for tr in tracks:
if si - tr["last_si"] > GAP_TOL:
continue
lb = tr["last_box"]
gap = si - tr["last_si"]
dx = abs(cx_of(b) - cx_of(lb))
dw = abs(wid_of(b) - wid_of(lb))
dy = abs(cy_of(b) - cy_of(lb))
# 同一水印:X几乎不变、宽度接近;Y 可滚动(按间隔放宽,每秒最多移动~60px)
if dx < 45 and dw < 28 and dy < 60 * max(1, gap):
d = dx * 2 + dw * 2 + dy * 0.2
if d < best_d:
best, best_d = tr, d
if best is None:
tracks.append({"boxes": {si: b}, "last_si": si, "last_box": b})
else:
best["boxes"][si] = b
best["last_si"] = si
best["last_box"] = b
# 阶段3:筛选有效轨迹 —— 不依赖视频总长
# ① 至少出现 MIN_SECONDS 秒(绝对阈值,滤掉一闪而过的噪点)
# ② 在其出现时段 [first,last] 内密度高(持续存在,不是零散巧合)
# ③ 宽度稳定(文字水印宽度恒定,人体亮区会抖)
MIN_SECONDS = 4
valid = [] # 有效轨迹的 boxes dict 列表
for tr in tracks:
boxes = tr["boxes"]
n_pts = len(boxes)
if n_pts < MIN_SECONDS:
continue
sids = sorted(boxes.keys())
span = sids[-1] - sids[0] + 1
density = n_pts / span
if density < 0.5:
continue
widths = [wid_of(b) for b in boxes.values()]
wmean = sum(widths) / len(widths)
wstd = (sum((x - wmean) ** 2 for x in widths) / len(widths)) ** 0.5
if wmean > 0 and wstd / wmean > 0.4:
continue
valid.append(boxes)
print(f"[WM-perframe] 每秒一帧采样{n_sampled}帧 轨迹{len(tracks)}条 有效{len(valid)}条", flush=True)
# 阶段4:每条有效轨迹在其 [first,last] 时段内逐采样点线性插值,
# 补全中间漏检的采样点(滚动水印经过浅背景漏检时也能被覆盖),
# 再把每个采样点的框展开到对应的 1 秒帧区间,y 方向外扩覆盖滚动移动。
ROLL_PAD = 16
full = {}
def lerp_box(ba, bb, t):
return tuple(int(round(ba[i] + (bb[i] - ba[i]) * t)) for i in range(4))
for boxes in valid:
sids = sorted(boxes.keys())
first, last = sids[0], sids[-1]
for si in range(first, last + 1):
if si in boxes:
b = boxes[si]
else:
# 线性插值:前后最近的已知采样点
prev = max(s for s in sids if s < si)
nxt = min(s for s in sids if s > si)
b = lerp_box(boxes[prev], boxes[nxt], (si - prev) / (nxt - prev))
x1, x2, y1, y2 = b
bx = (max(0, x1 - pad), min(w, x2 + pad),
max(0, y1 - ROLL_PAD), min(h, y2 + ROLL_PAD))
# 该采样点覆盖 [本帧, 下一采样帧) 的所有原始帧
f_start = si * step + 1
f_end = (si + 1) * step + 1
for f in range(f_start, min(f_end, total + 1)):
full.setdefault(f, [])
full[f].append(bx)
# 阶段5:彩色 logo(高饱和+静止)→ 静止框,注入所有帧
color_logos = detect_color_logo(video_path)
if color_logos:
for f in range(1, total + 1):
full.setdefault(f, [])
full[f] = full[f] + list(color_logos)
total_boxes = sum(len(v) for v in full.values())
print(f"[WM-perframe] 输出 {len(full)} 帧 共 {total_boxes} 个水印框(文字+彩色logo)", flush=True)
return full
def smart_detect_watermark(video_path: str, sample_frames: int = 30, gpu_id: int = 0):
"""
完整流程:帧差分(前30秒每秒一帧)→ SAM2 精确分割。
限制扫描前30秒:H.264压缩导致长视频里即使静态logo的像素std也会随时间累积超过阈值。
返回 [(x1, y1, x2, y2), ...] 像素坐标列表。
"""
print(f"[SAM2] smart_detect_watermark 开始: {video_path}", flush=True)
# 1. 静态区域检测:跨时间段帧对比法(全视频)
boxes = detect_static_regions(video_path, sample_frames)
print(f"[SAM2] detect_static_regions 返回 {len(boxes)} 个候选区域: {boxes}", flush=True)
if not boxes:
return []
# 2. 读取代表帧用于 SAM2
cap = cv2.VideoCapture(video_path)
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
cap.set(cv2.CAP_PROP_POS_FRAMES, total // 2)
ret, frame = cap.read()
cap.release()
if not ret:
return boxes # SAM2 失败时直接返回粗检测结果
# 3. SAM2 精确分割
mask = refine_with_sam2(frame, boxes, gpu_id=gpu_id)
if mask is None:
return boxes
# 4. 从 mask 提取最终 bounding boxes
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
refined = []
for cnt in contours:
if cv2.contourArea(cnt) > 100:
x, y, bw, bh = cv2.boundingRect(cnt)
refined.append((x, y, x + bw, y + bh))
result = refined if refined else boxes
print(f"[SAM2] smart_detect_watermark 最终返回 {len(result)} 个区域: {result}", flush=True)
return result
+92 -26
View File
@@ -21,9 +21,10 @@ class SubtitleDetect:
# 采样间隔,根据视频帧率在 _init_sample_step 中自适应设置
SAMPLE_STEP = 3
def __init__(self, video_path, sub_areas=[]):
def __init__(self, video_path, sub_areas=[], gpu_id=0):
self.video_path = video_path
self.sub_areas = sub_areas
self.gpu_id = gpu_id
self._init_sample_step()
def _init_sample_step(self):
@@ -46,10 +47,12 @@ class SubtitleDetect:
hardware_accelerator = HardwareAccelerator.instance()
onnx_providers = hardware_accelerator.onnx_providers
model_config = ModelConfig()
import torch as _torch
_device = f"gpu:{self.gpu_id}" if _torch.cuda.is_available() else "cpu"
return TextDetection(
model_name=model_config.DET_MODEL_NAME,
model_dir=model_config.DET_MODEL_DIR,
device="cpu",
device=_device,
enable_hpi=len(onnx_providers) > 0,
)
@@ -81,34 +84,97 @@ class SubtitleDetect:
break
return temp_list
def find_subtitle_frame_no(self, sub_remover=None):
video_cap = cv2.VideoCapture(get_readable_path(self.video_path))
frame_count = video_cap.get(cv2.CAP_PROP_FRAME_COUNT)
tbar = tqdm(total=int(frame_count), unit='frame', position=0, file=sys.__stdout__, desc='Subtitle Finding')
current_frame_no = 0
# 阶段1:采样检测,仅对每隔 sample_step 帧执行 OCR
sampled_results = {} # frame_no -> temp_list
if sub_remover:
sub_remover.append_output(tr['Main']['ProcessingStartFindingSubtitles'])
while video_cap.isOpened():
ret, frame = video_cap.read()
# 如果读取视频帧失败(视频读到最后一帧)
def _scan_range(self, start_frame, end_frame, ab_sections, results, lock,
progress_counter, frame_count, sub_remover, gpu_id=None):
"""单线程扫描指定帧范围。
每个线程在自己内部初始化 OCR 模型(CUDA 上下文归该线程所有),避免跨线程 CUDA 问题。
gpu_id: >=0 用 GPU,None 用 CPU。
"""
# 在本线程内创建并初始化 OCR 模型,CUDA 上下文属于本线程
if gpu_id is not None and gpu_id >= 0:
import torch as _t
if _t.cuda.is_available():
_local_det = SubtitleDetect(self.video_path, self.sub_areas, gpu_id=gpu_id)
_ = _local_det.text_detector # 在本线程建立 CUDA 上下文
else:
_local_det = SubtitleDetect(self.video_path, self.sub_areas)
else:
_local_det = SubtitleDetect(self.video_path, self.sub_areas)
cap = cv2.VideoCapture(get_readable_path(self.video_path))
cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
frame_no = start_frame
while frame_no < end_frame:
ret, frame = cap.read()
if not ret:
break
# 读取视频帧成功
current_frame_no += 1
if not is_frame_number_in_ab_sections(current_frame_no - 1, sub_remover.ab_sections):
tbar.update(1)
frame_no += 1
if not is_frame_number_in_ab_sections(frame_no - 1, ab_sections):
continue
# 仅对采样帧执行 OCR 推理
if (current_frame_no - 1) % self.SAMPLE_STEP == 0 or self.SAMPLE_STEP <= 1:
temp_list = self.detect_subtitle(frame)
if len(temp_list) > 0:
sampled_results[current_frame_no] = temp_list
tbar.update(1)
if sub_remover:
sub_remover.progress_total = (100 * float(current_frame_no) / float(frame_count)) // 2
if (frame_no - 1) % self.SAMPLE_STEP == 0 or self.SAMPLE_STEP <= 1:
temp_list = _local_det.detect_subtitle(frame)
if temp_list:
with lock:
results[frame_no] = temp_list
with lock:
progress_counter[0] += 1
if sub_remover:
sub_remover.progress_total = (100 * progress_counter[0] / frame_count) // 2
sub_remover.notify_progress_listeners()
cap.release()
def find_subtitle_frame_no(self, sub_remover=None, num_workers=4):
import threading
video_cap = cv2.VideoCapture(get_readable_path(self.video_path))
frame_count = int(video_cap.get(cv2.CAP_PROP_FRAME_COUNT))
video_cap.release()
tbar = tqdm(total=frame_count, unit='frame', position=0, file=sys.__stdout__, desc='Subtitle Finding')
# 阶段1:并行采样检测
sampled_results = {}
if sub_remover:
sub_remover.append_output(tr['Main']['ProcessingStartFindingSubtitles'])
ab_sections = sub_remover.ab_sections if sub_remover else []
lock = threading.Lock()
import torch as _torch
cuda_count = _torch.cuda.device_count() if _torch.cuda.is_available() else 0
# 每个线程在自己内部初始化模型,CUDA 上下文属于该线程,避免跨线程推理失败
# GPU0 用于 OCR,GPU1 留给 STTN/LaMa(若可用);无 GPU 则 CPU
_ocr_gpu_id = 0 if cuda_count >= 1 else None
print(f"[OCR] {'GPU' + str(_ocr_gpu_id) if _ocr_gpu_id is not None else 'CPU'} 推理,{num_workers} 线程并行(每线程独立模型)", flush=True)
progress_counter = [0]
chunk = max(1, frame_count // num_workers)
ranges = [(i * chunk, min((i + 1) * chunk, frame_count)) for i in range(num_workers)]
threads = []
for i, (s, e) in enumerate(ranges):
t = threading.Thread(
target=self._scan_range,
args=(s, e, ab_sections, sampled_results, lock, progress_counter, frame_count, sub_remover, _ocr_gpu_id),
daemon=True,
)
threads.append(t)
t.start()
# tqdm 进度跟踪(单独线程更新)
import time as _time
last = 0
while any(t.is_alive() for t in threads):
with lock:
cur = progress_counter[0]
tbar.update(cur - last)
last = cur
_time.sleep(0.2)
for t in threads:
t.join()
tbar.update(frame_count - last)
tbar.close()
# 线程退出后其局部 OCR 模型自动被 GC,触发 GPU 显存释放
import gc as _gc
_gc.collect()
try:
import torch as _t2
if _t2.cuda.is_available():
_t2.cuda.empty_cache()
except Exception:
pass
# 阶段2:插值填充 — 两个采样帧之间都有字幕时,中间帧也标记为有字幕
subtitle_frame_no_box_dict = {}
detected_nos = sorted(sampled_results.keys())
+63 -43
View File
@@ -25,7 +25,13 @@ class FramePrefetcher:
def _read_loop(self):
while not self._stopped:
ret, frame = self.cap.read()
self._buffer.put((ret, frame))
# 用带超时的 put,避免队列满时永久阻塞
while not self._stopped:
try:
self._buffer.put((ret, frame), timeout=0.5)
break
except queue.Full:
pass
if not ret:
break
@@ -39,11 +45,12 @@ class FramePrefetcher:
def stop(self):
"""停止预读取,不释放底层 video_cap。"""
self._stopped = True
try:
while not self._buffer.empty():
# 排空队列,解除后台线程可能阻塞的 put()
while True:
try:
self._buffer.get_nowait()
except queue.Empty:
pass
except queue.Empty:
break
self._thread.join(timeout=5)
def release(self):
@@ -53,52 +60,65 @@ class FramePrefetcher:
class FFmpegVideoWriter:
"""
通过 FFmpeg 管道写入帧,使用 libx264 编码。
接口兼容 cv2.VideoWriter(write/release)。
独立 I/O 线程写帧到 FFmpeg stdin,直接输出 H.264。
主线程只往队列里放帧,永不阻塞在 I/O 上。
"""
def __init__(self, output_path, fps, size):
w, h = size
cmd = [
FFmpegCLI.instance().ffmpeg_path,
'-y',
'-f', 'rawvideo',
'-vcodec', 'rawvideo',
'-s', f'{w}x{h}',
'-pix_fmt', 'bgr24',
'-r', str(fps),
'-i', '-',
'-c:v', 'libx264',
'-pix_fmt', 'yuv420p',
'-crf', '18',
'-preset', 'fast',
'-loglevel', 'error',
output_path
]
self._process = subprocess.Popen(
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
self._queue = queue.Queue(maxsize=60)
self._error = None
# 优先用系统 FFmpeg + NVENC(GPU 编码),回退到 libx264
import shutil as _shutil
_sys_ffmpeg = '/usr/bin/ffmpeg'
_nvenc_ok = (
_shutil.which(_sys_ffmpeg) is not None and
subprocess.run(
[_sys_ffmpeg, '-hide_banner', '-encoders'],
capture_output=True, text=True
).stdout.find('h264_nvenc') >= 0
)
ffmpeg_bin = _sys_ffmpeg if _nvenc_ok else FFmpegCLI.instance().ffmpeg_path
codec_args = (
['-vf', 'format=yuv420p', '-vcodec', 'h264_nvenc', '-cq', '18', '-preset', 'p4']
if _nvenc_ok else
['-vcodec', 'libx264', '-crf', '18', '-preset', 'ultrafast']
)
cmd = [
ffmpeg_bin, '-y',
'-f', 'rawvideo', '-vcodec', 'rawvideo',
'-s', f'{w}x{h}', '-pix_fmt', 'bgr24',
'-r', str(fps), '-i', 'pipe:0',
*codec_args,
'-loglevel', 'error', output_path,
]
self._proc = subprocess.Popen(cmd, stdin=subprocess.PIPE)
self._thread = threading.Thread(target=self._io_loop, daemon=True)
self._thread.start()
def _io_loop(self):
try:
while True:
frame = self._queue.get()
if frame is None: # sentinel
break
self._proc.stdin.write(frame.tobytes())
except Exception as e:
self._error = e
finally:
try:
self._proc.stdin.close()
except Exception:
pass
def write(self, frame):
"""写入一帧(numpy BGR 数组)。"""
if frame.dtype != np.uint8:
frame = np.clip(frame, 0, 255).astype(np.uint8)
try:
self._process.stdin.write(frame.tobytes())
except BrokenPipeError:
pass
self._queue.put(frame)
def release(self):
"""关闭管道并等待编码完成。"""
try:
self._process.stdin.close()
except BrokenPipeError:
pass
try:
self._process.wait(timeout=600)
except subprocess.TimeoutExpired:
self._process.terminate()
self._process.wait(timeout=5)
self._queue.put(None) # 通知 I/O 线程退出
self._thread.join() # 等待所有帧写完
self._proc.wait() # 等待 FFmpeg 完成编码
if self._error:
raise RuntimeError(f"FFmpegVideoWriter error: {self._error}")
+155
View File
@@ -0,0 +1,155 @@
"""
Minimal qfluentwidgets stub for headless/server use — no Qt required.
Provides just enough for backend/config.py to import and function correctly.
"""
import json
import os
class _Signal:
def connect(self, fn): pass
def disconnect(self, fn=None): pass
def emit(self, *args): pass
class ConfigValidator:
def validate(self, v): return True
def correct(self, v): return v
class BoolValidator(ConfigValidator):
def validate(self, v): return isinstance(v, bool)
def correct(self, v): return bool(v)
class OptionsValidator(ConfigValidator):
def __init__(self, options):
try:
self._opts = [e for e in options]
except TypeError:
self._opts = []
def validate(self, v): return v in self._opts
def correct(self, v): return v if v in self._opts else (self._opts[0] if self._opts else v)
class RangeValidator(ConfigValidator):
def __init__(self, min_val, max_val):
self.min_val = min_val
self.max_val = max_val
def validate(self, v): return self.min_val <= v <= self.max_val
def correct(self, v): return max(self.min_val, min(self.max_val, v))
class EnumSerializer:
def __init__(self, enum_cls):
self.enum_cls = enum_cls
def serialize(self, v):
return v.value if hasattr(v, 'value') else str(v)
def deserialize(self, s):
for e in self.enum_cls:
if e.value == s:
return e
return s
class ConfigItem:
def __init__(self, section, key, default, validator=None, restart=False):
self.section = section
self.key = key
self.value = default
self.validator = validator or ConfigValidator()
self.restart = restart
self.valueChanged = _Signal()
class OptionsConfigItem(ConfigItem):
def __init__(self, section, key, default, validator=None, serializer=None, restart=False):
super().__init__(section, key, default, validator, restart)
self.serializer = serializer
class RangeConfigItem(ConfigItem):
def __init__(self, section, key, default, validator=None):
super().__init__(section, key, default, validator)
class QConfig:
appRestartSig = _Signal()
def set(self, item, value):
item.value = value
try:
item.valueChanged.emit(value)
except Exception:
pass
class _QConfigLoader:
def load(self, filepath, config_obj):
if not os.path.exists(filepath):
return
try:
with open(filepath, 'r', encoding='utf-8') as f:
data = json.load(f)
except Exception:
return
cls = type(config_obj)
for attr_name in dir(cls):
if attr_name.startswith('_'):
continue
item = getattr(cls, attr_name, None)
if not isinstance(item, ConfigItem):
continue
section_data = data.get(item.section, {})
if item.key not in section_data:
continue
raw = section_data[item.key]
if isinstance(item, OptionsConfigItem) and item.serializer:
try:
raw = item.serializer.deserialize(raw)
except Exception:
pass
if item.validator.validate(raw):
item.value = raw
qconfig = _QConfigLoader()
# ── additional symbols referenced by gui code (safe no-ops) ──────────────
class FluentIcon:
HOME = None; SETTING = None; FOLDER = None; PLAY = None
class Theme:
DARK = 'dark'; LIGHT = 'light'
class NavigationItemPosition:
BOTTOM = 'bottom'
def setTheme(*a, **k): pass
def setThemeColor(*a, **k): pass
def isDarkTheme(): return True
# Fluent widget stubs — only imported by GUI code, never used by backend
class _Widget:
def __init__(self, *a, **k): pass
CardWidget = _Widget
PushButton = _Widget
Slider = _Widget
ProgressBar = _Widget
PlainTextEdit = _Widget
TextEdit = _Widget
SubtitleLabel = _Widget
HollowHandleStyle = _Widget
InfoBar = _Widget
SettingCardGroup = _Widget
ComboBoxSettingCard = _Widget
SwitchSettingCard = _Widget
FluentWindow = _Widget
NavigationWidget = _Widget
+1534
View File
File diff suppressed because it is too large Load Diff
+1348
View File
File diff suppressed because it is too large Load Diff