reorder
This commit is contained in:
159
AGENTS.md
159
AGENTS.md
@@ -63,42 +63,122 @@ FastAPI :8500 (edit_api.py)
|
|||||||
- **Systemd**: `comfyui-api`
|
- **Systemd**: `comfyui-api`
|
||||||
- **DB**: PostgreSQL at `192.168.1.160:5433`, database `dv`, table `person`
|
- **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 |
|
| Method | Path | Purpose |
|
||||||
|--------|------|---------|
|
|--------|------|---------|
|
||||||
| `GET` | `/images` | List all images with DB metadata |
|
| `GET` | `/images` | List all images with DB metadata |
|
||||||
| `GET` | `/images/{filename}` | Serve image file |
|
| `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` | `/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` | `/names` | Map `filename → person name` |
|
||||||
| `GET/POST` | `/config` | Read / write `config.json` |
|
| `POST` | `/names/{filename}` | Map image file to custom character name |
|
||||||
| `GET` | `/poses` | Load pose library from `poses.md` |
|
|
||||||
| `POST` | `/poses` | Create / update / rename a pose (`old_name` to rename) → rewrites `poses.md` |
|
##### Visibility, Archiving & Deletion
|
||||||
| `DELETE` | `/poses/{name}` | Delete a pose from `poses.md` |
|
| Method | Path | Purpose |
|
||||||
| `GET` | `/pose/check` | Report pose-estimator availability + backend |
|
|--------|------|---------|
|
||||||
|
| `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) |
|
| `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 |
|
| `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/similar` | Rank library images by similarity to a supplied (edited) skeleton |
|
||||||
| `POST` | `/pose/index` | Build pose descriptors for the whole library (daemon thread) |
|
| `POST` | `/pose/index` | Build pose descriptors for the whole library (daemon thread) |
|
||||||
| `GET` | `/pose/index/status` | Poll pose-index build progress |
|
| `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)
|
│ YES → strip keywords from Qwen prompt (keeps `is_transparent` flag)
|
||||||
│ set negative: "deformed anatomy, watermark, logo"
|
│ 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
|
├─ Upload reference image(s) to ComfyUI /upload/image
|
||||||
│ image1 = source image
|
│ image1 = source image
|
||||||
│ image2 = wireframe pose guide frame (optional)
|
│ image2 = wireframe pose guide frame (optional)
|
||||||
@@ -229,6 +313,8 @@ Model: `buffalo_l`
|
|||||||
| source_refs | TEXT (JSON) | Original filenames this was derived from |
|
| source_refs | TEXT (JSON) | Original filenames this was derived from |
|
||||||
| content_type | TEXT | `image` or `video` |
|
| content_type | TEXT | `image` or `video` |
|
||||||
| faceswap_source_video | TEXT | Source video for faceswapped clips |
|
| 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**:
|
**Modes**:
|
||||||
- **Gallery** — grid of all groups; click to open Studio
|
- **Gallery** — grid of all groups; click to open Studio
|
||||||
- **Studio** — filmstrip + large viewer + sidebar tabs
|
- **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**:
|
**Sidebar tabs**:
|
||||||
|
|
||||||
| Tab | Purpose |
|
| 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 |
|
| 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 |
|
| Faceswap | Source face selector, target video, preview quality toggle |
|
||||||
| Segment | SAM2 and rembg BG removal buttons |
|
| Segment | SAM2 and rembg BG removal buttons |
|
||||||
| Scenery | Scenery video reference for generation |
|
| 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**:
|
**Key state variables**:
|
||||||
- `lbCurrentGid` — active group ID
|
- `lbCurrentGid` — active group ID
|
||||||
- `lbIdx` — current filmstrip position
|
- `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/
|
qwen-image-edit-rapid-aio-nsfw-v23/
|
||||||
├── tour-comfy/
|
├── tour-comfy/
|
||||||
│ ├── car.html # frontend SPA
|
│ ├── car.html # frontend SPA (Studio Interface)
|
||||||
|
│ ├── trash.html # frontend Database Consistency Panel SPA
|
||||||
│ ├── edit_api.py # FastAPI backend
|
│ ├── edit_api.py # FastAPI backend
|
||||||
│ ├── database.py # PostgreSQL helpers
|
│ ├── database.py # PostgreSQL helpers
|
||||||
│ ├── watcher.py # output dir auto-processor
|
│ ├── watcher.py # output dir auto-processor
|
||||||
│ ├── config.json # output_dir, comfy_url, etc.
|
│ ├── config.json # output_dir, comfy_url, etc.
|
||||||
│ └── workflow_qwen_edit.json # ComfyUI graph template
|
│ └── workflow_qwen_edit.json # ComfyUI graph template
|
||||||
├── a6000-comfy/ # ComfyUI install (symlinked to ~/comfyui)
|
├── a6000-comfy/ # ComfyUI install (symlinked to ~/comfyui)
|
||||||
├── AGENTS.md # this file
|
├── AGENTS.md # this file (System Documentation)
|
||||||
├── architecture2.svg # system architecture diagram
|
├── ARCHETECTURE.svg # system architecture diagram
|
||||||
├── backlog.md # feature backlog
|
├── backlog.md # feature backlog
|
||||||
└── requirements.txt # Python deps
|
└── requirements.txt # Python deps
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ Scenery:
|
|||||||
- allow re-ordering of the images in the orbit generated frames
|
- 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
|
- 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
|
- 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.
|
- 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..
|
- 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
|
- mark image as source
|
||||||
|
|||||||
@@ -2227,6 +2227,26 @@
|
|||||||
|
|
||||||
// --- HYDRATION_START ---
|
// --- HYDRATION_START ---
|
||||||
const PRELOADED_IMAGES = [
|
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",
|
"20260628_001216_pad_20260626_035101_pad_20260626_034830_jb.nobg.png",
|
||||||
"20260627_232502_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",
|
"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_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",
|
"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/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_001_015deg.png",
|
||||||
"_turntable/cg_74544975/views/view_000_000deg.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",
|
"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_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_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",
|
"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_204815_image.png",
|
||||||
"20260627_204804_image.png",
|
"20260627_204804_image.png",
|
||||||
"20260627_204756_image.png",
|
"20260627_204756_image.png",
|
||||||
@@ -6732,6 +6732,7 @@
|
|||||||
let _sbPadBottom = 0;
|
let _sbPadBottom = 0;
|
||||||
let _sbPadLeft = 0;
|
let _sbPadLeft = 0;
|
||||||
let _sbPadOutpaint = false;
|
let _sbPadOutpaint = false;
|
||||||
|
let _sbPadUniform = false;
|
||||||
|
|
||||||
const CAMERA_ANGLES = [
|
const CAMERA_ANGLES = [
|
||||||
// Absolute camera positions
|
// Absolute camera positions
|
||||||
@@ -6890,33 +6891,126 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatPadValue(v) {
|
function formatPadValue(v) {
|
||||||
if (v === undefined || v === null || v === '') return '0.00';
|
if (v === undefined || v === null || v === '') return '0.00%';
|
||||||
let val = parseFloat(v);
|
const s = v.toString().trim().toLowerCase();
|
||||||
if (isNaN(val)) return '0.00';
|
let val = parseFloat(s);
|
||||||
const s = v.toString();
|
if (isNaN(val)) return '0.00%';
|
||||||
// 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 the user explicitly typed px
|
||||||
if (s.includes('.') || s.includes('%')) return val.toFixed(2);
|
if (s.endsWith('px')) {
|
||||||
if (val > 10) return val.toString();
|
return val.toString(); // integer pixels
|
||||||
return val.toFixed(2);
|
}
|
||||||
|
// 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) {
|
function syncPadFields(el) {
|
||||||
const id = el.id; // padLeft or sbPadLeft
|
const id = el.id; // padLeft, padTop, etc. or sbPadLeft, sbPadTop
|
||||||
let pairId = null;
|
const val = el.value;
|
||||||
if (id.endsWith('Left')) pairId = id.replace('Left', 'Right');
|
const prefix = id.startsWith('sbPad') ? 'sbPad' : 'pad';
|
||||||
else if (id.endsWith('Right')) pairId = id.replace('Right', 'Left');
|
const isSb = id.startsWith('sbPad');
|
||||||
else if (id.endsWith('Top')) pairId = id.replace('Top', 'Bottom');
|
|
||||||
else if (id.endsWith('Bottom')) pairId = id.replace('Bottom', 'Top');
|
|
||||||
|
|
||||||
if (pairId) {
|
// Check if Uniform mode is active for this context
|
||||||
const pairEl = document.getElementById(pairId);
|
const uniformEl = document.getElementById(isSb ? 'sbPadUniform' : 'padUniform');
|
||||||
// only sync if the other side is 'empty' (0)
|
const isUniform = uniformEl ? uniformEl.checked : false;
|
||||||
if (pairEl && (pairEl.value === '0.00' || pairEl.value === '0' || pairEl.value === '')) {
|
|
||||||
pairEl.value = el.value;
|
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();
|
updateSbGenBtn();
|
||||||
}
|
}
|
||||||
updatePadPreview();
|
updatePadPreview();
|
||||||
@@ -7000,12 +7094,13 @@
|
|||||||
|
|
||||||
function updatePadPreview() {
|
function updatePadPreview() {
|
||||||
const viewer = document.getElementById('studioViewer');
|
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');
|
const img = document.getElementById('lbImg');
|
||||||
if (!viewer || !img || img.style.display === 'none') return;
|
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);
|
const rect = getImageDisplayRect(img);
|
||||||
if (!rect) return;
|
if (!rect) return;
|
||||||
|
|
||||||
@@ -7015,17 +7110,37 @@
|
|||||||
return el ? el.value : '0';
|
return el ? el.value : '0';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fill = document.getElementById('padFill')?.value || 'transparent';
|
||||||
|
|
||||||
const sides = ['Left', 'Right', 'Top', 'Bottom'];
|
const sides = ['Left', 'Right', 'Top', 'Bottom'];
|
||||||
sides.forEach(side => {
|
sides.forEach(side => {
|
||||||
const vStr = getV(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);
|
const val = parseFloat(vStr);
|
||||||
if (isNaN(val) || val <= 0) return;
|
if (isNaN(val) || val <= 0) return;
|
||||||
|
|
||||||
const isPercent = vStr.includes('%') || (vStr.indexOf('.') !== -1 && !isNaN(vStr));
|
const isPercent = vStr.includes('%');
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
overlay.className = 'pad-preview-overlay';
|
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') {
|
if (side === 'Left' || side === 'Right') {
|
||||||
const w = isPercent ? (rect.width * val / 100) : val;
|
const w = isPercent ? (rect.width * val / 100) : val;
|
||||||
@@ -7173,7 +7288,12 @@
|
|||||||
const dimEl = document.getElementById('lbImgDims');
|
const dimEl = document.getElementById('lbImgDims');
|
||||||
if (dimEl) dimEl.textContent = `${lbImgEl.naturalWidth} x ${lbImgEl.naturalHeight}`;
|
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
|
_fsModelFilename = fname; // keep faceswap/scenery/segment in sync
|
||||||
@@ -7194,6 +7314,10 @@
|
|||||||
lbVideoEl.style.display = '';
|
lbVideoEl.style.display = '';
|
||||||
lbVideoEl.poster = posterFor(lbUrls[lbIdx]);
|
lbVideoEl.poster = posterFor(lbUrls[lbIdx]);
|
||||||
lbVideoEl.src = lbUrls[lbIdx];
|
lbVideoEl.src = lbUrls[lbIdx];
|
||||||
|
|
||||||
|
// Explicitly clean up pad previews and hide checkerboard when navigating to a video
|
||||||
|
updatePadPreview();
|
||||||
|
toggleCheckerboard(false);
|
||||||
} else {
|
} else {
|
||||||
lbVideoEl.style.display = 'none';
|
lbVideoEl.style.display = 'none';
|
||||||
lbVideoEl.src = '';
|
lbVideoEl.src = '';
|
||||||
@@ -7981,7 +8105,7 @@
|
|||||||
const timeLabel = document.getElementById('pvmTimeLabel');
|
const timeLabel = document.getElementById('pvmTimeLabel');
|
||||||
|
|
||||||
function pvmSetVideo(filename) {
|
function pvmSetVideo(filename) {
|
||||||
vidEl.src = `${API}/output/${encodeURIComponent(filename)}`;
|
vidEl.src = IMAGE_FOLDER + filename;
|
||||||
vidEl.load();
|
vidEl.load();
|
||||||
vidEl.addEventListener('loadedmetadata', () => {
|
vidEl.addEventListener('loadedmetadata', () => {
|
||||||
const dur = vidEl.duration || 10;
|
const dur = vidEl.duration || 10;
|
||||||
@@ -8316,15 +8440,24 @@
|
|||||||
const numInput = (id, label) => {
|
const numInput = (id, label) => {
|
||||||
const onInput = `syncPadFields(this)`;
|
const onInput = `syncPadFields(this)`;
|
||||||
const onBlur = `this.value = formatPadValue(this.value); updatePadPreview()`;
|
const onBlur = `this.value = formatPadValue(this.value); updatePadPreview()`;
|
||||||
return `<label style="display:flex;align-items:center;gap:3px;font-size:11px;color:#ccc">${label}<input type="text" id="${id}" value="0.00" oninput="${onInput}" onblur="${onBlur}" style="width:52px;background:#111;border:1px solid #333;color:#ccc;border-radius:4px;padding:2px 4px;font-size:11px;text-align:right" placeholder="px or %"></label>`;
|
const onKeyDown = `if(event.key === 'Enter') { confirmManualPad(); event.preventDefault(); } if(event.key === 'Escape') { cancelManualPad(); event.preventDefault(); }`;
|
||||||
|
return `<label style="display:flex;align-items:center;gap:3px;font-size:11px;color:#ccc">${label}<input type="text" id="${id}" value="0.00%" oninput="${onInput}" onblur="${onBlur}" onkeydown="${onKeyDown}" style="width:52px;background:#111;border:1px solid #333;color:#ccc;border-radius:4px;padding:2px 4px;font-size:11px;text-align:right" placeholder="px or %"></label>`;
|
||||||
};
|
};
|
||||||
bar.innerHTML =
|
bar.innerHTML =
|
||||||
`<span style="flex:1;font-size:11px;color:#aaa">Expand canvas (px or %):</span>`
|
`<span style="font-size:11px;color:#aaa">Preset:</span>`
|
||||||
|
+ `<button class="sb-btn" style="padding:2px 6px;font-size:10px;margin-right:2px;" onclick="applyPadPreset('10%', false)">+10%</button>`
|
||||||
|
+ `<button class="sb-btn" style="padding:2px 6px;font-size:10px;margin-right:2px;" onclick="applyPadPreset('20%', false)">+20%</button>`
|
||||||
|
+ `<button class="sb-btn" style="padding:2px 6px;font-size:10px;margin-right:2px;" onclick="applyPadPreset('50%', false)">+50%</button>`
|
||||||
|
+ `<button class="sb-btn" style="padding:2px 6px;font-size:10px;margin-right:4px;" onclick="applyPadPreset('100px', false)">+100px</button>`
|
||||||
|
+ `<button class="sb-btn" style="padding:2px 6px;font-size:10px;margin-right:8px;background:#3b0764;color:#d8b4fe;border-color:#581c87" onclick="applyPadPreset('0', false)">Reset</button>`
|
||||||
|
+ `<span style="font-size:11px;color:#aaa;margin-right:4px;">Custom:</span>`
|
||||||
+ numInput('padTop','↑')
|
+ numInput('padTop','↑')
|
||||||
+ numInput('padRight','→')
|
+ numInput('padRight','→')
|
||||||
+ numInput('padBottom','↓')
|
+ numInput('padBottom','↓')
|
||||||
+ numInput('padLeft','←')
|
+ numInput('padLeft','←')
|
||||||
+ `<select id="padFill" style="background:#111;border:1px solid #333;color:#aaa;border-radius:4px;font-size:11px;padding:2px 5px">
|
+ `<label style="display:flex;align-items:center;gap:3px;font-size:11px;color:#ccc;cursor:pointer" title="Keep all padding values equal">`
|
||||||
|
+ `<input type="checkbox" id="padUniform" style="cursor:pointer" onchange="togglePadUniform(this)">Uniform</label>`
|
||||||
|
+ `<select id="padFill" onchange="updatePadPreview()" style="background:#111;border:1px solid #333;color:#aaa;border-radius:4px;font-size:11px;padding:2px 5px">
|
||||||
<option value="transparent">Transparent</option>
|
<option value="transparent">Transparent</option>
|
||||||
<option value="black">Black</option>
|
<option value="black">Black</option>
|
||||||
<option value="white">White</option>
|
<option value="white">White</option>
|
||||||
@@ -8717,7 +8850,7 @@
|
|||||||
} else if (jd.video_filename) {
|
} else if (jd.video_filename) {
|
||||||
// Depth-card MP4 fallback
|
// Depth-card MP4 fallback
|
||||||
const mp4 = jd.video_filename;
|
const mp4 = jd.video_filename;
|
||||||
const src = mp4.includes('/') ? `${API}/output/${mp4}` : IMAGE_FOLDER + mp4;
|
const src = IMAGE_FOLDER + mp4;
|
||||||
const vid = document.getElementById('orbitVideo');
|
const vid = document.getElementById('orbitVideo');
|
||||||
vid.src = src + '?t=' + Date.now();
|
vid.src = src + '?t=' + Date.now();
|
||||||
vid.style.display = '';
|
vid.style.display = '';
|
||||||
@@ -8735,7 +8868,7 @@
|
|||||||
// ---- Frame flipper engine ----
|
// ---- Frame flipper engine ----
|
||||||
function _orbitStartFlipper(frameRelPaths) {
|
function _orbitStartFlipper(frameRelPaths) {
|
||||||
_orbitFlipperStop();
|
_orbitFlipperStop();
|
||||||
_orbitFrames = frameRelPaths.map(p => `${API}/output/${p}`);
|
_orbitFrames = frameRelPaths.map(p => IMAGE_FOLDER + p);
|
||||||
_orbitFlipperIdx = 0;
|
_orbitFlipperIdx = 0;
|
||||||
_orbitFlipperRunning = true;
|
_orbitFlipperRunning = true;
|
||||||
|
|
||||||
@@ -8765,7 +8898,7 @@
|
|||||||
_orbitFlipperStop();
|
_orbitFlipperStop();
|
||||||
document.getElementById('orbitFlipperPlayBtn').textContent = '▶ Play';
|
document.getElementById('orbitFlipperPlayBtn').textContent = '▶ Play';
|
||||||
} else {
|
} else {
|
||||||
_orbitStartFlipper(_orbitFrames.map(u => u.replace(`${API}/output/`, '')));
|
_orbitStartFlipper(_orbitFrames.map(u => u.startsWith(IMAGE_FOLDER) ? u.slice(IMAGE_FOLDER.length) : u));
|
||||||
document.getElementById('orbitFlipperPlayBtn').textContent = '⏸ Pause';
|
document.getElementById('orbitFlipperPlayBtn').textContent = '⏸ Pause';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8775,7 +8908,7 @@
|
|||||||
if (_orbitFlipperRunning && _orbitFrames.length > 0) {
|
if (_orbitFlipperRunning && _orbitFrames.length > 0) {
|
||||||
// Restart with new interval
|
// Restart with new interval
|
||||||
_orbitFlipperStop();
|
_orbitFlipperStop();
|
||||||
_orbitStartFlipper(_orbitFrames.map(u => u.replace(`${API}/output/`, '')));
|
_orbitStartFlipper(_orbitFrames.map(u => u.startsWith(IMAGE_FOLDER) ? u.slice(IMAGE_FOLDER.length) : u));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8783,9 +8916,7 @@
|
|||||||
function orbitDownload() {
|
function orbitDownload() {
|
||||||
if (!_orbitCurrentMp4) return;
|
if (!_orbitCurrentMp4) return;
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = _orbitCurrentMp4.includes('/')
|
a.href = IMAGE_FOLDER + _orbitCurrentMp4;
|
||||||
? `${API}/output/${_orbitCurrentMp4}`
|
|
||||||
: IMAGE_FOLDER + _orbitCurrentMp4;
|
|
||||||
a.download = _orbitCurrentMp4.split('/').pop();
|
a.download = _orbitCurrentMp4.split('/').pop();
|
||||||
a.click();
|
a.click();
|
||||||
}
|
}
|
||||||
@@ -8904,7 +9035,7 @@
|
|||||||
|
|
||||||
for (const t of _turntablesData) {
|
for (const t of _turntablesData) {
|
||||||
const label = t.group_name || t.group_id.slice(0, 8);
|
const label = t.group_name || t.group_id.slice(0, 8);
|
||||||
const thumb = t.frames.length > 0 ? `${API}/output/${t.frames[0]}` : '';
|
const thumb = t.frames.length > 0 ? IMAGE_FOLDER + t.frames[0] : '';
|
||||||
const compClass = t.completed ? ' complete' : '';
|
const compClass = t.completed ? ' complete' : '';
|
||||||
html += `<div class="orbit-tab-card${compClass}" onclick="_orbitTabOpen('${t.group_id}')" id="otc_${t.group_id}">
|
html += `<div class="orbit-tab-card${compClass}" onclick="_orbitTabOpen('${t.group_id}')" id="otc_${t.group_id}">
|
||||||
<div class="otc-img-wrap"><img id="otcImg_${t.group_id}" src="${thumb}" alt="${label}"></div>
|
<div class="otc-img-wrap"><img id="otcImg_${t.group_id}" src="${thumb}" alt="${label}"></div>
|
||||||
@@ -8926,7 +9057,7 @@
|
|||||||
function _orbitStartTabFlipper(groupId, frameRels) {
|
function _orbitStartTabFlipper(groupId, frameRels) {
|
||||||
const imgEl = document.getElementById(`otcImg_${groupId}`);
|
const imgEl = document.getElementById(`otcImg_${groupId}`);
|
||||||
if (!imgEl) return;
|
if (!imgEl) return;
|
||||||
const urls = frameRels.map(f => `${API}/output/${f}`);
|
const urls = frameRels.map(f => IMAGE_FOLDER + f);
|
||||||
let idx = 0;
|
let idx = 0;
|
||||||
if (_orbitTabFlippers[groupId]) clearInterval(_orbitTabFlippers[groupId].timer);
|
if (_orbitTabFlippers[groupId]) clearInterval(_orbitTabFlippers[groupId].timer);
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
@@ -8947,7 +9078,7 @@
|
|||||||
// Stop any existing full-player
|
// Stop any existing full-player
|
||||||
if (viewer._flipperTimer) { clearTimeout(viewer._flipperTimer); viewer._flipperTimer = null; }
|
if (viewer._flipperTimer) { clearTimeout(viewer._flipperTimer); viewer._flipperTimer = null; }
|
||||||
|
|
||||||
const urls = t.frames.map(f => `${API}/output/${f}`);
|
const urls = t.frames.map(f => IMAGE_FOLDER + f);
|
||||||
let idx = 0;
|
let idx = 0;
|
||||||
const label = t.group_name || t.group_id.slice(0, 8);
|
const label = t.group_name || t.group_id.slice(0, 8);
|
||||||
|
|
||||||
@@ -10208,6 +10339,7 @@
|
|||||||
_sbPadBottom = document.getElementById('sbPadBottom')?.value ?? _sbPadBottom;
|
_sbPadBottom = document.getElementById('sbPadBottom')?.value ?? _sbPadBottom;
|
||||||
_sbPadLeft = document.getElementById('sbPadLeft')?.value ?? _sbPadLeft;
|
_sbPadLeft = document.getElementById('sbPadLeft')?.value ?? _sbPadLeft;
|
||||||
_sbPadOutpaint = document.getElementById('sbPadOutpaint')?.checked ?? _sbPadOutpaint;
|
_sbPadOutpaint = document.getElementById('sbPadOutpaint')?.checked ?? _sbPadOutpaint;
|
||||||
|
_sbPadUniform = document.getElementById('sbPadUniform')?.checked ?? _sbPadUniform;
|
||||||
_fsSelectedPoses.forEach(name => {
|
_fsSelectedPoses.forEach(name => {
|
||||||
const el = document.getElementById('sbPoseEdit_' + CSS.escape(name));
|
const el = document.getElementById('sbPoseEdit_' + CSS.escape(name));
|
||||||
if (el) _sbPoseEdits[name] = el.value;
|
if (el) _sbPoseEdits[name] = el.value;
|
||||||
@@ -10377,8 +10509,15 @@
|
|||||||
return `<label style="display:flex;align-items:center;gap:2px;font-size:11px;color:#888">${side[0]}
|
return `<label style="display:flex;align-items:center;gap:2px;font-size:11px;color:#888">${side[0]}
|
||||||
<input type="text" id="sbPad${side}" value="${val}" placeholder="px or %"
|
<input type="text" id="sbPad${side}" value="${val}" placeholder="px or %"
|
||||||
style="width:48px;background:#111;border:1px solid #2a2a2a;color:#ccc;border-radius:4px;padding:2px 4px;font-size:11px;text-align:right"
|
style="width:48px;background:#111;border:1px solid #2a2a2a;color:#ccc;border-radius:4px;padding:2px 4px;font-size:11px;text-align:right"
|
||||||
oninput="${onInput}" onblur="${onBlur}"></label>`;
|
oninput="${onInput}" onblur="${onBlur}"
|
||||||
|
onkeydown="if(event.key === 'Enter') { submitSbGenerate(); event.preventDefault(); }"></label>`;
|
||||||
}).join('')}
|
}).join('')}
|
||||||
|
<label style="display:flex;align-items:center;gap:2px;font-size:11px;color:#888;cursor:pointer" title="Keep all padding values equal">
|
||||||
|
<input type="checkbox" id="sbPadUniform" ${_sbPadUniform?'checked':''} onchange="togglePadUniform(this)" style="cursor:pointer">Uniform</label>
|
||||||
|
<button class="sb-btn" style="padding:2px 5px;font-size:9px;margin:0" onclick="applyPadPreset('10%', true)">+10%</button>
|
||||||
|
<button class="sb-btn" style="padding:2px 5px;font-size:9px;margin:0" onclick="applyPadPreset('20%', true)">+20%</button>
|
||||||
|
<button class="sb-btn" style="padding:2px 5px;font-size:9px;margin:0" onclick="applyPadPreset('50%', true)">+50%</button>
|
||||||
|
<button class="sb-btn" style="padding:2px 5px;font-size:9px;margin:0;background:#3b0764;color:#d8b4fe;border-color:#581c87" onclick="applyPadPreset('0', true)">Reset</button>
|
||||||
<label style="display:flex;align-items:center;gap:3px;font-size:11px;color:#888;cursor:pointer" title="Instruct the model to fill in the padded area">
|
<label style="display:flex;align-items:center;gap:3px;font-size:11px;color:#888;cursor:pointer" title="Instruct the model to fill in the padded area">
|
||||||
<input type="checkbox" id="sbPadOutpaint" ${(_sbPadOutpaint??false)?'checked':''} onchange="updateSbGenBtn()" style="cursor:pointer">Outpaint</label>
|
<input type="checkbox" id="sbPadOutpaint" ${(_sbPadOutpaint??false)?'checked':''} onchange="updateSbGenBtn()" style="cursor:pointer">Outpaint</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3496,6 +3496,7 @@ def autocrop_image(filename: str):
|
|||||||
cmin, cmax = np.where(cols)[0][[0, -1]]
|
cmin, cmax = np.where(cols)[0][[0, -1]]
|
||||||
cropped = img.crop((cmin, rmin, cmax + 1, rmax + 1))
|
cropped = img.crop((cmin, rmin, cmax + 1, rmax + 1))
|
||||||
cropped.save(path, format="PNG")
|
cropped.save(path, format="PNG")
|
||||||
|
_invalidate_static()
|
||||||
return {"status": "success", "filename": filename, "box": [int(cmin), int(rmin), int(cmax+1), int(rmax+1)]}
|
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))
|
cropped = img.crop((x1, y1, x2, y2))
|
||||||
fmt = "PNG" if path.lower().endswith(".png") else "JPEG"
|
fmt = "PNG" if path.lower().endswith(".png") else "JPEG"
|
||||||
cropped.save(path, format=fmt)
|
cropped.save(path, format=fmt)
|
||||||
if req.as_copy:
|
_invalidate_static()
|
||||||
_invalidate_static()
|
|
||||||
return {"status": "success", "filename": filename, "new_filename": new_filename,
|
return {"status": "success", "filename": filename, "new_filename": new_filename,
|
||||||
"new_url": f"/output/{new_filename}", "as_copy": req.as_copy,
|
"new_url": f"/output/{new_filename}", "as_copy": req.as_copy,
|
||||||
"box": [x1, y1, x2, y2]}
|
"box": [x1, y1, x2, y2]}
|
||||||
@@ -3679,8 +3679,7 @@ def pad_image(filename: str, req: PadRequest):
|
|||||||
if req.fill == "transparent":
|
if req.fill == "transparent":
|
||||||
fmt = "PNG" # JPEG cannot store alpha
|
fmt = "PNG" # JPEG cannot store alpha
|
||||||
padded.save(path, format=fmt)
|
padded.save(path, format=fmt)
|
||||||
if req.as_copy:
|
_invalidate_static()
|
||||||
_invalidate_static()
|
|
||||||
return {
|
return {
|
||||||
"status": "success", "filename": filename, "new_filename": new_filename,
|
"status": "success", "filename": filename, "new_filename": new_filename,
|
||||||
"new_url": f"/output/{new_filename}", "as_copy": req.as_copy,
|
"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])
|
img = Image.open(path).transpose(cw_to_transpose[deg])
|
||||||
fmt = "PNG" if path.lower().endswith(".png") else "JPEG"
|
fmt = "PNG" if path.lower().endswith(".png") else "JPEG"
|
||||||
img.save(path, format=fmt)
|
img.save(path, format=fmt)
|
||||||
|
_invalidate_static()
|
||||||
return {"status": "success", "filename": filename, "degrees": deg}
|
return {"status": "success", "filename": filename, "degrees": deg}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,10 @@
|
|||||||
<div id="toast"></div>
|
<div id="toast"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const API = window.location.origin;
|
let API = window.location.origin;
|
||||||
|
if (API === 'null' || API.startsWith('file://')) {
|
||||||
|
API = 'http://127.0.0.1:8500';
|
||||||
|
}
|
||||||
|
|
||||||
function showToast(msg) {
|
function showToast(msg) {
|
||||||
const t = document.getElementById('toast');
|
const t = document.getElementById('toast');
|
||||||
|
|||||||
Reference in New Issue
Block a user