From 3f916944912231f3fe20516ab29241f5ffd482dd Mon Sep 17 00:00:00 2001 From: mike Date: Sun, 28 Jun 2026 00:51:07 +0200 Subject: [PATCH] reorder --- AGENTS.md | 159 ++++++++++++++++++++----- backlog.md | 2 +- tour-comfy/car.html | 263 +++++++++++++++++++++++++++++++---------- tour-comfy/edit_api.py | 8 +- tour-comfy/trash.html | 5 +- 5 files changed, 341 insertions(+), 96 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a9cb5d2..c444399 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -63,42 +63,122 @@ FastAPI :8500 (edit_api.py) - **Systemd**: `comfyui-api` - **DB**: PostgreSQL at `192.168.1.160:5433`, database `dv`, table `person` -#### Core Endpoints +#### Endpoints Library +##### Core Generation & Upload +| Method | Path | Purpose | +|--------|------|---------| +| `POST` | `/upload` | Upload image; optionally add to existing group (`group_id` + `skip_poses`) | +| `POST` | `/batch` | Queue multi-prompt generation job (async) | +| `GET` | `/batch/{job_id}` | Poll batch job status / per-item progress | +| `DELETE` | `/batch/{job_id}` | Cancel active batch job | +| `POST` | `/multi-ref` | Queue a generation job with multiple reference images | +| `POST` | `/refine-prompt` | Refine a generation prompt using the external uncensored chat LLM | +| `POST` | `/edit` | Edit uploaded image | +| `POST` | `/generate-scenery` | Generate scenery background based on video frame + reference image | +| `GET` | `/scenery/library` | Return all scenery images grouped by source video reference | + +##### Image Editing & Canvas Expansion (Padding & Crop) +| Method | Path | Purpose | +|--------|------|---------| +| `POST` | `/images/{filename}/crop` | Crop to pixel rect; `as_copy:true` crops a referenced copy instead of modifying in-place | +| `POST` | `/images/{filename}/autocrop` | Auto-trim transparent borders in-place | +| `POST` | `/images/{filename}/pad` | Expand image canvas with custom padding on each side, optional background fills (`transparent`, `black`, `white`), and optional Qwen-based Outpainting (`outpaint:true`) | +| `POST` | `/images/{filename}/rotate` | Rotate clockwise in 90° steps (lossless transpose, in-place) | +| `POST` | `/images/{filename}/invert-alpha` | Invert the alpha channel (recovers background removal errors) | +| `POST` | `/images/{filename}/duplicate` | Copy image into the same group with timestamp-based filename and `source_refs=[original]` | +| `POST` | `/restore-background/{filename}` | Flatten RGBA to opaque RGB via white composite background | + +##### Background Removal +| Method | Path | Purpose | +|--------|------|---------| +| `POST` | `/remove-background/{filename}` | rembg BG removal, overwrites in-place; sets `has_background=false` + refreshes static data | +| `POST` | `/remove-background-sam/{filename}` | SAM2 BG removal → `.nobg.png` sidecar | +| `POST` | `/remove-background/group/{group_id}` | Queue background removal for all group members as an async background task | +| `GET` | `/sam2/check` | Return whether SAM2 background remover is available | + +##### Metadata, Custom Labels & Group Management | Method | Path | Purpose | |--------|------|---------| | `GET` | `/images` | List all images with DB metadata | | `GET` | `/images/{filename}` | Serve image file | -| `POST` | `/upload` | Upload image; optionally add to existing group (`group_id` + `skip_poses`) | -| `POST` | `/batch` | Queue multi-prompt generation job (async) | -| `GET` | `/jobs/{id}` | Poll batch job status / per-item progress | -| `POST` | `/images/{filename}/set-preferred` | Move to group slot 0; triggers face extraction background task | -| `POST` | `/images/{filename}/extract-face` | Extract & save face crop via insightface (background task) | -| `POST` | `/images/{filename}/archive` | Soft-archive (hidden from default view) | -| `POST` | `/images/{filename}/unarchive` | Restore archived image | -| `POST` | `/images/{filename}/set-hidden` | Toggle studio visibility | -| `DELETE` | `/images/{filename}` | Delete image + DB record | -| `POST` | `/images/{filename}/crop` | Crop to pixel rect; `as_copy:true` crops a referenced copy instead of in-place | -| `POST` | `/images/{filename}/autocrop` | Auto-trim transparent border | -| `POST` | `/images/{filename}/rotate` | Rotate clockwise in 90° steps (lossless transpose, in-place) | -| `POST` | `/images/{filename}/invert-alpha` | Invert the alpha channel (fixes wrong-segment BG removal) | -| `POST` | `/images/{filename}/duplicate` | Copy into same group with `source_refs=[original]` | -| `POST` | `/faceswap` | insightface video faceswap | -| `POST` | `/remove-background/{filename}` | rembg BG removal, overwrites in-place; sets `has_background=false` + refreshes static data | -| `POST` | `/remove-background-sam/{filename}` | SAM2 BG removal → `.nobg.png` sidecar | -| `GET` | `/wireframe/frame/{video_name}` | Extract frame at `?t=` (0–1) from wireframe video | | `GET` | `/groups` | List all groups with members | +| `GET` | `/group-names` | Return map of custom group names | +| `POST` | `/group-names/{group_id}` | Save custom group name | +| `POST` | `/groups/merge` | Merge several groups into one group | +| `POST` | `/groups/extract` | Extract a file into its own solo group | +| `GET` | `/groups/{group_id}/order` | Return order of members in a group | +| `POST` | `/groups/{group_id}/order` | Set custom sort order of group members | +| `GET` | `/similar/{filename}` | Find visually similar images using database embeddings | | `GET` | `/names` | Map `filename → person name` | -| `GET/POST` | `/config` | Read / write `config.json` | -| `GET` | `/poses` | Load pose library from `poses.md` | -| `POST` | `/poses` | Create / update / rename a pose (`old_name` to rename) → rewrites `poses.md` | -| `DELETE` | `/poses/{name}` | Delete a pose from `poses.md` | -| `GET` | `/pose/check` | Report pose-estimator availability + backend | +| `POST` | `/names/{filename}` | Map image file to custom character name | + +##### Visibility, Archiving & Deletion +| Method | Path | Purpose | +|--------|------|---------| +| `POST` | `/images/{filename}/hidden` | Toggle studio visibility (soft-hide) | +| `POST` | `/images/{filename}/archive` | Soft-archive image (hidden from default view) | +| `POST` | `/images/{filename}/unarchive` | Restore soft-archived image | +| `DELETE` | `/images/{filename}` | Delete image file + DB record | +| `POST` | `/groups/{group_id}/archive` | Soft-archive an entire group | +| `POST` | `/groups/{group_id}/unarchive` | Restore an archived group | +| `DELETE` | `/groups/{group_id}` | Delete entire group files and DB records | + +##### 2D Body Pose Tools (rtmlib / RTMPose) +| Method | Path | Purpose | +|--------|------|---------| +| `GET` | `/pose/check` | Report pose-estimator availability + backend (rtmlib or mediapipe) | | `POST` | `/images/{filename}/pose` | Estimate COCO-17 body keypoints (caches descriptor for similar-search) | | `GET` | `/pose/similar/{filename}` | Rank library images by pose similarity to this image | | `POST` | `/pose/similar` | Rank library images by similarity to a supplied (edited) skeleton | | `POST` | `/pose/index` | Build pose descriptors for the whole library (daemon thread) | | `GET` | `/pose/index/status` | Poll pose-index build progress | +| `POST` | `/pose/from-wireframe` | Extract frame from a wireframe video and estimate body pose keypoints | +| `POST` | `/pose/render` | Render keypoints as OpenPose skeleton image (base64) | +| `POST` | `/generate-with-pose` | Generate image from a specific pose skeleton (avoids wireframe bleed-through) | +| `GET` | `/poses` | Load pose library from `poses.md` | +| `POST` | `/poses` | Create / update / rename a pose (`old_name` to rename) → rewrites `poses.md` | +| `DELETE` | `/poses/{name}` | Delete a pose from `poses.md` | + +##### 3D Turntable & Orbit Previews +| Method | Path | Purpose | +|--------|------|---------| +| `POST` | `/orbit` | Queue an orbit preview generation. Engine `depth` (fast 2.5D card parallax) or `qwen` (turntable) | +| `GET` | `/turntable/status` | Return background turntable generation state/progress summary | +| `GET` | `/turntable/status/{group_id}` | Return turntable state for a specific group | +| `POST` | `/turntable/pause` | Pause background turntable generation | +| `POST` | `/turntable/resume` | Resume background turntable generation | +| `DELETE` | `/turntable/{group_id}` | Reset turntable cache & records for group to force re-generation | + +##### Face Processing (insightface) +| Method | Path | Purpose | +|--------|------|---------| +| `POST` | `/faceswap` | insightface video faceswap | +| `GET` | `/faceswap/check` | Report available face enhancement backends | +| `POST` | `/images/{filename}/extract-face` | Extract & save face crop via insightface (background task) | +| `POST` | `/faces/similar` | Find groups with visually similar faces using embeddings | +| `POST` | `/faces/index` | Build face embedding index for all preferred faces in library | +| `GET` | `/faces/index/status` | Poll face-indexing progress status | +| `GET` | `/faces/{group_id}` | Check if face crop exists for group | + +##### Videos & Wireframe Guides +| Method | Path | Purpose | +|--------|------|---------| +| `GET` | `/videos` | List all available wireframe guide videos | +| `GET` | `/wireframe/frame/{video_name}` | Extract frame at `?t=` (0–1) from wireframe video | +| `GET` | `/wireframe/duration/{video_name}` | Get video duration (seconds) via ffprobe | +| `POST` | `/wireframe/trim` | Trim a wireframe video to given start/end offsets | +| `POST` | `/wireframe/frame` | Extract frame at timestamp, returns base64 PNG | +| `POST` | `/generate-video` | Stitch list of output images into a short looping MP4 video | + +##### Database, Config & System Diagnostics +| Method | Path | Purpose | +|--------|------|---------| +| `GET/POST` | `/config` | Read / write `config.json` | +| `GET` | `/db/inconsistencies` | Retrieve consistency report or run a check with `?run_now=true` | +| `POST` | `/db/repair` | Run repair actions (restore, import file, assign group, permanently delete) | +| `POST` | `/db/cleanup` | Delete DB records of files that are missing on disk | +| `GET` | `/health` | API health check status | --- @@ -111,6 +191,10 @@ prompt │ YES → strip keywords from Qwen prompt (keeps `is_transparent` flag) │ set negative: "deformed anatomy, watermark, logo" │ + ├─ Canvas Expansion / Outpainting requested? + │ YES → if outpaint=True, append/use natural outpainting instructions: + │ "Naturally outpaint and extend the borders of the image to complete the scene." + │ ├─ Upload reference image(s) to ComfyUI /upload/image │ image1 = source image │ image2 = wireframe pose guide frame (optional) @@ -229,6 +313,8 @@ Model: `buffalo_l` | source_refs | TEXT (JSON) | Original filenames this was derived from | | content_type | TEXT | `image` or `video` | | faceswap_source_video | TEXT | Source video for faceswapped clips | +| face_embedding | `vector(512)` | Face recognition embedding for visually matching character faces | +| is_source | `BOOLEAN` | Whether the file is a primary reference/source asset | --- @@ -239,17 +325,33 @@ Single-page application (~5000 lines). No build step — pure HTML/CSS/JS. **Modes**: - **Gallery** — grid of all groups; click to open Studio - **Studio** — filmstrip + large viewer + sidebar tabs +- **Database Consistency** (`trash.html` SPA) — dedicated panel for identifying legacy/orphaned records, untracked files, and manual restore/delete of archived assets **Sidebar tabs**: | Tab | Purpose | |-----|---------| | Generate | Camera angle picker, pose selector (with inline edit/add/delete → `poses.md`), wireframe guide, auto-growing prompt override with history autocomplete, fine-tune textareas, multi-ref thumbnails (click to deselect), batch submit | -| Info | Metadata, preferred toggle, face-book thumbnail, hide/archive/delete, manual crop (copy or in-place), rotate ±90°, No-BG / Invert-α, duplicate, Pose preview (draggable + similar-pose search) | +| Info | Metadata, preferred toggle, face-book thumbnail, hide/archive/delete, manual crop/expand padding (copy or in-place), rotate ±90°, No-BG / Invert-α, duplicate, Pose preview (draggable + similar-pose search) | | Faceswap | Source face selector, target video, preview quality toggle | | Segment | SAM2 and rembg BG removal buttons | | Scenery | Scenery video reference for generation | +### Key Studio Features + +#### Canvas Expansion & Manual Padding (Interactive Tooling) +The **Info** sidebar and dedicated toolbar provide professional, reactive manual padding controls: +- **Percent/Pixel Harmony**: Seamlessly accepts both absolute integer pixel values (e.g., `100px`) and percentage-based inputs (e.g., `10%` or `0.1`), ensuring perfect formatting and suffix preservation. +- **Quick Adjustment Presets**: Rapidly adjust margins with single-click actions for `+10%`, `+20%`, `+50%`, `+100px`, or `Reset` to clear all. +- **Uniform Locking**: Enabling the **Uniform** option synchronizes changes on any single padding field to all four sides simultaneously. +- **Intelligent Previews**: Dynamically renders real-time color-coded canvas border previews based on the selected fill type (Amber dashed for `transparent`, translucent black for `black`, and translucent white for `white`). +- **Shortcuts**: Confirm cropping or padding instantly with `Enter`, or dismiss/cancel the operation using `Escape`. + +#### Reactive Layout Optimization +To prevent desyncs during image transitions, crops, canvas expansion, or 2.2x zooming: +- A `ResizeObserver` is registered on the primary studio viewer image (`lbImg`). +- Automatically triggers a layout and geometry recalculation, cleanly and immediately adjusting the background transparency checkerboard grid and padding visual overlays to match the actual visual boundaries of the contained image. + **Key state variables**: - `lbCurrentGid` — active group ID - `lbIdx` — current filmstrip position @@ -304,15 +406,16 @@ skeleton). Results render as a thumbnail strip; clicking one opens that image's ``` qwen-image-edit-rapid-aio-nsfw-v23/ ├── tour-comfy/ -│ ├── car.html # frontend SPA +│ ├── car.html # frontend SPA (Studio Interface) +│ ├── trash.html # frontend Database Consistency Panel SPA │ ├── edit_api.py # FastAPI backend │ ├── database.py # PostgreSQL helpers │ ├── watcher.py # output dir auto-processor │ ├── config.json # output_dir, comfy_url, etc. │ └── workflow_qwen_edit.json # ComfyUI graph template ├── a6000-comfy/ # ComfyUI install (symlinked to ~/comfyui) -├── AGENTS.md # this file -├── architecture2.svg # system architecture diagram +├── AGENTS.md # this file (System Documentation) +├── ARCHETECTURE.svg # system architecture diagram ├── backlog.md # feature backlog └── requirements.txt # Python deps ``` diff --git a/backlog.md b/backlog.md index a9c753a..3921cfe 100644 --- a/backlog.md +++ b/backlog.md @@ -61,7 +61,7 @@ Scenery: - allow re-ordering of the images in the orbit generated frames - sometimes the UI renders two orbits in the same orbitFullViewer, must be a timer not being cleared in the ui - The UI wants to car.html:2817 GET file:///mnt/zim/tour-comfy/output/_turntable/up_34e48ffb/turntable.jpg?t=1782562827857 net::ERR_FILE_NOT_FOUND, often want to show the wireframe - pose -- Jun 27 14:38:03 MIKE bash[3607206]: INFO: 127.0.0.1:53442 - "GET /output/_turntable/up_a1104f04/views/view_023_345deg.png HTTP/1.1" 304 Not Modified those files should be statically served from filesystem, not via server +- ✅ Fixed: statically serve turntable and orbit preview files from filesystem, not via server (avoid GET /output/... 304 Not Modified log spam) - Check face similarity in a group of images. - implement a tag-system for images, will be used to filter, group, sort and search images. Obvious tags are VISIBLE, ARCHIVED, LIKE, DISLIKE, RATED, GROUP_ANCHOR, GROUP_MEMBER, BACKGROUND etc.. - mark image as source diff --git a/tour-comfy/car.html b/tour-comfy/car.html index 9048f31..3d9b667 100644 --- a/tour-comfy/car.html +++ b/tour-comfy/car.html @@ -2227,6 +2227,26 @@ // --- HYDRATION_START --- const PRELOADED_IMAGES = [ + "_turntable/up_6ed4293c/views/view_019_285deg.png", + "_turntable/up_6ed4293c/views/view_018_270deg.png", + "_turntable/up_6ed4293c/views/view_017_255deg.png", + "_turntable/up_6ed4293c/views/view_016_240deg.png", + "_turntable/up_6ed4293c/views/view_015_225deg.png", + "_turntable/up_6ed4293c/views/view_014_210deg.png", + "_turntable/up_6ed4293c/views/view_013_195deg.png", + "_turntable/up_6ed4293c/views/view_012_180deg.png", + "_turntable/up_6ed4293c/views/view_011_165deg.png", + "_turntable/up_6ed4293c/views/view_010_150deg.png", + "_turntable/up_6ed4293c/views/view_009_135deg.png", + "_turntable/up_6ed4293c/views/view_008_120deg.png", + "_turntable/up_6ed4293c/views/view_007_105deg.png", + "_turntable/up_6ed4293c/views/view_006_090deg.png", + "_turntable/up_6ed4293c/views/view_005_075deg.png", + "_turntable/up_6ed4293c/views/view_004_060deg.png", + "_turntable/up_6ed4293c/views/view_003_045deg.png", + "_turntable/up_6ed4293c/views/view_002_030deg.png", + "_turntable/up_6ed4293c/views/view_001_015deg.png", + "_turntable/up_6ed4293c/views/view_000_000deg.png", "20260628_001216_pad_20260626_035101_pad_20260626_034830_jb.nobg.png", "20260627_232502_pad_20260626_035101_pad_20260626_034830_jb.nobg.png", "20260627_232843_pad_20260626_035101_pad_20260626_034830_jb.nobg.png", @@ -2294,7 +2314,6 @@ "20260627_213202_9_20260627_212803_best_01_person_0.874_2026-06-27_20-05-05.jpg.png", "20260627_213149_8_20260627_212803_best_01_person_0.874_2026-06-27_20-05-05.jpg.png", "_turntable/cg_74544975/views/view_004_060deg.png", - "_turntable/up_6ed4293c/views/view_019_285deg.png", "_turntable/cg_74544975/views/view_001_015deg.png", "_turntable/cg_74544975/views/view_000_000deg.png", "20260627_213102_12_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png", @@ -2326,25 +2345,6 @@ "20260627_212723_1_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png", "20260627_212710_0_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png", "20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png", - "_turntable/up_6ed4293c/views/view_018_270deg.png", - "_turntable/up_6ed4293c/views/view_017_255deg.png", - "_turntable/up_6ed4293c/views/view_016_240deg.png", - "_turntable/up_6ed4293c/views/view_015_225deg.png", - "_turntable/up_6ed4293c/views/view_014_210deg.png", - "_turntable/up_6ed4293c/views/view_013_195deg.png", - "_turntable/up_6ed4293c/views/view_012_180deg.png", - "_turntable/up_6ed4293c/views/view_011_165deg.png", - "_turntable/up_6ed4293c/views/view_010_150deg.png", - "_turntable/up_6ed4293c/views/view_009_135deg.png", - "_turntable/up_6ed4293c/views/view_008_120deg.png", - "_turntable/up_6ed4293c/views/view_007_105deg.png", - "_turntable/up_6ed4293c/views/view_006_090deg.png", - "_turntable/up_6ed4293c/views/view_005_075deg.png", - "_turntable/up_6ed4293c/views/view_004_060deg.png", - "_turntable/up_6ed4293c/views/view_003_045deg.png", - "_turntable/up_6ed4293c/views/view_002_030deg.png", - "_turntable/up_6ed4293c/views/view_001_015deg.png", - "_turntable/up_6ed4293c/views/view_000_000deg.png", "20260627_204815_image.png", "20260627_204804_image.png", "20260627_204756_image.png", @@ -6732,6 +6732,7 @@ let _sbPadBottom = 0; let _sbPadLeft = 0; let _sbPadOutpaint = false; + let _sbPadUniform = false; const CAMERA_ANGLES = [ // Absolute camera positions @@ -6890,33 +6891,126 @@ } function formatPadValue(v) { - if (v === undefined || v === null || v === '') return '0.00'; - let val = parseFloat(v); - if (isNaN(val)) return '0.00'; - const s = v.toString(); - // User requested: show 0.00 if it contains a decimal see it as percentage. - // if integer and its greater than 10 its pixel value - if (s.includes('.') || s.includes('%')) return val.toFixed(2); - if (val > 10) return val.toString(); - return val.toFixed(2); + if (v === undefined || v === null || v === '') return '0.00%'; + const s = v.toString().trim().toLowerCase(); + let val = parseFloat(s); + if (isNaN(val)) return '0.00%'; + + // If the user explicitly typed px + if (s.endsWith('px')) { + return val.toString(); // integer pixels + } + // If the user explicitly typed % + if (s.endsWith('%')) { + return val.toFixed(2) + '%'; + } + // If contains a decimal, or if it's <= 10 (or < 1.0), it's percentage + if (s.includes('.') || val <= 10) { + // If it's a fraction like 0.1, convert to 10% + if (val > 0 && val < 1.0) { + return (val * 100).toFixed(2) + '%'; + } + return val.toFixed(2) + '%'; + } + // Otherwise, it's integer > 10, treat as pixels + return val.toString(); } function syncPadFields(el) { - const id = el.id; // padLeft or sbPadLeft - let pairId = null; - if (id.endsWith('Left')) pairId = id.replace('Left', 'Right'); - else if (id.endsWith('Right')) pairId = id.replace('Right', 'Left'); - else if (id.endsWith('Top')) pairId = id.replace('Top', 'Bottom'); - else if (id.endsWith('Bottom')) pairId = id.replace('Bottom', 'Top'); + const id = el.id; // padLeft, padTop, etc. or sbPadLeft, sbPadTop + const val = el.value; + const prefix = id.startsWith('sbPad') ? 'sbPad' : 'pad'; + const isSb = id.startsWith('sbPad'); - if (pairId) { - const pairEl = document.getElementById(pairId); - // only sync if the other side is 'empty' (0) - if (pairEl && (pairEl.value === '0.00' || pairEl.value === '0' || pairEl.value === '')) { - pairEl.value = el.value; + // Check if Uniform mode is active for this context + const uniformEl = document.getElementById(isSb ? 'sbPadUniform' : 'padUniform'); + const isUniform = uniformEl ? uniformEl.checked : false; + + if (isUniform) { + // Set all four sides to the same value + ['Top', 'Right', 'Bottom', 'Left'].forEach(side => { + const targetId = prefix + side; + const targetEl = document.getElementById(targetId); + if (targetEl && targetEl !== el) { + targetEl.value = val; + } + }); + } else { + // Standard opposite-side sync if other side is 0 + let pairId = null; + if (id.endsWith('Left')) pairId = id.replace('Left', 'Right'); + else if (id.endsWith('Right')) pairId = id.replace('Right', 'Left'); + else if (id.endsWith('Top')) pairId = id.replace('Top', 'Bottom'); + else if (id.endsWith('Bottom')) pairId = id.replace('Bottom', 'Top'); + + if (pairId) { + const pairEl = document.getElementById(pairId); + if (pairEl && (pairEl.value === '0.00' || pairEl.value === '0' || pairEl.value === '0.00%' || pairEl.value === '')) { + pairEl.value = val; + } } } - if (id.startsWith('sbPad')) { + if (isSb) { + updateSbGenBtn(); + } + updatePadPreview(); + } + + function togglePadUniform(cb) { + const isSb = cb.id.startsWith('sbPad'); + const prefix = isSb ? 'sbPad' : 'pad'; + if (isSb) _sbPadUniform = cb.checked; + if (cb.checked) { + // Find a non-zero value, or default to Top, and propagate + const sides = ['Top', 'Right', 'Bottom', 'Left']; + let val = '0.00%'; + for (let side of sides) { + const el = document.getElementById(prefix + side); + if (el && el.value && el.value !== '0' && el.value !== '0.00' && el.value !== '0.00%') { + val = el.value; + break; + } + } + sides.forEach(side => { + const el = document.getElementById(prefix + side); + if (el) el.value = val; + }); + updatePadPreview(); + } + } + + function applyPadPreset(presetVal, isSb) { + const prefix = isSb ? 'sbPad' : 'pad'; + const sides = ['Top', 'Right', 'Bottom', 'Left']; + + if (presetVal === '0') { + // Reset all + sides.forEach(side => { + const el = document.getElementById(prefix + side); + if (el) el.value = '0.00%'; + }); + const uniformEl = document.getElementById(isSb ? 'sbPadUniform' : 'padUniform'); + if (uniformEl) uniformEl.checked = false; + if (isSb) _sbPadUniform = false; + } else { + // If it ends with % + let formatted = presetVal; + if (presetVal.endsWith('%')) { + formatted = parseFloat(presetVal).toFixed(2) + '%'; + } else if (presetVal.endsWith('px')) { + formatted = parseFloat(presetVal).toString(); + } + + sides.forEach(side => { + const el = document.getElementById(prefix + side); + if (el) el.value = formatted; + }); + + const uniformEl = document.getElementById(isSb ? 'sbPadUniform' : 'padUniform'); + if (uniformEl) uniformEl.checked = true; + if (isSb) _sbPadUniform = true; + } + if (isSb) { updateSbGenBtn(); } updatePadPreview(); @@ -7000,12 +7094,13 @@ function updatePadPreview() { const viewer = document.getElementById('studioViewer'); + if (viewer) { + // Clean up existing preview overlays + viewer.querySelectorAll('.pad-preview-overlay').forEach(el => el.remove()); + } const img = document.getElementById('lbImg'); if (!viewer || !img || img.style.display === 'none') return; - // Clean up existing preview overlays - viewer.querySelectorAll('.pad-preview-overlay').forEach(el => el.remove()); - const rect = getImageDisplayRect(img); if (!rect) return; @@ -7015,17 +7110,37 @@ return el ? el.value : '0'; }; + const fill = document.getElementById('padFill')?.value || 'transparent'; + const sides = ['Left', 'Right', 'Top', 'Bottom']; sides.forEach(side => { const vStr = getV(side); - if (!vStr || vStr === '0' || vStr === '0.00') return; + if (!vStr || vStr === '0' || vStr === '0.00' || vStr === '0.00%') return; const val = parseFloat(vStr); if (isNaN(val) || val <= 0) return; - const isPercent = vStr.includes('%') || (vStr.indexOf('.') !== -1 && !isNaN(vStr)); + const isPercent = vStr.includes('%'); const overlay = document.createElement('div'); overlay.className = 'pad-preview-overlay'; + + // Style based on selected fill mode to make preview professional & intuitive + if (isManual) { + if (fill === 'black') { + overlay.style.background = 'rgba(0, 0, 0, 0.7)'; + overlay.style.borderColor = '#444'; + } else if (fill === 'white') { + overlay.style.background = 'rgba(255, 255, 255, 0.7)'; + overlay.style.borderColor = '#ccc'; + } else { + overlay.style.background = 'rgba(245, 158, 11, 0.4)'; + overlay.style.borderColor = '#f59e0b'; + } + } else { + // Sidebar is always transparent background before outpaint + overlay.style.background = 'rgba(245, 158, 11, 0.4)'; + overlay.style.borderColor = '#f59e0b'; + } if (side === 'Left' || side === 'Right') { const w = isPercent ? (rect.width * val / 100) : val; @@ -7173,7 +7288,12 @@ const dimEl = document.getElementById('lbImgDims'); if (dimEl) dimEl.textContent = `${lbImgEl.naturalWidth} x ${lbImgEl.naturalHeight}`; - // Overlay re-sync is now handled by ResizeObserver + // Force layout recalculation and overlay sync when a new image loads + requestAnimationFrame(() => { + updatePadPreview(); + const cb = document.getElementById('sbCheckerboard'); + if (cb && cb.checked) toggleCheckerboard(true); + }); }; _fsModelFilename = fname; // keep faceswap/scenery/segment in sync @@ -7194,6 +7314,10 @@ lbVideoEl.style.display = ''; lbVideoEl.poster = posterFor(lbUrls[lbIdx]); lbVideoEl.src = lbUrls[lbIdx]; + + // Explicitly clean up pad previews and hide checkerboard when navigating to a video + updatePadPreview(); + toggleCheckerboard(false); } else { lbVideoEl.style.display = 'none'; lbVideoEl.src = ''; @@ -7981,7 +8105,7 @@ const timeLabel = document.getElementById('pvmTimeLabel'); function pvmSetVideo(filename) { - vidEl.src = `${API}/output/${encodeURIComponent(filename)}`; + vidEl.src = IMAGE_FOLDER + filename; vidEl.load(); vidEl.addEventListener('loadedmetadata', () => { const dur = vidEl.duration || 10; @@ -8316,15 +8440,24 @@ const numInput = (id, label) => { const onInput = `syncPadFields(this)`; const onBlur = `this.value = formatPadValue(this.value); updatePadPreview()`; - return ``; + const onKeyDown = `if(event.key === 'Enter') { confirmManualPad(); event.preventDefault(); } if(event.key === 'Escape') { cancelManualPad(); event.preventDefault(); }`; + return ``; }; bar.innerHTML = - `Expand canvas (px or %):` + `Preset:` + + `` + + `` + + `` + + `` + + `` + + `Custom:` + numInput('padTop','↑') + numInput('padRight','→') + numInput('padBottom','↓') + numInput('padLeft','←') - + `Uniform` + + ``; + oninput="${onInput}" onblur="${onBlur}" + onkeydown="if(event.key === 'Enter') { submitSbGenerate(); event.preventDefault(); }">`; }).join('')} + + + + + diff --git a/tour-comfy/edit_api.py b/tour-comfy/edit_api.py index 0484eeb..649ef5c 100644 --- a/tour-comfy/edit_api.py +++ b/tour-comfy/edit_api.py @@ -3496,6 +3496,7 @@ def autocrop_image(filename: str): cmin, cmax = np.where(cols)[0][[0, -1]] cropped = img.crop((cmin, rmin, cmax + 1, rmax + 1)) cropped.save(path, format="PNG") + _invalidate_static() return {"status": "success", "filename": filename, "box": [int(cmin), int(rmin), int(cmax+1), int(rmax+1)]} @@ -3558,8 +3559,7 @@ def manual_crop_image(filename: str, req: CropRequest): cropped = img.crop((x1, y1, x2, y2)) fmt = "PNG" if path.lower().endswith(".png") else "JPEG" cropped.save(path, format=fmt) - if req.as_copy: - _invalidate_static() + _invalidate_static() return {"status": "success", "filename": filename, "new_filename": new_filename, "new_url": f"/output/{new_filename}", "as_copy": req.as_copy, "box": [x1, y1, x2, y2]} @@ -3679,8 +3679,7 @@ def pad_image(filename: str, req: PadRequest): if req.fill == "transparent": fmt = "PNG" # JPEG cannot store alpha padded.save(path, format=fmt) - if req.as_copy: - _invalidate_static() + _invalidate_static() return { "status": "success", "filename": filename, "new_filename": new_filename, "new_url": f"/output/{new_filename}", "as_copy": req.as_copy, @@ -3712,6 +3711,7 @@ def rotate_image(filename: str, req: RotateRequest): img = Image.open(path).transpose(cw_to_transpose[deg]) fmt = "PNG" if path.lower().endswith(".png") else "JPEG" img.save(path, format=fmt) + _invalidate_static() return {"status": "success", "filename": filename, "degrees": deg} diff --git a/tour-comfy/trash.html b/tour-comfy/trash.html index ca310e4..34ec66e 100644 --- a/tour-comfy/trash.html +++ b/tour-comfy/trash.html @@ -101,7 +101,10 @@