Summary
• Implemented global offline mode for all HuggingFace-dependent modules to eliminate unauthenticated request warnings and prevent external connection attempts during startup and runtime. Changes • Global Offline Configuration: Added HF_HUB_OFFLINE=1 and TRANSFORMERS_OFFLINE=1 to the environment variables at the top of edit_api.py, embeddings.py, watcher.py, and pose_llm/pose_llm_api.py. • Explicit Local Files Only: Updated all huggingface_hub.hf_hub_download calls in edit_api.py to include local_files_only=True, ensuring they never attempt to reach the Hub if models are already cached. • LLM Service Optimization: Updated AutoTokenizer and AutoModelForCausalLM calls in pose_llm/pose_llm_api.py to use local_files_only=True. • Consistency: Ensured that shared modules like embeddings.py which uses open_clip are also locked to offline mode to prevent background downloads.
This commit is contained in:
66
AGENTS.md
66
AGENTS.md
@@ -63,6 +63,14 @@ 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`
|
||||||
|
|
||||||
|
#### In-Memory File Metadata Cache
|
||||||
|
|
||||||
|
To mitigate performance and network latency bottlenecks on remote/shared storage filesystems (e.g. `/mnt/zim`), the API server employs a light, thread-safe in-memory metadata cache (`_file_meta_cache`):
|
||||||
|
- **Cached Items**: File existence (`exists`) and modification timestamps (`mtime`) mapped by `filename`.
|
||||||
|
- **Cache TTL**: 5.0 seconds.
|
||||||
|
- **Idempotency & Invalidation**: Any mutation endpoints (such as crop, padding, duplication, background removal, or deletion) automatically update (`_update_cached_file_meta`) or explicitly delete (`_clear_cached_file_meta`) entries to keep cache and disk perfectly in sync.
|
||||||
|
- **Preloading Optimization**: Leverages cache queries in static indexing loops (`_sync_preloaded_images`, `_write_all_static`, `list_images`) and tracks changes to completely skip redundant static HTML rewrites if the preloaded image set is identical (`_last_preloaded_images_set`).
|
||||||
|
|
||||||
#### Endpoints Library
|
#### Endpoints Library
|
||||||
|
|
||||||
##### Core Generation & Upload
|
##### Core Generation & Upload
|
||||||
@@ -112,6 +120,12 @@ FastAPI :8500 (edit_api.py)
|
|||||||
| `GET` | `/similar/{filename}` | Find visually similar images using database embeddings |
|
| `GET` | `/similar/{filename}` | Find visually similar images using database embeddings |
|
||||||
| `GET` | `/names` | Map `filename → person name` |
|
| `GET` | `/names` | Map `filename → person name` |
|
||||||
| `POST` | `/names/{filename}` | Map image file to custom character name |
|
| `POST` | `/names/{filename}` | Map image file to custom character name |
|
||||||
|
| `POST` | `/images/{filename}/tags` | Set image tags list (supports tags like `ARCHIVED`, `HIDDEN`, `SOURCE`, `VISIBLE`, `LIKE`, `DISLIKE`, `21+`) |
|
||||||
|
| `POST` | `/images/{filename}/tag-action` | Add, remove, or toggle a specific tag on an image (automatically handles mutual exclusivity of `LIKE`/`DISLIKE`) |
|
||||||
|
| `POST` | `/images/{filename}/source` | Explicitly toggle marking an image as a source asset (sets/clears `is_source` and toggles the `SOURCE` tag) |
|
||||||
|
| `POST` | `/images/{filename}/estimate-21plus` | Analyze image with the WD tagger to estimate adult/sensitive rating and apply `21+` tag |
|
||||||
|
| `POST` | `/images/bulk-move` | Move a list of filenames to a specific target filmstrip (`VISIBLE`, `HIDDEN`, `ARCHIVED`, `SOURCE`) in bulk, updating tags/columns |
|
||||||
|
| `POST` | `/images/{filename}/undress` | Queue an asynchronous batch job utilizing the `UNDRESS_PROMPT` on the given reference image |
|
||||||
|
|
||||||
##### Visibility, Archiving & Deletion
|
##### Visibility, Archiving & Deletion
|
||||||
| Method | Path | Purpose |
|
| Method | Path | Purpose |
|
||||||
@@ -192,11 +206,13 @@ prompt
|
|||||||
│ set negative: "deformed anatomy, watermark, logo"
|
│ set negative: "deformed anatomy, watermark, logo"
|
||||||
│
|
│
|
||||||
├─ Canvas Expansion / Outpainting requested?
|
├─ Canvas Expansion / Outpainting requested?
|
||||||
│ YES → if outpaint=True, append/use natural outpainting instructions:
|
│ YES → if outpaint=True:
|
||||||
│ "Naturally outpaint and extend the borders of the image to complete the scene."
|
│ ├─ If source is transparent (RGBA): composite onto flat black background for maximum contrast.
|
||||||
|
│ ├─ Formulate dynamic prompt description: e.g. "replacing the black/white background areas"
|
||||||
|
│ └─ Append outpainting prompt instructions: "Naturally outpaint and extend..."
|
||||||
│
|
│
|
||||||
├─ Upload reference image(s) to ComfyUI /upload/image
|
├─ Upload reference image(s) to ComfyUI /upload/image
|
||||||
│ image1 = source image
|
│ image1 = source image (composited RGB if outpainting transparent images)
|
||||||
│ image2 = wireframe pose guide frame (optional)
|
│ image2 = wireframe pose guide frame (optional)
|
||||||
│
|
│
|
||||||
├─ Patch workflow graph nodes (prompt, size, seed, sampler)
|
├─ Patch workflow graph nodes (prompt, size, seed, sampler)
|
||||||
@@ -315,6 +331,9 @@ Model: `buffalo_l`
|
|||||||
| 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 |
|
| face_embedding | `vector(512)` | Face recognition embedding for visually matching character faces |
|
||||||
| is_source | `BOOLEAN` | Whether the file is a primary reference/source asset |
|
| is_source | `BOOLEAN` | Whether the file is a primary reference/source asset |
|
||||||
|
| tags | TEXT (JSON) | Custom user-applied tags/labels (e.g., `['VISIBLE', 'LIKE', '21+']`) |
|
||||||
|
| pose_description | TEXT | AI-generated descriptive text of the body pose |
|
||||||
|
| pose_skeleton | TEXT | Coordinates and scoring details of the 2D COCO-17 body pose skeleton |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -352,10 +371,51 @@ To prevent desyncs during image transitions, crops, canvas expansion, or 2.2x zo
|
|||||||
- A `ResizeObserver` is registered on the primary studio viewer image (`lbImg`).
|
- 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.
|
- 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.
|
||||||
|
|
||||||
|
#### Multiple Filmstrips System
|
||||||
|
The filmstrip bar has been updated with five tabs that filter the group's images dynamically:
|
||||||
|
- **Group Active**: The default filmstrip containing the main/preferred assets of the group.
|
||||||
|
- **Group Variant** (`VISIBLE`): Shows all standard visible variants within the group.
|
||||||
|
- **Hidden** (`HIDDEN`): Soft-hidden variants (excluded from main cycling).
|
||||||
|
- **Source** (`SOURCE`): Displays reference images explicitly marked as source assets.
|
||||||
|
- **Archived** (`ARCHIVED`): Displays soft-archived variants.
|
||||||
|
- **Drag-and-Drop Reclassification**: Users can drag any variant thumbnail from the filmstrip and drop it directly onto any of the tab buttons to instantly update its tag/status and reassign it to that list.
|
||||||
|
|
||||||
|
#### Bulk Multi-Select & Actions
|
||||||
|
Toggling the **Multi-select** button unlocks checkbox-based selection across the filmstrip:
|
||||||
|
- Provides a live selection counter.
|
||||||
|
- Enables bulk move/reclassification buttons to dispatch batch updates to `/images/bulk-move` (e.g., `Move to Group`, `Move to Hidden`, `Move to Source`, `Move to Archived`).
|
||||||
|
|
||||||
|
#### Seamless Non-Blinking State Updates
|
||||||
|
To prevent irritating browser reload blinks or blank screen latency during intensive image editing operations:
|
||||||
|
- **Local Metadata Cloning**: Duplication, manual cropping, padding, and background removal immediately clone the original's DB metadata values locally on the client and splice the new file record into the active filmstrip list.
|
||||||
|
- **Optimistic Merge**: During periodic background server syncs (`refreshNow`), an optimistic merge algorithm compares the local client-side list and the server JSON. It preserves any freshly created local assets that haven't yet been processed/synced on the backend, maintaining perfect continuity.
|
||||||
|
|
||||||
|
#### Studio Keyboard Shortcuts Reference
|
||||||
|
|
||||||
|
| Key | Action | Description |
|
||||||
|
|-----|--------|-------------|
|
||||||
|
| `P` | Toggle Privacy Mode | Replaces the page with a discrete "AI Assist" study-chat and disguises taskbar thumbnails. |
|
||||||
|
| `C` | Toggle Checkerboard | Toggle transparent background checkerboard grid. |
|
||||||
|
| `A` | Expand/Pad Canvas | Instantly trigger the manual padding toolbar. |
|
||||||
|
| `G` / `Alt+Enter` | Trigger Generation | Clicks the main "Generate" button (when Generate tab is active). |
|
||||||
|
| `ArrowLeft` / `ArrowRight` | Nav Filmstrip | Cycle through the current list of image variants (debounced). |
|
||||||
|
| `Home` / `End` | Jump to Bounds | Instantly jump to the first or last variant in the filmstrip. |
|
||||||
|
| `Space` | Play/Pause Video | Pauses or plays a running wireframe/clip inside the viewer. |
|
||||||
|
| `H` | Toggle Hidden | Reclasses the image between standard variants and hidden list. |
|
||||||
|
| `F` | Set as Preferred | Reorders the group, placing the current image at index 0 (as preferred thumbnail). |
|
||||||
|
| `S` | Toggle Source | Mark/unmark current image as a primary reference/source asset. |
|
||||||
|
| `E` | Run 21+ Tagger | Analyze image via WD tagger and apply/remove the `21+` tag. |
|
||||||
|
| `Delete` | Soft-Archive | Move the current image into the Archive filmstrip. |
|
||||||
|
| `ArrowUp` / `ArrowDown` | Shift Order | Move/reorder the current image earlier or later in the group (debounced). |
|
||||||
|
| `2` | Move to Second | Instantly reorder current image to the 2nd position in the group. |
|
||||||
|
| `9` | Move to Last-1 | Instantly reorder current image to the second to last position. |
|
||||||
|
| `Escape` | Cancel / Dismiss | Cancel cropping, padding, close variant picker, or exit selection mode. |
|
||||||
|
|
||||||
**Key state variables**:
|
**Key state variables**:
|
||||||
- `lbCurrentGid` — active group ID
|
- `lbCurrentGid` — active group ID
|
||||||
- `lbIdx` — current filmstrip position
|
- `lbIdx` — current filmstrip position
|
||||||
- `_followLatestGid` — auto-follow newly generated image
|
- `_followLatestGid` — auto-follow newly generated image
|
||||||
|
- `_followLatestFilename` — auto-follow specific new output file
|
||||||
- `_sbSelectedAngles` — selected camera angle names
|
- `_sbSelectedAngles` — selected camera angle names
|
||||||
- `_sbWireframeRef` / `_sbWireframeTime` — wireframe video + frame time
|
- `_sbWireframeRef` / `_sbWireframeTime` — wireframe video + frame time
|
||||||
- `privacyMode` — privacy overlay toggle (key: `P`)
|
- `privacyMode` — privacy overlay toggle (key: `P`)
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ Scenery:
|
|||||||
- introduce a like/dislike base system for images/group to determine the sort order (and more), would also dislike the pose
|
- introduce a like/dislike base system for images/group to determine the sort order (and more), would also dislike the pose
|
||||||
- ✅ creating scenery should keep both video-frame + ref images as references, we only see 1 image now. ✅ fixed
|
- ✅ creating scenery should keep both video-frame + ref images as references, we only see 1 image now. ✅ fixed
|
||||||
- gesture ideas for future (require ControlNet / OpenPose conditioning):
|
- gesture ideas for future (require ControlNet / OpenPose conditioning):
|
||||||
|
- add expand preset for sqaure image
|
||||||
## notes
|
## notes
|
||||||
|
|
||||||
pose bestaat gewoon uit meerdere delen, camera, scenery + addition
|
pose bestaat gewoon uit meerdere delen, camera, scenery + addition
|
||||||
|
|||||||
@@ -2299,7 +2299,55 @@
|
|||||||
|
|
||||||
// --- HYDRATION_START ---
|
// --- HYDRATION_START ---
|
||||||
const PRELOADED_IMAGES = [
|
const PRELOADED_IMAGES = [
|
||||||
"20260628_133544_pad_20260621_032709_image.png",
|
"20260628_165000_sc_image.png",
|
||||||
|
"20260628_164850_sc_sc_image.png",
|
||||||
|
"20260628_154819_sc_image.png",
|
||||||
|
"20260628_154609_image.png",
|
||||||
|
"20260628_154508_sc_image.png",
|
||||||
|
"20260628_154429_dup_20260621_032709_image.png",
|
||||||
|
"20260628_154339_image.png",
|
||||||
|
"20260628_153602_sc_dup_20260621_032709_image.nobg.png",
|
||||||
|
"20260628_153452_sc_image.png",
|
||||||
|
"20260628_153333_sc_image.png",
|
||||||
|
"20260628_151457_sc_image.png",
|
||||||
|
"20260628_151037_sc_image.png",
|
||||||
|
"up_6d311abe_face.png",
|
||||||
|
"20260628_150512_dup_20260621_032709_image.nobg.png",
|
||||||
|
"20260628_145644_dup_20260621_032709_image.nobg.png",
|
||||||
|
"_turntable/cg_2afca8fc/views/20260628_145317_pad_20260628_144836_view_001_015deg.nobg.png",
|
||||||
|
"_turntable/cg_2afca8fc/views/20260628_145305_pad_20260628_144836_view_001_015deg.nobg.png",
|
||||||
|
"_turntable/cg_2afca8fc/views/20260628_145254_pad_20260628_144836_view_001_015deg.nobg.png",
|
||||||
|
"_turntable/cg_2afca8fc/views/20260628_145243_pad_20260628_144836_view_001_015deg.nobg.png",
|
||||||
|
"_turntable/cg_2afca8fc/views/20260628_145232_pad_20260628_144836_view_001_015deg.nobg.png",
|
||||||
|
"_turntable/cg_2afca8fc/views/20260628_145221_pad_20260628_144836_view_001_015deg.nobg.png",
|
||||||
|
"_turntable/cg_2afca8fc/views/20260628_145209_pad_20260628_144836_view_001_015deg.nobg.png",
|
||||||
|
"_turntable/cg_2afca8fc/views/20260628_145127_pad_20260628_144836_view_001_015deg.nobg.png",
|
||||||
|
"cg_2afca8fc_face.png",
|
||||||
|
"_turntable/cg_2afca8fc/views/20260628_144836_view_001_015deg.nobg.png",
|
||||||
|
"_turntable/cg_2afca8fc/views/20260628_144836_view_001_015deg.png",
|
||||||
|
"_turntable/cg_2afca8fc/views/20260628_144825_view_001_015deg.png",
|
||||||
|
"_turntable/cg_2afca8fc/views/20260628_144814_view_001_015deg.png",
|
||||||
|
"cg_75340e8e_face.png",
|
||||||
|
"cg_08203e88_face.png",
|
||||||
|
"cg_74544975_face.png",
|
||||||
|
"kk563t.png_face.png",
|
||||||
|
"20260628_143350_kk563t.nobg.png",
|
||||||
|
"20260628_143619_dup_20260628_143350_kk563t.nobg.png",
|
||||||
|
"20260628_143350_kk563t.png",
|
||||||
|
"20260628_143206_kk563t.png",
|
||||||
|
"pass-1.png_face.png",
|
||||||
|
"20260626_231655_sc_image.nobg.png",
|
||||||
|
"20260618_181600_image.png_face.png",
|
||||||
|
"_turntable/20260618_181600_image.png/views/view_000_000deg.png",
|
||||||
|
"_turntable/cg_74544975/views/20260628_140758_view_000_000deg.nobg.png",
|
||||||
|
"_turntable/cg_74544975/views/20260628_142102_dup_20260628_140758_view_000_000deg.nobg.png",
|
||||||
|
"_turntable/cg_74544975/views/20260628_140758_view_000_000deg.png",
|
||||||
|
"up_3a05a2de_face.png",
|
||||||
|
"up_aa1add4d_face.png",
|
||||||
|
"20260628_134529_dup_20260621_032709_image.nobg.png",
|
||||||
|
"_turntable/cg_7ec17537/views/20260628_135858_dup_20260628_035257_dup_20260628_033600_dup_view_000_000deg.nobg.png",
|
||||||
|
"cg_7ec17537_face.png",
|
||||||
|
"20260628_134529_dup_20260621_032709_image.png",
|
||||||
"_turntable/up_7b6eaabb/views/view_023_345deg.png",
|
"_turntable/up_7b6eaabb/views/view_023_345deg.png",
|
||||||
"_turntable/up_7b6eaabb/views/view_022_330deg.png",
|
"_turntable/up_7b6eaabb/views/view_022_330deg.png",
|
||||||
"_turntable/up_7b6eaabb/views/view_021_315deg.png",
|
"_turntable/up_7b6eaabb/views/view_021_315deg.png",
|
||||||
@@ -2443,16 +2491,6 @@
|
|||||||
"_turntable/pass-1.png/views/view_008_120deg.png",
|
"_turntable/pass-1.png/views/view_008_120deg.png",
|
||||||
"_turntable/pass-1.png/views/view_007_105deg.png",
|
"_turntable/pass-1.png/views/view_007_105deg.png",
|
||||||
"_turntable/pass-1.png/views/view_006_090deg.png",
|
"_turntable/pass-1.png/views/view_006_090deg.png",
|
||||||
"20260628_115611_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
|
||||||
"20260628_115349_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
|
||||||
"20260628_114542_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
|
||||||
"20260628_114232_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
|
||||||
"20260628_114212_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
|
||||||
"20260628_113939_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
|
||||||
"20260628_113737_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
|
||||||
"20260628_113405_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
|
||||||
"20260628_113320_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
|
||||||
"20260628_113242_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
|
||||||
"20260628_113152_sc_8_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
|
"20260628_113152_sc_8_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
|
||||||
"20260628_111908_sc_best_01_person_0.874_2026-06-27_12-18-49.png",
|
"20260628_111908_sc_best_01_person_0.874_2026-06-27_12-18-49.png",
|
||||||
"_turntable/pass-1.png/views/view_005_075deg.png",
|
"_turntable/pass-1.png/views/view_005_075deg.png",
|
||||||
@@ -2477,20 +2515,15 @@
|
|||||||
"_turntable/cg_9bb8f3bb/views/view_010_150deg.png",
|
"_turntable/cg_9bb8f3bb/views/view_010_150deg.png",
|
||||||
"_turntable/cg_9bb8f3bb/views/view_009_135deg.png",
|
"_turntable/cg_9bb8f3bb/views/view_009_135deg.png",
|
||||||
"_turntable/cg_9bb8f3bb/views/view_008_120deg.png",
|
"_turntable/cg_9bb8f3bb/views/view_008_120deg.png",
|
||||||
"20260628_105942_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
|
||||||
"_turntable/cg_9bb8f3bb/views/view_007_105deg.png",
|
"_turntable/cg_9bb8f3bb/views/view_007_105deg.png",
|
||||||
"_turntable/cg_9bb8f3bb/views/view_006_090deg.png",
|
"_turntable/cg_9bb8f3bb/views/view_006_090deg.png",
|
||||||
"_turntable/cg_9bb8f3bb/views/view_005_075deg.png",
|
"_turntable/cg_9bb8f3bb/views/view_005_075deg.png",
|
||||||
"20260628_105539_sc_pad_20260627_212800_4_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.nobg.png",
|
|
||||||
"_turntable/cg_9bb8f3bb/views/view_004_060deg.png",
|
"_turntable/cg_9bb8f3bb/views/view_004_060deg.png",
|
||||||
"20260628_105040_pad_20260627_212800_4_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.nobg.png",
|
|
||||||
"_turntable/cg_9bb8f3bb/views/view_003_045deg.png",
|
"_turntable/cg_9bb8f3bb/views/view_003_045deg.png",
|
||||||
"_turntable/cg_9bb8f3bb/views/view_002_030deg.png",
|
"_turntable/cg_9bb8f3bb/views/view_002_030deg.png",
|
||||||
"_turntable/cg_9bb8f3bb/views/view_001_015deg.png",
|
"_turntable/cg_9bb8f3bb/views/view_001_015deg.png",
|
||||||
"_turntable/cg_9bb8f3bb/views/view_000_000deg.png",
|
"_turntable/cg_9bb8f3bb/views/view_000_000deg.png",
|
||||||
"_turntable/pa01.png/views/view_023_345deg.png",
|
"_turntable/pa01.png/views/view_023_345deg.png",
|
||||||
"20260628_105307_dup_20260628_105040_pad_20260627_212800_4_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
|
|
||||||
"20260628_105040_pad_20260627_212800_4_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
|
|
||||||
"_turntable/pa01.png/views/view_022_330deg.png",
|
"_turntable/pa01.png/views/view_022_330deg.png",
|
||||||
"_turntable/pa01.png/views/view_021_315deg.png",
|
"_turntable/pa01.png/views/view_021_315deg.png",
|
||||||
"20260627_212812_5_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
|
"20260627_212812_5_20260627_212658_best_00_person_0.874_2026-06-27_12-18-49.png",
|
||||||
@@ -2563,14 +2596,6 @@
|
|||||||
"_turntable/up_2687be75/views/view_002_030deg.png",
|
"_turntable/up_2687be75/views/view_002_030deg.png",
|
||||||
"_turntable/up_2687be75/views/view_001_015deg.png",
|
"_turntable/up_2687be75/views/view_001_015deg.png",
|
||||||
"_turntable/up_2687be75/views/view_000_000deg.png",
|
"_turntable/up_2687be75/views/view_000_000deg.png",
|
||||||
"20260628_061520_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
|
||||||
"20260628_061440_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
|
||||||
"20260628_061355_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
|
||||||
"20260628_061322_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
|
||||||
"20260628_061249_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
|
||||||
"20260628_061152_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
|
||||||
"20260628_061006_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
|
||||||
"20260628_060550_sc_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
|
||||||
"20260628_060113_sc_best_00_person_0.874_2026-06-27_12-18-49.png",
|
"20260628_060113_sc_best_00_person_0.874_2026-06-27_12-18-49.png",
|
||||||
"20260628_055858_sc_best_01_person_0.874_2026-06-27_12-18-49.png",
|
"20260628_055858_sc_best_01_person_0.874_2026-06-27_12-18-49.png",
|
||||||
"_turntable/cg_7ec17537/views/20260628_055541_sc_dup_20260628_033600_dup_view_000_000deg.png",
|
"_turntable/cg_7ec17537/views/20260628_055541_sc_dup_20260628_033600_dup_view_000_000deg.png",
|
||||||
@@ -2647,11 +2672,6 @@
|
|||||||
"20260618_053802_image.png_face.png",
|
"20260618_053802_image.png_face.png",
|
||||||
"20260628_032232_dup_20260618_053932_7_20260618_053802_image.nobg.nobg.nobg.png",
|
"20260628_032232_dup_20260618_053932_7_20260618_053802_image.nobg.nobg.nobg.png",
|
||||||
"20260628_032232_dup_20260618_053932_7_20260618_053802_image.nobg.nobg.png",
|
"20260628_032232_dup_20260618_053932_7_20260618_053802_image.nobg.nobg.png",
|
||||||
"pass-1.png_face.png",
|
|
||||||
"20260628_031641_dup_20260628_031119_pad_pass-1.nobg.png",
|
|
||||||
"20260628_031119_pad_pass-1.nobg.png",
|
|
||||||
"20260628_031316_pad_pass-1.png",
|
|
||||||
"20260628_031119_pad_pass-1.png",
|
|
||||||
"pa01.png_face.png",
|
"pa01.png_face.png",
|
||||||
"20260628_030838_pa01.png",
|
"20260628_030838_pa01.png",
|
||||||
"20260628_030808_pa01.png",
|
"20260628_030808_pa01.png",
|
||||||
@@ -2659,51 +2679,19 @@
|
|||||||
"20260628_030532_pa01.png",
|
"20260628_030532_pa01.png",
|
||||||
"20260628_030241_pa01.png",
|
"20260628_030241_pa01.png",
|
||||||
"20260628_030115_pa01.png",
|
"20260628_030115_pa01.png",
|
||||||
"kk563t.png_face.png",
|
|
||||||
"20260628_025504_pad_pass-1.png",
|
|
||||||
"up_e25ad102_face.png",
|
"up_e25ad102_face.png",
|
||||||
"20260628_020853_pad_20260628_015651_img_12.nobg.png",
|
|
||||||
"20260628_020642_pad_20260628_015651_img_12.nobg.png",
|
|
||||||
"20260628_020610_pad_20260628_015651_img_12.nobg.png",
|
|
||||||
"20260628_020546_pad_20260628_015651_img_12.nobg.png",
|
|
||||||
"20260628_020519_pad_20260628_015651_img_12.nobg.png",
|
|
||||||
"20260628_020428_pad_20260628_015651_img_12.nobg.png",
|
|
||||||
"20260628_020302_pad_20260628_015651_img_12.nobg.png",
|
|
||||||
"20260628_020125_pad_20260628_015651_img_12.nobg.png",
|
|
||||||
"20260628_015815_pad_20260628_015651_img_12.nobg.png",
|
|
||||||
"20260628_015815_pad_20260628_015651_img_12.png",
|
|
||||||
"20260628_015707_img_12.png",
|
"20260628_015707_img_12.png",
|
||||||
"20260628_015651_img_12.png",
|
"20260628_015651_img_12.png",
|
||||||
"20260615_153426_img_12.png",
|
"20260615_153426_img_12.png",
|
||||||
"20260628_015423_img_12.png",
|
"20260628_015423_img_12.png",
|
||||||
"cg_5991f3f2_face.png",
|
"cg_5991f3f2_face.png",
|
||||||
"20260628_012806_pad_20260624_143523_Screenshot_From_2026-06-17_07-41-27.nobg.nobg.png",
|
|
||||||
"20260628_014614_p10.nobg.png",
|
"20260628_014614_p10.nobg.png",
|
||||||
"p10.png_face.png",
|
"p10.png_face.png",
|
||||||
"20260628_014614_p10.png",
|
"20260628_014614_p10.png",
|
||||||
"20260628_014603_p10.png",
|
"20260628_014603_p10.png",
|
||||||
"up_2687be75_face.png",
|
"up_2687be75_face.png",
|
||||||
"20260628_013521_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
|
||||||
"20260628_013511_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
|
||||||
"20260628_013459_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
|
||||||
"20260628_013429_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.nobg.png",
|
|
||||||
"20260628_013429_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.png",
|
|
||||||
"20260628_013412_pad_20260627_213816_best_01_person_0.874_2026-06-27_12-18-49.png",
|
|
||||||
"20260628_012832_pad_20260624_143523_Screenshot_From_2026-06-17_07-41-27.nobg.nobg.png",
|
|
||||||
"_turntable/cg_5991f3f2/views/view_022_330deg.png",
|
"_turntable/cg_5991f3f2/views/view_022_330deg.png",
|
||||||
"20260628_011700_pad_20260624_154318_img_72.png",
|
|
||||||
"20260628_012006_pad_20260624_154318_img_72.png",
|
|
||||||
"20260628_011837_pad_20260624_154318_img_72.png",
|
|
||||||
"20260628_012240_crop_20260628_011837_pad_20260624_154318_img_72.png",
|
|
||||||
"20260628_011227_dup_Pasted image (9).png",
|
"20260628_011227_dup_Pasted image (9).png",
|
||||||
"20260628_011118_pad_20260628_010755_pad_20260626_005644_dup_20260626_005504_Pasted image (9).nobg.nobg.png",
|
|
||||||
"20260628_010957_pad_20260628_010755_pad_20260626_005644_dup_20260626_005504_Pasted image (9).nobg.nobg.png",
|
|
||||||
"20260628_010947_pad_20260628_010755_pad_20260626_005644_dup_20260626_005504_Pasted image (9).nobg.nobg.png",
|
|
||||||
"20260628_010858_pad_20260628_010755_pad_20260626_005644_dup_20260626_005504_Pasted image (9).nobg.nobg.png",
|
|
||||||
"20260628_010858_pad_20260628_010755_pad_20260626_005644_dup_20260626_005504_Pasted image (9).nobg.png",
|
|
||||||
"20260628_010841_pad_20260628_010755_pad_20260626_005644_dup_20260626_005504_Pasted image (9).nobg.png",
|
|
||||||
"20260628_010755_pad_20260626_005644_dup_20260626_005504_Pasted image (9).nobg.png",
|
|
||||||
"20260628_010630_pad_20260626_005644_dup_20260626_005504_Pasted image (9).nobg.png",
|
|
||||||
"20260628_010408_image.png",
|
"20260628_010408_image.png",
|
||||||
"20260627_203626_image.png",
|
"20260627_203626_image.png",
|
||||||
"up_7b6eaabb_face.png",
|
"up_7b6eaabb_face.png",
|
||||||
@@ -2738,12 +2726,6 @@
|
|||||||
"_turntable/up_6ed4293c/views/view_002_030deg.png",
|
"_turntable/up_6ed4293c/views/view_002_030deg.png",
|
||||||
"_turntable/up_6ed4293c/views/view_001_015deg.png",
|
"_turntable/up_6ed4293c/views/view_001_015deg.png",
|
||||||
"_turntable/up_6ed4293c/views/view_000_000deg.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",
|
|
||||||
"20260627_232526_pad_20260626_035101_pad_20260626_034830_jb.nobg.png",
|
|
||||||
"20260627_232302_pad_20260627_232133_image.png",
|
|
||||||
"20260627_232238_pad_20260627_232133_image.png",
|
|
||||||
"up_6ed4293c_face.png",
|
"up_6ed4293c_face.png",
|
||||||
"20260627_232209_image.png",
|
"20260627_232209_image.png",
|
||||||
"20260627_232151_image.png",
|
"20260627_232151_image.png",
|
||||||
@@ -3259,7 +3241,6 @@
|
|||||||
"20260627_161342_image.nobg.png",
|
"20260627_161342_image.nobg.png",
|
||||||
"20260618_053458_image.png_face.png",
|
"20260618_053458_image.png_face.png",
|
||||||
"20260627_161342_image.png",
|
"20260627_161342_image.png",
|
||||||
"20260627_161217_pad_20260627_161055_image.png",
|
|
||||||
"20260627_161055_image.png",
|
"20260627_161055_image.png",
|
||||||
"20260627_160901_mr_sc_image.png",
|
"20260627_160901_mr_sc_image.png",
|
||||||
"20260627_160718_mr_sc_image.png",
|
"20260627_160718_mr_sc_image.png",
|
||||||
@@ -3275,7 +3256,6 @@
|
|||||||
"20260620_031100_20150913_211324.jpg",
|
"20260620_031100_20150913_211324.jpg",
|
||||||
"20260627_154745_20150913_211324.jpg",
|
"20260627_154745_20150913_211324.jpg",
|
||||||
"20260627_154404_20150913_211324.jpg",
|
"20260627_154404_20150913_211324.jpg",
|
||||||
"20260627_153734_pad_20260620_031100_20150913_211324.jpg",
|
|
||||||
"20260625_050554_sc_3_20260618_173728_image.nobg.png",
|
"20260625_050554_sc_3_20260618_173728_image.nobg.png",
|
||||||
"_turntable/cg_ad73335e/views/view_022_330deg.png",
|
"_turntable/cg_ad73335e/views/view_022_330deg.png",
|
||||||
"_turntable/cg_ad73335e/views/view_021_315deg.png",
|
"_turntable/cg_ad73335e/views/view_021_315deg.png",
|
||||||
@@ -3286,11 +3266,6 @@
|
|||||||
"cg_ac6274f0_face.png",
|
"cg_ac6274f0_face.png",
|
||||||
"20260618_180939_image.png_face.png",
|
"20260618_180939_image.png_face.png",
|
||||||
"img_51.png_face.png",
|
"img_51.png_face.png",
|
||||||
"20260627_150448_pad_20260627_150234_pad_20260627_150207_pad_20260627_145719_img_51.png",
|
|
||||||
"20260627_150351_pad_20260627_150234_pad_20260627_150207_pad_20260627_145719_img_51.png",
|
|
||||||
"20260627_150251_pad_20260627_150234_pad_20260627_150207_pad_20260627_145719_img_51.png",
|
|
||||||
"20260627_150234_pad_20260627_150207_pad_20260627_145719_img_51.png",
|
|
||||||
"20260627_150207_pad_20260627_145719_img_51.png",
|
|
||||||
"20260627_150013_img_51.png",
|
"20260627_150013_img_51.png",
|
||||||
"20260627_145730_img_51.png",
|
"20260627_145730_img_51.png",
|
||||||
"20260627_145719_img_51.png",
|
"20260627_145719_img_51.png",
|
||||||
@@ -3345,27 +3320,13 @@
|
|||||||
"up_ae825fce_face.png",
|
"up_ae825fce_face.png",
|
||||||
"_turntable/cg_4e2b6449/views/view_000_000deg.png",
|
"_turntable/cg_4e2b6449/views/view_000_000deg.png",
|
||||||
"_turntable/cg_4e2b6449/views/view_001_015deg.png",
|
"_turntable/cg_4e2b6449/views/view_001_015deg.png",
|
||||||
"20260627_134506_pad_20260627_130837_image.png",
|
|
||||||
"20260627_134409_pad_20260627_130837_image.png",
|
|
||||||
"_turntable/cg_4e2b6449/views/view_003_045deg.png",
|
"_turntable/cg_4e2b6449/views/view_003_045deg.png",
|
||||||
"_turntable/cg_4e2b6449/views/view_002_030deg.png",
|
"_turntable/cg_4e2b6449/views/view_002_030deg.png",
|
||||||
"20260627_133359_pad_20260627_132332_image.nobg.png",
|
|
||||||
"20260627_133236_pad_20260627_132332_image.nobg.png",
|
|
||||||
"20260627_133149_pad_20260627_132332_image.nobg.png",
|
|
||||||
"20260627_133128_pad_20260627_132332_image.nobg.png",
|
|
||||||
"20260627_133024_pad_20260627_132332_image.nobg.png",
|
|
||||||
"20260627_132913_pad_20260627_132332_image.nobg.png",
|
|
||||||
"20260627_132744_pad_20260627_132332_image.nobg.png",
|
|
||||||
"20260627_132456_pad_20260627_132332_image.nobg.png",
|
|
||||||
"20260627_132402_pad_20260627_132332_image.nobg.png",
|
|
||||||
"20260627_132402_pad_20260627_132332_image.png",
|
|
||||||
"20260627_132332_image.png",
|
"20260627_132332_image.png",
|
||||||
"_turntable/cg_65f71c4d/views/view_001_015deg.png",
|
"_turntable/cg_65f71c4d/views/view_001_015deg.png",
|
||||||
"_turntable/cg_65f71c4d/views/view_000_000deg.png",
|
"_turntable/cg_65f71c4d/views/view_000_000deg.png",
|
||||||
"_turntable/cg_65f71c4d/views/view_023_345deg.png",
|
"_turntable/cg_65f71c4d/views/view_023_345deg.png",
|
||||||
"20260627_132208_pad_20260626_224632_sc_image.png",
|
|
||||||
"20260627_132151_sc_image.png",
|
"20260627_132151_sc_image.png",
|
||||||
"20260627_132035_pad_20260626_224632_sc_image.png",
|
|
||||||
"_turntable/cg_870f3f93/views/view_023_345deg.png",
|
"_turntable/cg_870f3f93/views/view_023_345deg.png",
|
||||||
"_turntable/cg_870f3f93/views/view_018_270deg.png",
|
"_turntable/cg_870f3f93/views/view_018_270deg.png",
|
||||||
"_turntable/cg_870f3f93/views/view_016_240deg.png",
|
"_turntable/cg_870f3f93/views/view_016_240deg.png",
|
||||||
@@ -3382,12 +3343,7 @@
|
|||||||
"_turntable/cg_870f3f93/views/view_003_045deg.png",
|
"_turntable/cg_870f3f93/views/view_003_045deg.png",
|
||||||
"_turntable/cg_870f3f93/views/view_001_015deg.png",
|
"_turntable/cg_870f3f93/views/view_001_015deg.png",
|
||||||
"_turntable/cg_870f3f93/views/view_000_000deg.png",
|
"_turntable/cg_870f3f93/views/view_000_000deg.png",
|
||||||
"20260627_131628_pad_20260622_053758_2_20260621_115215_image.png",
|
|
||||||
"20260627_131605_pad_20260622_053758_2_20260621_115215_image.png",
|
|
||||||
"20260627_131420_2_20260621_115215_image.png",
|
"20260627_131420_2_20260621_115215_image.png",
|
||||||
"20260627_131120_pad_20260627_130837_image.png",
|
|
||||||
"20260627_131008_pad_20260627_130837_image.png",
|
|
||||||
"20260627_130925_pad_20260627_130837_image.png",
|
|
||||||
"20260627_130837_image.png",
|
"20260627_130837_image.png",
|
||||||
"20260627_130827_image.png",
|
"20260627_130827_image.png",
|
||||||
"_turntable/20260618_133729_image.png/views/view_021_315deg.png",
|
"_turntable/20260618_133729_image.png/views/view_021_315deg.png",
|
||||||
@@ -3482,7 +3438,6 @@
|
|||||||
"20260627_123945_8_20260627_123731_image.png",
|
"20260627_123945_8_20260627_123731_image.png",
|
||||||
"20260627_123935_image.png",
|
"20260627_123935_image.png",
|
||||||
"20260627_123928_7_20260627_123731_image.png",
|
"20260627_123928_7_20260627_123731_image.png",
|
||||||
"20260627_123911_6_20260627_123731_image.png",
|
|
||||||
"20260627_123855_5_20260627_123731_image.png",
|
"20260627_123855_5_20260627_123731_image.png",
|
||||||
"20260627_123838_4_20260627_123731_image.png",
|
"20260627_123838_4_20260627_123731_image.png",
|
||||||
"20260627_123822_3_20260627_123731_image.png",
|
"20260627_123822_3_20260627_123731_image.png",
|
||||||
@@ -3749,9 +3704,9 @@
|
|||||||
"_turntable/cg_7ec17537/views/view_003_045deg.png",
|
"_turntable/cg_7ec17537/views/view_003_045deg.png",
|
||||||
"_turntable/cg_7ec17537/views/view_002_030deg.png",
|
"_turntable/cg_7ec17537/views/view_002_030deg.png",
|
||||||
"_turntable/cg_7ec17537/views/view_001_015deg.png",
|
"_turntable/cg_7ec17537/views/view_001_015deg.png",
|
||||||
"_turntable/cg_7ec17537/views/20260628_033831_dup_20260628_033600_dup_view_000_000deg.png",
|
|
||||||
"_turntable/cg_7ec17537/views/20260628_033600_dup_view_000_000deg.png",
|
|
||||||
"_turntable/cg_7ec17537/views/view_000_000deg.png",
|
"_turntable/cg_7ec17537/views/view_000_000deg.png",
|
||||||
|
"_turntable/cg_7ec17537/views/20260628_033600_dup_view_000_000deg.png",
|
||||||
|
"_turntable/cg_7ec17537/views/20260628_033831_dup_20260628_033600_dup_view_000_000deg.png",
|
||||||
"_turntable/up_154b502c/views/view_023_345deg.png",
|
"_turntable/up_154b502c/views/view_023_345deg.png",
|
||||||
"_turntable/up_154b502c/views/view_022_330deg.png",
|
"_turntable/up_154b502c/views/view_022_330deg.png",
|
||||||
"_turntable/up_154b502c/views/view_021_315deg.png",
|
"_turntable/up_154b502c/views/view_021_315deg.png",
|
||||||
@@ -4287,8 +4242,6 @@
|
|||||||
"_turntable/cg_18040099/views/view_002_030deg.png",
|
"_turntable/cg_18040099/views/view_002_030deg.png",
|
||||||
"_turntable/cg_18040099/views/view_001_015deg.png",
|
"_turntable/cg_18040099/views/view_001_015deg.png",
|
||||||
"_turntable/cg_18040099/views/view_000_000deg.png",
|
"_turntable/cg_18040099/views/view_000_000deg.png",
|
||||||
"20260627_044054_pad_20260621_205908_0_20260621_205151_image.png",
|
|
||||||
"20260627_044022_pad_20260621_205908_0_20260621_205151_image.png",
|
|
||||||
"20260627_043800_dup_20260624_055327_image.nobg.nobg.png",
|
"20260627_043800_dup_20260624_055327_image.nobg.nobg.png",
|
||||||
"20260624_125511_dup_20260624_055327_image.nobg.nobg.png",
|
"20260624_125511_dup_20260624_055327_image.nobg.nobg.png",
|
||||||
"20260624_125511_dup_20260624_055327_image.nobg.nobg.nobg.png",
|
"20260624_125511_dup_20260624_055327_image.nobg.nobg.nobg.png",
|
||||||
@@ -4296,16 +4249,6 @@
|
|||||||
"20260627_043555_dup_20260624_055327_image.nobg.nobg.png",
|
"20260627_043555_dup_20260624_055327_image.nobg.nobg.png",
|
||||||
"20260627_043455_dup_20260624_055327_image.nobg.png",
|
"20260627_043455_dup_20260624_055327_image.nobg.png",
|
||||||
"20260624_125511_dup_20260624_055327_image.nobg.png",
|
"20260624_125511_dup_20260624_055327_image.nobg.png",
|
||||||
"20260627_043155_crop_20260627_042652_pad_20260627_042554_pad_20260627_042228_image.nobg.png",
|
|
||||||
"20260627_043055_pad_20260627_042228_image.nobg.png",
|
|
||||||
"20260627_042900_pad_20260627_042554_pad_20260627_042228_image.nobg.png",
|
|
||||||
"20260627_042835_crop_20260627_042652_pad_20260627_042554_pad_20260627_042228_image.nobg.png",
|
|
||||||
"20260627_042719_pad_20260627_042554_pad_20260627_042228_image.nobg.png",
|
|
||||||
"20260627_042652_pad_20260627_042554_pad_20260627_042228_image.nobg.png",
|
|
||||||
"20260627_042652_pad_20260627_042554_pad_20260627_042228_image.nobg.nobg.png",
|
|
||||||
"20260627_042620_pad_20260627_042228_image.nobg.png",
|
|
||||||
"20260627_042554_pad_20260627_042228_image.nobg.png",
|
|
||||||
"20260627_042554_pad_20260627_042228_image.nobg.nobg.png",
|
|
||||||
"20260627_042525_image.nobg.png",
|
"20260627_042525_image.nobg.png",
|
||||||
"20260627_042228_image.nobg.png",
|
"20260627_042228_image.nobg.png",
|
||||||
"20260627_042228_image.png",
|
"20260627_042228_image.png",
|
||||||
@@ -4317,8 +4260,6 @@
|
|||||||
"20260627_041236_image.nobg.png",
|
"20260627_041236_image.nobg.png",
|
||||||
"20260627_041236_image.png",
|
"20260627_041236_image.png",
|
||||||
"20260621_033259_image.nobg.png",
|
"20260621_033259_image.nobg.png",
|
||||||
"20260627_040944_pad_20260627_030425_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
|
|
||||||
"20260627_040926_pad_20260627_030425_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
|
|
||||||
"20260627_040817_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
|
"20260627_040817_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
|
||||||
"20260627_040738_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
|
"20260627_040738_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
|
||||||
"20260627_040304_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
|
"20260627_040304_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
|
||||||
@@ -4348,7 +4289,6 @@
|
|||||||
"20260627_030739_sc_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
|
"20260627_030739_sc_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
|
||||||
"cg_18040099_face.png",
|
"cg_18040099_face.png",
|
||||||
"20260627_030425_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
|
"20260627_030425_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
|
||||||
"20260627_030441_crop_20260627_030425_dup_20260624_125526_dup_20260624_055327_image.nobg.png",
|
|
||||||
"20260627_030425_dup_20260624_125526_dup_20260624_055327_image.nobg.nobg.png",
|
"20260627_030425_dup_20260624_125526_dup_20260624_055327_image.nobg.nobg.png",
|
||||||
"20260627_030344_sc_image.png",
|
"20260627_030344_sc_image.png",
|
||||||
"20260627_030145_sc_dup_20260624_055327_image.nobg.png",
|
"20260627_030145_sc_dup_20260624_055327_image.nobg.png",
|
||||||
@@ -4402,10 +4342,6 @@
|
|||||||
"_turntable/20260618_181600_image.png/views/view_003_090deg.png",
|
"_turntable/20260618_181600_image.png/views/view_003_090deg.png",
|
||||||
"_turntable/20260618_181600_image.png/views/view_002_060deg.png",
|
"_turntable/20260618_181600_image.png/views/view_002_060deg.png",
|
||||||
"_turntable/20260618_181600_image.png/views/view_001_030deg.png",
|
"_turntable/20260618_181600_image.png/views/view_001_030deg.png",
|
||||||
"_turntable/20260618_181600_image.png/views/view_000_000deg.png",
|
|
||||||
"20260626_234431_crop_20260626_231655_sc_image.nobg.png",
|
|
||||||
"20260626_231655_sc_image.nobg.png",
|
|
||||||
"20260618_181600_image.png_face.png",
|
|
||||||
"20260626_234334_dup_20260626_231655_sc_image.png",
|
"20260626_234334_dup_20260626_231655_sc_image.png",
|
||||||
"20260626_231655_sc_image.png",
|
"20260626_231655_sc_image.png",
|
||||||
"20260626_231349_sc_image.png",
|
"20260626_231349_sc_image.png",
|
||||||
@@ -4688,7 +4624,6 @@
|
|||||||
"20260618_015041_20260618_004313_20260616_021938_20160903_200935.nobg.nobg.png",
|
"20260618_015041_20260618_004313_20260616_021938_20160903_200935.nobg.nobg.png",
|
||||||
"20260626_124145_20160903_200935.jpg",
|
"20260626_124145_20160903_200935.jpg",
|
||||||
"cg_4671b6c4_face.png",
|
"cg_4671b6c4_face.png",
|
||||||
"20260626_152247_crop_20260626_152107_20160903_200935.nobg.nobg.nobg.nobg.png",
|
|
||||||
"20260626_152107_20160903_200935.nobg.png",
|
"20260626_152107_20160903_200935.nobg.png",
|
||||||
"20260626_151945_dup_20260626_151844_20160903_200935.nobg.png",
|
"20260626_151945_dup_20260626_151844_20160903_200935.nobg.png",
|
||||||
"20260626_151844_20160903_200935.nobg.png",
|
"20260626_151844_20160903_200935.nobg.png",
|
||||||
@@ -5056,25 +4991,10 @@
|
|||||||
"_turntable/b1.png/views/view_002_030deg.png",
|
"_turntable/b1.png/views/view_002_030deg.png",
|
||||||
"_turntable/b1.png/views/view_001_015deg.png",
|
"_turntable/b1.png/views/view_001_015deg.png",
|
||||||
"_turntable/b1.png/views/view_000_000deg.png",
|
"_turntable/b1.png/views/view_000_000deg.png",
|
||||||
"20260626_053719_pad_20260619_224737_img_14.nobg.png",
|
|
||||||
"20260626_053719_pad_20260619_224737_img_14.nobg.nobg.png",
|
|
||||||
"20260626_053754_pad_20260619_224737_img_14.png",
|
|
||||||
"20260626_053719_pad_20260619_224737_img_14.png",
|
|
||||||
"20260626_043734_7_20260618_053802_image.nobg.nobg.png",
|
"20260626_043734_7_20260618_053802_image.nobg.nobg.png",
|
||||||
"20260626_043915_crop_20260626_043734_7_20260618_053802_image.nobg.nobg.png",
|
|
||||||
"20260626_043734_7_20260618_053802_image.nobg.png",
|
"20260626_043734_7_20260618_053802_image.nobg.png",
|
||||||
"20260628_032038_dup_20260618_053932_7_20260618_053802_image.nobg.png",
|
|
||||||
"20260618_053932_7_20260618_053802_image.nobg.png",
|
"20260618_053932_7_20260618_053802_image.nobg.png",
|
||||||
"20260626_041550_sc_pad_20260626_034830_jb.nobg.png",
|
"20260628_032038_dup_20260618_053932_7_20260618_053802_image.nobg.png",
|
||||||
"20260626_041539_sc_pad_20260626_034830_jb.nobg.png",
|
|
||||||
"20260626_041515_sc_pad_20260626_034830_jb.nobg.png",
|
|
||||||
"20260626_041456_sc_pad_20260626_034830_jb.nobg.png",
|
|
||||||
"20260626_041410_sc_pad_20260626_034830_jb.nobg.png",
|
|
||||||
"20260626_041256_sc_pad_20260626_034830_jb.nobg.png",
|
|
||||||
"20260626_040655_sc_pad_20260626_034830_jb.nobg.png",
|
|
||||||
"20260626_040550_sc_pad_20260626_034830_jb.nobg.png",
|
|
||||||
"20260626_035131_pose_pad_20260626_034830_jb.nobg.png",
|
|
||||||
"20260626_035101_pad_20260626_034830_jb.nobg.png",
|
|
||||||
"20260626_034830_jb.nobg.png",
|
"20260626_034830_jb.nobg.png",
|
||||||
"20260626_034830_jb.png",
|
"20260626_034830_jb.png",
|
||||||
"20260626_034708_jb.png",
|
"20260626_034708_jb.png",
|
||||||
@@ -5094,11 +5014,6 @@
|
|||||||
"20260626_030918_jb1.png",
|
"20260626_030918_jb1.png",
|
||||||
"20260626_030908_1_20260618_050846_img_4.png",
|
"20260626_030908_1_20260618_050846_img_4.png",
|
||||||
"20260626_025708_2_20260625_205722_image.png",
|
"20260626_025708_2_20260625_205722_image.png",
|
||||||
"20260626_025338_pad_20260625_205913_8_20260625_205722_image.png",
|
|
||||||
"20260626_023528_pad_20260625_205913_8_20260625_205722_image.png",
|
|
||||||
"20260626_021942_pad_20260625_205913_8_20260625_205722_image.png",
|
|
||||||
"20260626_021533_pad_20260625_205828_4_20260625_205722_image.png",
|
|
||||||
"20260626_014422_pad_20260626_005644_dup_20260626_005504_Pasted image (9).nobg.png",
|
|
||||||
"20260626_005644_dup_20260626_005504_Pasted image (9).nobg.png",
|
"20260626_005644_dup_20260626_005504_Pasted image (9).nobg.png",
|
||||||
"20260626_005504_Pasted image (9).nobg.png",
|
"20260626_005504_Pasted image (9).nobg.png",
|
||||||
"20260626_005504_Pasted image (9).png",
|
"20260626_005504_Pasted image (9).png",
|
||||||
@@ -5112,7 +5027,6 @@
|
|||||||
"20260625_231601_sc_image.png",
|
"20260625_231601_sc_image.png",
|
||||||
"20260625_225929_sc_image.png",
|
"20260625_225929_sc_image.png",
|
||||||
"20260625_223304_p12.png",
|
"20260625_223304_p12.png",
|
||||||
"20260625_213330_pad_20260625_193323_image.png",
|
|
||||||
"20260625_210437_image.png",
|
"20260625_210437_image.png",
|
||||||
"20260625_210432_image.png",
|
"20260625_210432_image.png",
|
||||||
"20260625_210424_image.png",
|
"20260625_210424_image.png",
|
||||||
@@ -5210,8 +5124,8 @@
|
|||||||
"20260625_021910_image.png",
|
"20260625_021910_image.png",
|
||||||
"20260625_021702_sc_image.png",
|
"20260625_021702_sc_image.png",
|
||||||
"20260625_021615_sc_image.png",
|
"20260625_021615_sc_image.png",
|
||||||
"20260625_015711_sc_image.png",
|
|
||||||
"20260625_020212_dup_20260625_015711_sc_image.png",
|
"20260625_020212_dup_20260625_015711_sc_image.png",
|
||||||
|
"20260625_015711_sc_image.png",
|
||||||
"20260625_015711_sc_image.nobg.png",
|
"20260625_015711_sc_image.nobg.png",
|
||||||
"20260625_015900_sc_image.png",
|
"20260625_015900_sc_image.png",
|
||||||
"20260625_015413_sc_image.png",
|
"20260625_015413_sc_image.png",
|
||||||
@@ -5422,8 +5336,6 @@
|
|||||||
"20260624_164404_dup_20260624_163951_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
|
"20260624_164404_dup_20260624_163951_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
|
||||||
"20260624_164353_dup_20260624_163951_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
|
"20260624_164353_dup_20260624_163951_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
|
||||||
"20260624_162601_mr_0_20260622_093058_image.nobg.png",
|
"20260624_162601_mr_0_20260622_093058_image.nobg.png",
|
||||||
"20260624_164240_crop_20260624_162342_1_20260622_093058_image.png",
|
|
||||||
"20260624_164218_crop_20260624_162601_mr_0_20260622_093058_image.nobg.png",
|
|
||||||
"20260624_164059_dup_20260624_163951_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
|
"20260624_164059_dup_20260624_163951_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
|
||||||
"20260624_163951_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
|
"20260624_163951_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
|
||||||
"20260624_164116_dup_20260624_163951_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
|
"20260624_164116_dup_20260624_163951_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
|
||||||
@@ -9308,12 +9220,38 @@
|
|||||||
cancelManualCrop();
|
cancelManualCrop();
|
||||||
if (asCopy && d.new_filename) {
|
if (asCopy && d.new_filename) {
|
||||||
showToast('Cropped to copy', 'success');
|
showToast('Cropped to copy', 'success');
|
||||||
|
const newFn = d.new_filename;
|
||||||
|
|
||||||
|
// Clone metadata locally to avoid refresh blink
|
||||||
|
filePrompts[newFn] = filePrompts[fname];
|
||||||
|
filePoses[newFn] = filePoses[fname];
|
||||||
|
fileHasBg[newFn] = fileHasBg[fname];
|
||||||
|
fileHasClothing[newFn] = fileHasClothing[fname];
|
||||||
|
fileContentType[newFn] = fileContentType[fname] || 'image';
|
||||||
|
fileSourceRefs[newFn] = [fname];
|
||||||
|
fileHidden[newFn] = false;
|
||||||
|
fileIsSource[newFn] = false;
|
||||||
|
fileArchived[newFn] = false;
|
||||||
|
|
||||||
// Insert the cropped copy right after the original in the studio strip.
|
// Insert the cropped copy right after the original in the studio strip.
|
||||||
lbUrls.splice(lbIdx + 1, 0, IMAGE_FOLDER + d.new_filename + '?t=' + Date.now());
|
lbUrls.splice(lbIdx + 1, 0, IMAGE_FOLDER + newFn + '?t=' + Date.now());
|
||||||
lbNames.splice(lbIdx + 1, 0, d.new_filename);
|
lbNames.splice(lbIdx + 1, 0, newFn);
|
||||||
lbIdx = lbIdx + 1;
|
lbIdx = lbIdx + 1;
|
||||||
|
|
||||||
|
const group = groupData.get(lbCurrentGid);
|
||||||
|
if (group) {
|
||||||
|
const gIdx = group.names.indexOf(fname);
|
||||||
|
if (gIdx !== -1) {
|
||||||
|
group.urls.splice(gIdx + 1, 0, lbUrls[lbIdx]);
|
||||||
|
group.names.splice(gIdx + 1, 0, lbNames[lbIdx]);
|
||||||
|
group.visibleIdx = group.names.map((_, i) => i).filter(i => !fileHidden[group.names[i]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateStudio();
|
updateStudio();
|
||||||
refreshNow();
|
_followLatestFilename = newFn;
|
||||||
|
_followLatestGid = lbCurrentGid;
|
||||||
|
refreshNow(true);
|
||||||
} else {
|
} else {
|
||||||
showToast('Cropped', 'success');
|
showToast('Cropped', 'success');
|
||||||
lbUrls[lbIdx] = IMAGE_FOLDER + fname + '?t=' + Date.now();
|
lbUrls[lbIdx] = IMAGE_FOLDER + fname + '?t=' + Date.now();
|
||||||
@@ -9414,6 +9352,9 @@
|
|||||||
fileHasClothing[newFn] = fileHasClothing[fname];
|
fileHasClothing[newFn] = fileHasClothing[fname];
|
||||||
fileContentType[newFn] = 'image';
|
fileContentType[newFn] = 'image';
|
||||||
fileSourceRefs[newFn] = [fname];
|
fileSourceRefs[newFn] = [fname];
|
||||||
|
fileHidden[newFn] = false;
|
||||||
|
fileIsSource[newFn] = false;
|
||||||
|
fileArchived[newFn] = false;
|
||||||
|
|
||||||
lbUrls.splice(lbIdx + 1, 0, IMAGE_FOLDER + newFn + '?t=' + Date.now());
|
lbUrls.splice(lbIdx + 1, 0, IMAGE_FOLDER + newFn + '?t=' + Date.now());
|
||||||
lbNames.splice(lbIdx + 1, 0, newFn);
|
lbNames.splice(lbIdx + 1, 0, newFn);
|
||||||
@@ -9421,10 +9362,13 @@
|
|||||||
|
|
||||||
const group = groupData.get(lbCurrentGid);
|
const group = groupData.get(lbCurrentGid);
|
||||||
if (group) {
|
if (group) {
|
||||||
group.urls.splice(lbIdx, 0, lbUrls[lbIdx]);
|
const gIdx = group.names.indexOf(fname);
|
||||||
group.names.splice(lbIdx, 0, lbNames[lbIdx]);
|
if (gIdx !== -1) {
|
||||||
|
group.urls.splice(gIdx + 1, 0, lbUrls[lbIdx]);
|
||||||
|
group.names.splice(gIdx + 1, 0, lbNames[lbIdx]);
|
||||||
group.visibleIdx = group.names.map((_, i) => i).filter(i => !fileHidden[group.names[i]]);
|
group.visibleIdx = group.names.map((_, i) => i).filter(i => !fileHidden[group.names[i]]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateStudio();
|
updateStudio();
|
||||||
_followLatestFilename = newFn;
|
_followLatestFilename = newFn;
|
||||||
@@ -9464,20 +9408,24 @@
|
|||||||
fileHasClothing[newFn] = fileHasClothing[fname];
|
fileHasClothing[newFn] = fileHasClothing[fname];
|
||||||
fileContentType[newFn] = fileContentType[fname] || 'image';
|
fileContentType[newFn] = fileContentType[fname] || 'image';
|
||||||
fileSourceRefs[newFn] = [fname];
|
fileSourceRefs[newFn] = [fname];
|
||||||
|
fileHidden[newFn] = false;
|
||||||
|
fileIsSource[newFn] = false;
|
||||||
|
fileArchived[newFn] = false;
|
||||||
|
|
||||||
// Insert the duplicate right after the current image in the studio strip
|
// Append the duplicate to the end of the filmstrip
|
||||||
lbUrls.splice(lbIdx + 1, 0, IMAGE_FOLDER + newFn + '?t=' + Date.now());
|
lbUrls.push(IMAGE_FOLDER + newFn + '?t=' + Date.now());
|
||||||
lbNames.splice(lbIdx + 1, 0, newFn);
|
lbNames.push(newFn);
|
||||||
lbIdx = lbIdx + 1;
|
lbIdx = lbUrls.length - 1;
|
||||||
|
|
||||||
const group = groupData.get(lbCurrentGid);
|
const group = groupData.get(lbCurrentGid);
|
||||||
if (group) {
|
if (group) {
|
||||||
group.urls.splice(lbIdx, 0, lbUrls[lbIdx]);
|
group.urls.push(lbUrls[lbIdx]);
|
||||||
group.names.splice(lbIdx, 0, lbNames[lbIdx]);
|
group.names.push(lbNames[lbIdx]);
|
||||||
group.visibleIdx = group.names.map((_, i) => i).filter(i => !fileHidden[group.names[i]]);
|
group.visibleIdx = group.names.map((_, i) => i).filter(i => !fileHidden[group.names[i]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStudio();
|
updateStudio();
|
||||||
|
_followLatestFilename = newFn;
|
||||||
_followLatestGid = lbCurrentGid;
|
_followLatestGid = lbCurrentGid;
|
||||||
refreshNow(true);
|
refreshNow(true);
|
||||||
} else {
|
} else {
|
||||||
@@ -10507,21 +10455,50 @@
|
|||||||
const fresh = groupData.get(lbCurrentGid);
|
const fresh = groupData.get(lbCurrentGid);
|
||||||
if (fresh) {
|
if (fresh) {
|
||||||
const curName = lbNames[lbIdx];
|
const curName = lbNames[lbIdx];
|
||||||
const newIdx = fresh.names.indexOf(curName);
|
|
||||||
// Snapshot ref filenames before overwriting lbNames
|
// Snapshot ref filenames before overwriting lbNames
|
||||||
const oldRefNames = _sbRefIndices.map(i => lbNames[i]).filter(Boolean);
|
const oldRefNames = _sbRefIndices.map(i => lbNames[i]).filter(Boolean);
|
||||||
lbUrls = fresh.urls;
|
|
||||||
lbNames = fresh.names;
|
// Optimistic merge: retain any locally added names/urls that are not yet in the freshly loaded server list
|
||||||
// Keep lbIdx stable when current image not yet in fresh data
|
const mergedNames = [...fresh.names];
|
||||||
// (e.g. duplicate just created — server JSON may lag by the debounce window).
|
const mergedUrls = [...fresh.urls];
|
||||||
|
|
||||||
|
for (let i = 0; i < lbNames.length; i++) {
|
||||||
|
const localName = lbNames[i];
|
||||||
|
if (!mergedNames.includes(localName)) {
|
||||||
|
if (i === 0) {
|
||||||
|
mergedNames.unshift(localName);
|
||||||
|
mergedUrls.unshift(lbUrls[i]);
|
||||||
|
} else {
|
||||||
|
const prevLocal = lbNames[i - 1];
|
||||||
|
const freshIdx = mergedNames.indexOf(prevLocal);
|
||||||
|
if (freshIdx !== -1) {
|
||||||
|
mergedNames.splice(freshIdx + 1, 0, localName);
|
||||||
|
mergedUrls.splice(freshIdx + 1, 0, lbUrls[i]);
|
||||||
|
} else {
|
||||||
|
mergedNames.push(localName);
|
||||||
|
mergedUrls.push(lbUrls[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update groupData as well so that other components have correct metadata
|
||||||
|
fresh.names = mergedNames;
|
||||||
|
fresh.urls = mergedUrls;
|
||||||
|
fresh.visibleIdx = fresh.names.map((_, idx) => idx).filter(idx => !fileHidden[fresh.names[idx]]);
|
||||||
|
|
||||||
|
lbUrls = mergedUrls;
|
||||||
|
lbNames = mergedNames;
|
||||||
|
|
||||||
|
const newIdx = lbNames.indexOf(curName);
|
||||||
if (newIdx >= 0) {
|
if (newIdx >= 0) {
|
||||||
lbIdx = newIdx;
|
lbIdx = newIdx;
|
||||||
} else if (lbIdx >= fresh.names.length) {
|
} else if (lbIdx >= lbNames.length) {
|
||||||
lbIdx = fresh.names.length - 1;
|
lbIdx = lbNames.length - 1;
|
||||||
}
|
}
|
||||||
// Re-map multi-ref selections to new positions by filename
|
// Re-map multi-ref selections to new positions by filename
|
||||||
_sbRefIndices = oldRefNames
|
_sbRefIndices = oldRefNames
|
||||||
.map(n => fresh.names.indexOf(n))
|
.map(n => lbNames.indexOf(n))
|
||||||
.filter(i => i >= 0);
|
.filter(i => i >= 0);
|
||||||
updateStudio();
|
updateStudio();
|
||||||
}
|
}
|
||||||
@@ -11266,13 +11243,32 @@
|
|||||||
const r = await fetch(`${API}/images/${encodeURIComponent(filename)}/set-preferred`, {method:'POST'});
|
const r = await fetch(`${API}/images/${encodeURIComponent(filename)}/set-preferred`, {method:'POST'});
|
||||||
if (!r.ok) { console.error('set-preferred failed', r.status); return; }
|
if (!r.ok) { console.error('set-preferred failed', r.status); return; }
|
||||||
const data = await r.json();
|
const data = await r.json();
|
||||||
// Update sort orders locally
|
|
||||||
const gid = data.group_id;
|
const gid = data.group_id;
|
||||||
fileSortOrders[filename] = 0;
|
fileSortOrders[filename] = 0;
|
||||||
|
const fresh = groupData.get(gid);
|
||||||
|
if (fresh) {
|
||||||
|
fresh.names.forEach(n => {
|
||||||
|
if (n !== filename) {
|
||||||
|
fileSortOrders[n] = (fileSortOrders[n] || 0) + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const paired = fresh.names.map((name, i) => ({ name, url: fresh.urls[i] }));
|
||||||
|
paired.sort((a, b) => {
|
||||||
|
const oa = fileSortOrders[a.name] ?? 9999;
|
||||||
|
const ob = fileSortOrders[b.name] ?? 9999;
|
||||||
|
if (oa !== ob) return oa - ob;
|
||||||
|
return a.name.length - b.name.length;
|
||||||
|
});
|
||||||
|
fresh.names = paired.map(p => p.name);
|
||||||
|
fresh.urls = paired.map(p => p.url);
|
||||||
|
fresh.visibleIdx = fresh.names.map((_, i) => i).filter(i => !fileHidden[fresh.names[i]]);
|
||||||
|
}
|
||||||
|
|
||||||
// Re-open picker to reflect new order
|
// Re-open picker to reflect new order
|
||||||
showVariantPicker(filename);
|
showVariantPicker(gid);
|
||||||
// Poll for the freshly-extracted face crop (async on the server).
|
|
||||||
loadFaceThumb(gid, 8);
|
loadFaceThumb(gid, 8);
|
||||||
|
setTimeout(refreshNow, 2000);
|
||||||
} catch(e) { console.error(e); }
|
} catch(e) { console.error(e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12037,6 +12033,9 @@
|
|||||||
// Refresh UI as each task completes; auto-follow to newest image
|
// Refresh UI as each task completes; auto-follow to newest image
|
||||||
if (job.done > prevDone) {
|
if (job.done > prevDone) {
|
||||||
prevDone = job.done;
|
prevDone = job.done;
|
||||||
|
if (job.latest_output) {
|
||||||
|
_followLatestFilename = job.latest_output;
|
||||||
|
}
|
||||||
_followLatestGid = lbCurrentGid;
|
_followLatestGid = lbCurrentGid;
|
||||||
refreshNow(true);
|
refreshNow(true);
|
||||||
}
|
}
|
||||||
@@ -12057,6 +12056,9 @@
|
|||||||
_sbSelectedAngles.clear();
|
_sbSelectedAngles.clear();
|
||||||
_sbPoseEdits = {};
|
_sbPoseEdits = {};
|
||||||
_sbGenJobId = null;
|
_sbGenJobId = null;
|
||||||
|
if (job.latest_output) {
|
||||||
|
_followLatestFilename = job.latest_output;
|
||||||
|
}
|
||||||
_followLatestGid = lbCurrentGid;
|
_followLatestGid = lbCurrentGid;
|
||||||
refreshNow(true);
|
refreshNow(true);
|
||||||
renderSidebarGenerate();
|
renderSidebarGenerate();
|
||||||
@@ -13150,10 +13152,43 @@
|
|||||||
const label = d.used_sam2 ? 'Done (SAM2)' : 'Done (rembg)';
|
const label = d.used_sam2 ? 'Done (SAM2)' : 'Done (rembg)';
|
||||||
if (statusEl) statusEl.textContent = label;
|
if (statusEl) statusEl.textContent = label;
|
||||||
showToast('Background removed', 'success');
|
showToast('Background removed', 'success');
|
||||||
refreshNow(true);
|
|
||||||
// Build URL same way as all gallery images: IMAGE_FOLDER + filename
|
const newFn = d.nobg_filename;
|
||||||
const nobgUrl = IMAGE_FOLDER + d.nobg_filename + '?t=' + Date.now();
|
const nobgUrl = IMAGE_FOLDER + newFn + '?t=' + Date.now();
|
||||||
_nobgSidecars.set(fname, nobgUrl);
|
_nobgSidecars.set(fname, nobgUrl);
|
||||||
|
|
||||||
|
// Clone metadata locally to avoid refresh blink
|
||||||
|
filePrompts[newFn] = filePrompts[fname];
|
||||||
|
filePoses[newFn] = (filePoses[fname] || '') + ' (No BG)';
|
||||||
|
fileHasBg[newFn] = false;
|
||||||
|
fileHasClothing[newFn] = fileHasClothing[fname];
|
||||||
|
fileContentType[newFn] = 'image';
|
||||||
|
fileSourceRefs[newFn] = [fname];
|
||||||
|
fileHidden[newFn] = false;
|
||||||
|
fileIsSource[newFn] = false;
|
||||||
|
fileArchived[newFn] = false;
|
||||||
|
|
||||||
|
// Insert the sidecar right after the current image in the studio strip
|
||||||
|
lbUrls.splice(lbIdx + 1, 0, nobgUrl);
|
||||||
|
lbNames.splice(lbIdx + 1, 0, newFn);
|
||||||
|
|
||||||
|
const group = groupData.get(lbCurrentGid);
|
||||||
|
if (group) {
|
||||||
|
const gIdx = group.names.indexOf(fname);
|
||||||
|
if (gIdx !== -1) {
|
||||||
|
group.urls.splice(gIdx + 1, 0, nobgUrl);
|
||||||
|
group.names.splice(gIdx + 1, 0, newFn);
|
||||||
|
group.visibleIdx = group.names.map((_, i) => i).filter(i => !fileHidden[group.names[i]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move to the new no-bg image
|
||||||
|
lbIdx = lbIdx + 1;
|
||||||
|
updateStudio();
|
||||||
|
_followLatestFilename = newFn;
|
||||||
|
_followLatestGid = lbCurrentGid;
|
||||||
|
refreshNow(true);
|
||||||
|
|
||||||
// Preload before swap to avoid black flash
|
// Preload before swap to avoid black flash
|
||||||
const preload = new Image();
|
const preload = new Image();
|
||||||
preload.onload = () => {
|
preload.onload = () => {
|
||||||
@@ -13197,16 +13232,10 @@
|
|||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
if (statusEl) statusEl.textContent = 'Done (rembg)';
|
if (statusEl) statusEl.textContent = 'Done (rembg)';
|
||||||
showToast('Background removed (rembg)', 'success');
|
showToast('Background removed (rembg)', 'success');
|
||||||
refreshNow(true);
|
fileHasBg[fname] = false;
|
||||||
const nobgUrl = IMAGE_FOLDER + d.nobg_filename + '?t=' + Date.now();
|
_bustStudioImage(fname);
|
||||||
_nobgSidecars.set(fname, nobgUrl);
|
|
||||||
const preload = new Image();
|
|
||||||
preload.onload = () => {
|
|
||||||
const img = document.getElementById('lbImg');
|
|
||||||
if (img) img.src = nobgUrl;
|
|
||||||
toggleCheckerboard(true);
|
toggleCheckerboard(true);
|
||||||
};
|
refreshNow(true);
|
||||||
preload.src = nobgUrl;
|
|
||||||
}
|
}
|
||||||
} catch (e) { if (statusEl) statusEl.textContent = 'Error: ' + e; }
|
} catch (e) { if (statusEl) statusEl.textContent = 'Error: ' + e; }
|
||||||
if (btn) btn.disabled = false;
|
if (btn) btn.disabled = false;
|
||||||
@@ -13248,25 +13277,42 @@
|
|||||||
try {
|
try {
|
||||||
const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/set-preferred`, {method:'POST'});
|
const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/set-preferred`, {method:'POST'});
|
||||||
if (!r.ok) return;
|
if (!r.ok) return;
|
||||||
|
|
||||||
|
// Update sort orders locally to match backend logic
|
||||||
|
const gid = lbCurrentGid;
|
||||||
fileSortOrders[fname] = 0;
|
fileSortOrders[fname] = 0;
|
||||||
const url = lbUrls[lbIdx];
|
const fresh = groupData.get(gid);
|
||||||
const otherUrls = lbUrls.filter((_, i) => i !== lbIdx);
|
if (fresh) {
|
||||||
const otherNames = lbNames.filter((_, i) => i !== lbIdx);
|
fresh.names.forEach(n => {
|
||||||
lbUrls = [url, ...otherUrls];
|
if (n !== fname) {
|
||||||
lbNames = [fname, ...otherNames];
|
fileSortOrders[n] = (fileSortOrders[n] || 0) + 1;
|
||||||
lbIdx = 0;
|
|
||||||
updateStudio();
|
|
||||||
refreshNow(true);
|
|
||||||
// Fire-and-forget face extraction for face-book
|
|
||||||
fetch(`${API}/images/${encodeURIComponent(fname)}/extract-face`, { method: 'POST' })
|
|
||||||
.then(r => r.ok ? r.json() : null)
|
|
||||||
.then(d => {
|
|
||||||
if (d?.status === 'queued') {
|
|
||||||
showToast('Extracting face reference…', 'info');
|
|
||||||
loadFaceThumb(lbCurrentGid, 8); // poll until the crop lands
|
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
.catch(() => {});
|
// Re-sort local group data
|
||||||
|
const paired = fresh.names.map((name, i) => ({ name, url: fresh.urls[i] }));
|
||||||
|
paired.sort((a, b) => {
|
||||||
|
const oa = fileSortOrders[a.name] ?? 9999;
|
||||||
|
const ob = fileSortOrders[b.name] ?? 9999;
|
||||||
|
if (oa !== ob) return oa - ob;
|
||||||
|
return a.name.length - b.name.length;
|
||||||
|
});
|
||||||
|
fresh.names = paired.map(p => p.name);
|
||||||
|
fresh.urls = paired.map(p => p.url);
|
||||||
|
fresh.visibleIdx = fresh.names.map((_, i) => i).filter(i => !fileHidden[fresh.names[i]]);
|
||||||
|
|
||||||
|
lbUrls = fresh.urls;
|
||||||
|
lbNames = fresh.names;
|
||||||
|
lbIdx = lbNames.indexOf(fname);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStudio();
|
||||||
|
showToast('Set as preferred', 'success');
|
||||||
|
|
||||||
|
// Delayed refresh to reconcile with server without disrupting local state
|
||||||
|
setTimeout(refreshNow, 2000);
|
||||||
|
|
||||||
|
// Poll for the freshly-extracted face crop (async on the server).
|
||||||
|
loadFaceThumb(gid, 8);
|
||||||
} catch(e) { console.error(e); }
|
} catch(e) { console.error(e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -118,6 +118,43 @@ IDLE_THRESHOLD = 45 # seconds of inactivity before background gen starts
|
|||||||
IDLE_CHECK_INTERVAL = 4 # polling interval (seconds)
|
IDLE_CHECK_INTERVAL = 4 # polling interval (seconds)
|
||||||
|
|
||||||
|
|
||||||
|
# --- File Metadata In-Memory Cache (resolves performance bottlenecks on /mnt/zim) ---
|
||||||
|
_file_meta_cache = {} # filename -> (exists, mtime, cache_time)
|
||||||
|
_file_meta_cache_lock = threading.Lock()
|
||||||
|
|
||||||
|
def _get_cached_file_meta(filename: str, output_dir: str):
|
||||||
|
now = time.time()
|
||||||
|
with _file_meta_cache_lock:
|
||||||
|
cached = _file_meta_cache.get(filename)
|
||||||
|
if cached and (now - cached[2] < 5.0):
|
||||||
|
return cached[0], cached[1]
|
||||||
|
|
||||||
|
fpath = os.path.join(output_dir, filename)
|
||||||
|
exists = os.path.exists(fpath)
|
||||||
|
mtime = 0.0
|
||||||
|
if exists:
|
||||||
|
try:
|
||||||
|
mtime = os.path.getmtime(fpath)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
with _file_meta_cache_lock:
|
||||||
|
_file_meta_cache[filename] = (exists, mtime, now)
|
||||||
|
return exists, mtime
|
||||||
|
|
||||||
|
def _update_cached_file_meta(filename: str, exists: bool = True, mtime: float = None):
|
||||||
|
if mtime is None:
|
||||||
|
mtime = time.time()
|
||||||
|
with _file_meta_cache_lock:
|
||||||
|
_file_meta_cache[filename] = (exists, mtime, time.time())
|
||||||
|
|
||||||
|
def _clear_cached_file_meta(filename: str):
|
||||||
|
with _file_meta_cache_lock:
|
||||||
|
if filename in _file_meta_cache:
|
||||||
|
del _file_meta_cache[filename]
|
||||||
|
|
||||||
|
|
||||||
|
_last_preloaded_images_set = None
|
||||||
|
|
||||||
@app.middleware("http")
|
@app.middleware("http")
|
||||||
async def _track_activity(request, call_next):
|
async def _track_activity(request, call_next):
|
||||||
global _last_request_time
|
global _last_request_time
|
||||||
@@ -126,6 +163,7 @@ async def _track_activity(request, call_next):
|
|||||||
|
|
||||||
def _sync_preloaded_images():
|
def _sync_preloaded_images():
|
||||||
"""Update PRELOADED_IMAGES in car.html (both source and output) to match current DB state."""
|
"""Update PRELOADED_IMAGES in car.html (both source and output) to match current DB state."""
|
||||||
|
global _last_preloaded_images_set
|
||||||
try:
|
try:
|
||||||
output_dir = _load_output_dir()
|
output_dir = _load_output_dir()
|
||||||
paths = [
|
paths = [
|
||||||
@@ -134,11 +172,21 @@ def _sync_preloaded_images():
|
|||||||
]
|
]
|
||||||
|
|
||||||
persons = database.list_persons(include_archived=False)
|
persons = database.list_persons(include_archived=False)
|
||||||
# Only include if file actually exists on disk
|
# Only include if file actually exists on disk, using the fast cache
|
||||||
db_images = [p[0] for p in persons if os.path.exists(os.path.join(output_dir, p[0]))]
|
db_images = []
|
||||||
|
for p in persons:
|
||||||
|
exists, mtime = _get_cached_file_meta(p[0], output_dir)
|
||||||
|
if exists:
|
||||||
|
db_images.append(p[0])
|
||||||
|
|
||||||
# Sort by mtime, newest first
|
# Sort by mtime, newest first
|
||||||
db_images.sort(key=lambda x: os.path.getmtime(os.path.join(output_dir, x)), reverse=True)
|
db_images.sort(key=lambda x: _get_cached_file_meta(x, output_dir)[1], reverse=True)
|
||||||
|
|
||||||
|
# Avoid redundant HTML rewrites if the preloaded set is unchanged
|
||||||
|
current_set = set(db_images)
|
||||||
|
if _last_preloaded_images_set is not None and _last_preloaded_images_set == current_set:
|
||||||
|
return
|
||||||
|
_last_preloaded_images_set = current_set
|
||||||
|
|
||||||
images_json = json.dumps(db_images, indent=12).strip()
|
images_json = json.dumps(db_images, indent=12).strip()
|
||||||
# Format for JS insertion
|
# Format for JS insertion
|
||||||
@@ -195,7 +243,8 @@ def _watch_frontend():
|
|||||||
def _load_wireframe_dir() -> str:
|
def _load_wireframe_dir() -> str:
|
||||||
with open(CONFIG_PATH, "r") as f:
|
with open(CONFIG_PATH, "r") as f:
|
||||||
conf = json.load(f)
|
conf = json.load(f)
|
||||||
return conf.get("wireframe_dir", "/mnt/zim/tour-comfy/wireframe")
|
d = conf.get("wireframe_dir", "/mnt/zim/tour-comfy/wireframe")
|
||||||
|
return os.path.expanduser(d)
|
||||||
|
|
||||||
|
|
||||||
def _load_faceswap_model_path() -> str:
|
def _load_faceswap_model_path() -> str:
|
||||||
@@ -1055,9 +1104,9 @@ def _faceswap_worker_ff(job_id: str, model_filename: str, video_name: str,
|
|||||||
source_refs=json.dumps([model_filename]),
|
source_refs=json.dumps([model_filename]),
|
||||||
sort_order=database.get_next_sort_order(group_id),
|
sort_order=database.get_next_sort_order(group_id),
|
||||||
)
|
)
|
||||||
|
_invalidate_static()
|
||||||
jobs[job_id]['status'] = 'done'
|
jobs[job_id]['status'] = 'done'
|
||||||
jobs[job_id]['output'] = out_name
|
jobs[job_id]['output'] = out_name
|
||||||
_invalidate_static()
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'[faceswap-ff] error: {e}')
|
print(f'[faceswap-ff] error: {e}')
|
||||||
@@ -1270,7 +1319,7 @@ jobs: dict[str, dict] = {}
|
|||||||
def _load_output_dir() -> str:
|
def _load_output_dir() -> str:
|
||||||
with open(CONFIG_PATH, "r") as f:
|
with open(CONFIG_PATH, "r") as f:
|
||||||
conf = json.load(f)
|
conf = json.load(f)
|
||||||
d = conf["output_dir"]
|
d = os.path.expanduser(conf["output_dir"])
|
||||||
if not os.path.isabs(d):
|
if not os.path.isabs(d):
|
||||||
d = os.path.normpath(os.path.join(os.path.dirname(CONFIG_PATH), "..", d))
|
d = os.path.normpath(os.path.join(os.path.dirname(CONFIG_PATH), "..", d))
|
||||||
return d
|
return d
|
||||||
@@ -1363,8 +1412,8 @@ def _write_all_static() -> None:
|
|||||||
db_images = []
|
db_images = []
|
||||||
archived_count = 0
|
archived_count = 0
|
||||||
for p in persons:
|
for p in persons:
|
||||||
fpath = os.path.join(output_dir, p[0])
|
exists, mtime = _get_cached_file_meta(p[0], output_dir)
|
||||||
if not os.path.exists(fpath):
|
if not exists:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
is_archived = bool(p[14]) if p[14] else False
|
is_archived = bool(p[14]) if p[14] else False
|
||||||
@@ -1406,7 +1455,7 @@ def _write_all_static() -> None:
|
|||||||
print(f"[static] write_all: {len(db_images)} total images, {archived_count} archived")
|
print(f"[static] write_all: {len(db_images)} total images, {archived_count} archived")
|
||||||
try:
|
try:
|
||||||
db_images.sort(
|
db_images.sort(
|
||||||
key=lambda x: os.path.getmtime(os.path.join(output_dir, x["filename"])),
|
key=lambda x: _get_cached_file_meta(x["filename"], output_dir)[1],
|
||||||
reverse=True,
|
reverse=True,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -1705,6 +1754,7 @@ def _batch_worker(job_id: str, filenames: list, prompts: list[str], poses: list,
|
|||||||
except Exception as db_err:
|
except Exception as db_err:
|
||||||
print(f"Database error in batch worker: {db_err}")
|
print(f"Database error in batch worker: {db_err}")
|
||||||
|
|
||||||
|
jobs[job_id]["latest_output"] = out_name
|
||||||
jobs[job_id]["done"] += 1
|
jobs[job_id]["done"] += 1
|
||||||
# Regenerate static JSON so the frontend's polling picks up the
|
# Regenerate static JSON so the frontend's polling picks up the
|
||||||
# new image immediately (progressive refresh, not just at the end).
|
# new image immediately (progressive refresh, not just at the end).
|
||||||
@@ -1806,6 +1856,7 @@ def _multi_ref_worker(job_id: str, filenames: list[str], prompts: list[str], pos
|
|||||||
except Exception as db_err:
|
except Exception as db_err:
|
||||||
print(f"DB error in multi-ref: {db_err}")
|
print(f"DB error in multi-ref: {db_err}")
|
||||||
|
|
||||||
|
jobs[job_id]["latest_output"] = out_name
|
||||||
jobs[job_id]["done"] += 1
|
jobs[job_id]["done"] += 1
|
||||||
# Regenerate static JSON so the frontend's polling picks up the new
|
# Regenerate static JSON so the frontend's polling picks up the new
|
||||||
# image immediately (progressive refresh, matching _batch_worker).
|
# image immediately (progressive refresh, matching _batch_worker).
|
||||||
@@ -2044,8 +2095,8 @@ def list_images(archived: bool = False):
|
|||||||
# source_refs, has_clothing, content_type, faceswap_source_video, archived
|
# source_refs, has_clothing, content_type, faceswap_source_video, archived
|
||||||
db_images = []
|
db_images = []
|
||||||
for p in persons:
|
for p in persons:
|
||||||
fpath = os.path.join(output_dir, p[0])
|
exists, mtime = _get_cached_file_meta(p[0], output_dir)
|
||||||
if not os.path.exists(fpath):
|
if not exists:
|
||||||
continue
|
continue
|
||||||
db_images.append({
|
db_images.append({
|
||||||
"filename": p[0],
|
"filename": p[0],
|
||||||
@@ -2065,7 +2116,7 @@ def list_images(archived: bool = False):
|
|||||||
"archived": bool(p[14]) if p[14] else False,
|
"archived": bool(p[14]) if p[14] else False,
|
||||||
})
|
})
|
||||||
db_images.sort(
|
db_images.sort(
|
||||||
key=lambda x: os.path.getmtime(os.path.join(output_dir, x["filename"])),
|
key=lambda x: _get_cached_file_meta(x["filename"], output_dir)[1],
|
||||||
reverse=True,
|
reverse=True,
|
||||||
)
|
)
|
||||||
return {"images": db_images}
|
return {"images": db_images}
|
||||||
@@ -2079,7 +2130,7 @@ def list_images(archived: bool = False):
|
|||||||
if f.lower().endswith(all_extensions)
|
if f.lower().endswith(all_extensions)
|
||||||
and not (f.lower().endswith('.jpg') and os.path.splitext(f)[0] in video_stems)
|
and not (f.lower().endswith('.jpg') and os.path.splitext(f)[0] in video_stems)
|
||||||
]
|
]
|
||||||
files.sort(key=lambda x: os.path.getmtime(os.path.join(output_dir, x)), reverse=True)
|
files.sort(key=lambda x: _get_cached_file_meta(x, output_dir)[1], reverse=True)
|
||||||
return {"images": [{"filename": f} for f in files]}
|
return {"images": [{"filename": f} for f in files]}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(500, str(e))
|
raise HTTPException(500, str(e))
|
||||||
@@ -2568,7 +2619,7 @@ def merge_groups(req: MergeRequest):
|
|||||||
|
|
||||||
# Write synchronously: the frontend reloads images.json immediately after this
|
# Write synchronously: the frontend reloads images.json immediately after this
|
||||||
# returns, so an async rebuild would race and show the pre-merge grouping.
|
# returns, so an async rebuild would race and show the pre-merge grouping.
|
||||||
_write_all_static()
|
_invalidate_static()
|
||||||
return {"group_id": gid, "files": req.filenames}
|
return {"group_id": gid, "files": req.filenames}
|
||||||
|
|
||||||
|
|
||||||
@@ -2584,7 +2635,7 @@ def extract_from_group(req: ExtractRequest):
|
|||||||
except Exception as db_err:
|
except Exception as db_err:
|
||||||
print(f"Database error in extract: {db_err}")
|
print(f"Database error in extract: {db_err}")
|
||||||
|
|
||||||
_write_all_static()
|
_invalidate_static()
|
||||||
return {"filename": req.filename}
|
return {"filename": req.filename}
|
||||||
|
|
||||||
|
|
||||||
@@ -2762,7 +2813,7 @@ def set_privacy_lock():
|
|||||||
global _privacy_locked
|
global _privacy_locked
|
||||||
with _privacy_lock:
|
with _privacy_lock:
|
||||||
_privacy_locked = True
|
_privacy_locked = True
|
||||||
_write_all_static()
|
_invalidate_static()
|
||||||
return {"status": "locked"}
|
return {"status": "locked"}
|
||||||
|
|
||||||
|
|
||||||
@@ -2771,7 +2822,7 @@ def set_privacy_unlock():
|
|||||||
global _privacy_locked
|
global _privacy_locked
|
||||||
with _privacy_lock:
|
with _privacy_lock:
|
||||||
_privacy_locked = False
|
_privacy_locked = False
|
||||||
_write_all_static()
|
_invalidate_static()
|
||||||
return {"status": "unlocked"}
|
return {"status": "unlocked"}
|
||||||
|
|
||||||
|
|
||||||
@@ -2827,7 +2878,8 @@ def _extract_face_bg(filename: str, fpath: str):
|
|||||||
group_id = person[1] if person else None
|
group_id = person[1] if person else None
|
||||||
gid_tag = (group_id or "face").replace("/", "_")
|
gid_tag = (group_id or "face").replace("/", "_")
|
||||||
face_fname = f"{gid_tag}_face.png"
|
face_fname = f"{gid_tag}_face.png"
|
||||||
face_path = os.path.join(os.path.dirname(fpath), face_fname)
|
output_dir = _load_output_dir()
|
||||||
|
face_path = os.path.join(output_dir, face_fname)
|
||||||
cropped.save(face_path)
|
cropped.save(face_path)
|
||||||
face_embed = face.normed_embedding.tolist() if hasattr(face, 'normed_embedding') and face.normed_embedding is not None else None
|
face_embed = face.normed_embedding.tolist() if hasattr(face, 'normed_embedding') and face.normed_embedding is not None else None
|
||||||
database.upsert_person(face_fname, filepath=face_path, group_id=group_id,
|
database.upsert_person(face_fname, filepath=face_path, group_id=group_id,
|
||||||
@@ -2837,6 +2889,7 @@ def _extract_face_bg(filename: str, fpath: str):
|
|||||||
hidden=True,
|
hidden=True,
|
||||||
tags=["FACE"])
|
tags=["FACE"])
|
||||||
print(f"[extract-face] saved {face_fname}" + (" + face embedding" if face_embed else ""))
|
print(f"[extract-face] saved {face_fname}" + (" + face embedding" if face_embed else ""))
|
||||||
|
_invalidate_static()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[extract-face] error for {filename}: {e}")
|
print(f"[extract-face] error for {filename}: {e}")
|
||||||
|
|
||||||
@@ -2933,6 +2986,9 @@ def upload_image(
|
|||||||
shutil.copyfileobj(image.file, f)
|
shutil.copyfileobj(image.file, f)
|
||||||
|
|
||||||
# Fast path: add to existing group without pose generation
|
# Fast path: add to existing group without pose generation
|
||||||
|
if not group_id:
|
||||||
|
group_id = naming.get_base_name(filename)
|
||||||
|
|
||||||
if group_id and skip_poses:
|
if group_id and skip_poses:
|
||||||
sort_order = database.get_next_sort_order(group_id)
|
sort_order = database.get_next_sort_order(group_id)
|
||||||
database.upsert_person(filename, filepath=file_path, group_id=group_id,
|
database.upsert_person(filename, filepath=file_path, group_id=group_id,
|
||||||
@@ -3055,8 +3111,9 @@ def set_image_preferred(filename: str):
|
|||||||
others = [r[0] for r in rows if r[0] != filename]
|
others = [r[0] for r in rows if r[0] != filename]
|
||||||
database.set_group_order(group_id, [filename] + others)
|
database.set_group_order(group_id, [filename] + others)
|
||||||
_invalidate_static()
|
_invalidate_static()
|
||||||
fpath = os.path.join(_load_output_dir(), filename)
|
# Use stored filepath if available (absolute), else resolve relative to output_dir
|
||||||
if os.path.exists(fpath):
|
fpath = person[5] if (len(person) > 5 and person[5]) else os.path.join(_load_output_dir(), filename)
|
||||||
|
if fpath and os.path.exists(fpath):
|
||||||
_face_executor.submit(_extract_face_bg, filename, fpath)
|
_face_executor.submit(_extract_face_bg, filename, fpath)
|
||||||
return {"filename": filename, "group_id": group_id}
|
return {"filename": filename, "group_id": group_id}
|
||||||
|
|
||||||
@@ -3064,9 +3121,9 @@ def set_image_preferred(filename: str):
|
|||||||
@app.post("/images/{filename:path}/extract-face")
|
@app.post("/images/{filename:path}/extract-face")
|
||||||
def extract_face_endpoint(filename: str):
|
def extract_face_endpoint(filename: str):
|
||||||
"""Detect and crop the largest face from image; saves as {group_id}_face.png."""
|
"""Detect and crop the largest face from image; saves as {group_id}_face.png."""
|
||||||
output_dir = _load_output_dir()
|
person = database.get_person(filename)
|
||||||
fpath = os.path.join(output_dir, filename)
|
fpath = person[5] if (person and len(person) > 5 and person[5]) else os.path.join(_load_output_dir(), filename)
|
||||||
if not os.path.exists(fpath):
|
if not fpath or not os.path.exists(fpath):
|
||||||
raise HTTPException(404, "not found")
|
raise HTTPException(404, "not found")
|
||||||
_face_executor.submit(_extract_face_bg, filename, fpath)
|
_face_executor.submit(_extract_face_bg, filename, fpath)
|
||||||
return {"status": "queued", "filename": filename}
|
return {"status": "queued", "filename": filename}
|
||||||
@@ -3506,6 +3563,7 @@ def delete_image(filename: str):
|
|||||||
_move_to_trash(person[5])
|
_move_to_trash(person[5])
|
||||||
|
|
||||||
database.delete_person(filename)
|
database.delete_person(filename)
|
||||||
|
_update_cached_file_meta(filename, exists=False)
|
||||||
_invalidate_static()
|
_invalidate_static()
|
||||||
return {"status": "deleted", "filename": filename}
|
return {"status": "deleted", "filename": filename}
|
||||||
|
|
||||||
@@ -3563,6 +3621,7 @@ def delete_group(group_id: str):
|
|||||||
for filename, filepath in files:
|
for filename, filepath in files:
|
||||||
if filepath and os.path.exists(filepath):
|
if filepath and os.path.exists(filepath):
|
||||||
_move_to_trash(filepath)
|
_move_to_trash(filepath)
|
||||||
|
_update_cached_file_meta(filename, exists=False)
|
||||||
|
|
||||||
database.delete_group(group_id)
|
database.delete_group(group_id)
|
||||||
_invalidate_static()
|
_invalidate_static()
|
||||||
@@ -3748,6 +3807,7 @@ def _scenery_worker(job_id: str, model_filename: str, scene_pil: Image.Image,
|
|||||||
source_refs=json.dumps(refs))
|
source_refs=json.dumps(refs))
|
||||||
except Exception as db_err:
|
except Exception as db_err:
|
||||||
print(f"[scenery] DB error: {db_err}")
|
print(f"[scenery] DB error: {db_err}")
|
||||||
|
jobs[job_id]["latest_output"] = out_name
|
||||||
jobs[job_id]["status"] = "done"
|
jobs[job_id]["status"] = "done"
|
||||||
jobs[job_id]["output"] = out_name
|
jobs[job_id]["output"] = out_name
|
||||||
# Regenerate the static JSON so the frontend's polling surfaces the new
|
# Regenerate the static JSON so the frontend's polling surfaces the new
|
||||||
@@ -4182,11 +4242,27 @@ def remove_background_sam(filename: str):
|
|||||||
f.write(transparent_png)
|
f.write(transparent_png)
|
||||||
|
|
||||||
# Register sidecar in DB so it appears in the same group
|
# Register sidecar in DB so it appears in the same group
|
||||||
group_id = person[1]
|
group_id = person[1] if person and person[1] else naming.get_base_name(os.path.basename(filename))
|
||||||
database.upsert_person(nobg_filename, filepath=nobg_path,
|
tags_list = None
|
||||||
group_id=group_id, has_background=False,
|
if person[2]:
|
||||||
source_refs=json.dumps([filename]))
|
try:
|
||||||
|
tags_list = json.loads(person[2]) if isinstance(person[2], str) else person[2]
|
||||||
|
except Exception:
|
||||||
|
tags_list = None
|
||||||
|
database.upsert_person(
|
||||||
|
nobg_filename, filepath=nobg_path, group_id=group_id,
|
||||||
|
name=person[0], tags=tags_list, embedding=person[3],
|
||||||
|
clip_description=person[4], prompt=person[6], pose=person[7],
|
||||||
|
group_name=person[9], hidden=person[10],
|
||||||
|
has_background=False,
|
||||||
|
has_clothing=person[13],
|
||||||
|
is_source=person[14],
|
||||||
|
pose_description=person[15],
|
||||||
|
pose_skeleton=person[16],
|
||||||
|
source_refs=json.dumps([filename]), # original is the reference
|
||||||
|
)
|
||||||
|
|
||||||
|
_invalidate_static()
|
||||||
used_sam2 = _sam2_predictor is not False and _sam2_predictor is not None
|
used_sam2 = _sam2_predictor is not False and _sam2_predictor is not None
|
||||||
return {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
@@ -4258,10 +4334,23 @@ def manual_crop_image(filename: str, req: CropRequest):
|
|||||||
new_filename = new_basename
|
new_filename = new_basename
|
||||||
path = os.path.join(output_dir, new_basename)
|
path = os.path.join(output_dir, new_basename)
|
||||||
shutil.copy2(src_path, path)
|
shutil.copy2(src_path, path)
|
||||||
|
group_id = person[1] if person and person[1] else naming.get_base_name(os.path.basename(filename))
|
||||||
|
tags_list = None
|
||||||
|
if person[2]:
|
||||||
|
try:
|
||||||
|
tags_list = json.loads(person[2]) if isinstance(person[2], str) else person[2]
|
||||||
|
except Exception:
|
||||||
|
tags_list = None
|
||||||
database.upsert_person(
|
database.upsert_person(
|
||||||
new_filename, filepath=path, group_id=person[1],
|
new_filename, filepath=path, group_id=group_id,
|
||||||
prompt=person[6], pose=person[7],
|
name=person[0], tags=tags_list, embedding=person[3],
|
||||||
has_background=person[11], has_clothing=person[13],
|
clip_description=person[4], prompt=person[6], pose=person[7],
|
||||||
|
group_name=person[9], hidden=person[10],
|
||||||
|
has_background=person[11],
|
||||||
|
has_clothing=person[13],
|
||||||
|
is_source=person[14],
|
||||||
|
pose_description=person[15],
|
||||||
|
pose_skeleton=person[16],
|
||||||
source_refs=json.dumps([filename]), # original is the reference
|
source_refs=json.dumps([filename]), # original is the reference
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -4376,10 +4465,23 @@ def pad_image(filename: str, req: PadRequest):
|
|||||||
new_filename = new_basename
|
new_filename = new_basename
|
||||||
path = os.path.join(output_dir, new_basename)
|
path = os.path.join(output_dir, new_basename)
|
||||||
shutil.copy2(src_path, path)
|
shutil.copy2(src_path, path)
|
||||||
|
group_id = person[1] if person and person[1] else naming.get_base_name(os.path.basename(filename))
|
||||||
|
tags_list = None
|
||||||
|
if person[2]:
|
||||||
|
try:
|
||||||
|
tags_list = json.loads(person[2]) if isinstance(person[2], str) else person[2]
|
||||||
|
except Exception:
|
||||||
|
tags_list = None
|
||||||
database.upsert_person(
|
database.upsert_person(
|
||||||
new_filename, filepath=path, group_id=person[1],
|
new_filename, filepath=path, group_id=group_id,
|
||||||
prompt=person[6], pose=person[7],
|
name=person[0], tags=tags_list, embedding=person[3],
|
||||||
has_background=person[11], has_clothing=person[13],
|
clip_description=person[4], prompt=person[6], pose=person[7],
|
||||||
|
group_name=person[9], hidden=person[10],
|
||||||
|
has_background=person[11],
|
||||||
|
has_clothing=person[13],
|
||||||
|
is_source=person[14],
|
||||||
|
pose_description=person[15],
|
||||||
|
pose_skeleton=person[16],
|
||||||
source_refs=json.dumps([filename]),
|
source_refs=json.dumps([filename]),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -4393,7 +4495,20 @@ def pad_image(filename: str, req: PadRequest):
|
|||||||
|
|
||||||
if req.outpaint:
|
if req.outpaint:
|
||||||
# Trigger Qwen outpainting
|
# Trigger Qwen outpainting
|
||||||
out_instr = "Naturally outpaint and extend the borders of the image to complete the scene."
|
# If the image is transparent, composite it onto a neutral background (black)
|
||||||
|
# so the AI can see the area to be filled.
|
||||||
|
qwen_input = padded
|
||||||
|
fill_desc = ""
|
||||||
|
if qwen_input.mode == "RGBA":
|
||||||
|
# Composite onto black for maximal contrast in outpainting
|
||||||
|
bg = Image.new("RGB", qwen_input.size, (0, 0, 0))
|
||||||
|
bg.paste(qwen_input, mask=qwen_input.split()[3])
|
||||||
|
qwen_input = bg
|
||||||
|
fill_desc = " replacing the black background areas"
|
||||||
|
elif req.fill in ["black", "white"]:
|
||||||
|
fill_desc = f" replacing the {req.fill} background areas"
|
||||||
|
|
||||||
|
out_instr = f"Naturally outpaint and extend the borders of the image to complete the scene{fill_desc}."
|
||||||
actual_prompt = req.prompt or out_instr
|
actual_prompt = req.prompt or out_instr
|
||||||
if req.prompt and out_instr.lower() not in actual_prompt.lower():
|
if req.prompt and out_instr.lower() not in actual_prompt.lower():
|
||||||
actual_prompt = f"{actual_prompt}. {out_instr}"
|
actual_prompt = f"{actual_prompt}. {out_instr}"
|
||||||
@@ -4401,7 +4516,7 @@ def pad_image(filename: str, req: PadRequest):
|
|||||||
actual_prompt = f"{person[6]}. {out_instr}"
|
actual_prompt = f"{person[6]}. {out_instr}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
png_bytes = _run_pipeline(padded, actual_prompt)
|
png_bytes = _run_pipeline(qwen_input, actual_prompt)
|
||||||
padded = Image.open(io.BytesIO(png_bytes))
|
padded = Image.open(io.BytesIO(png_bytes))
|
||||||
# Register the outpaint prompt and set has_background=True in DB
|
# Register the outpaint prompt and set has_background=True in DB
|
||||||
database.upsert_person(new_filename, prompt=actual_prompt, has_background=True)
|
database.upsert_person(new_filename, prompt=actual_prompt, has_background=True)
|
||||||
@@ -4474,16 +4589,29 @@ def duplicate_image(filename: str):
|
|||||||
|
|
||||||
new_path = os.path.join(output_dir, new_basename)
|
new_path = os.path.join(output_dir, new_basename)
|
||||||
_shutil.copy2(path, new_path)
|
_shutil.copy2(path, new_path)
|
||||||
group_id = person[1]
|
group_id = person[1] if person and person[1] else naming.get_base_name(os.path.basename(filename))
|
||||||
# person tuple: (name, group_id, tags, embedding, clip_description, filepath,
|
|
||||||
# prompt, pose, sort_order, group_name, hidden, has_background, source_refs, has_clothing)
|
# Parse tags list if present
|
||||||
|
tags_list = None
|
||||||
|
if person[2]:
|
||||||
|
try:
|
||||||
|
tags_list = json.loads(person[2]) if isinstance(person[2], str) else person[2]
|
||||||
|
except Exception:
|
||||||
|
tags_list = None
|
||||||
|
|
||||||
database.upsert_person(
|
database.upsert_person(
|
||||||
new_filename, filepath=new_path, group_id=group_id,
|
new_filename, filepath=new_path, group_id=group_id,
|
||||||
prompt=person[6], pose=person[7],
|
name=person[0], tags=tags_list, embedding=person[3],
|
||||||
|
clip_description=person[4], prompt=person[6], pose=person[7],
|
||||||
|
group_name=person[9], hidden=person[10],
|
||||||
has_background=person[11],
|
has_background=person[11],
|
||||||
has_clothing=person[13],
|
has_clothing=person[13],
|
||||||
|
is_source=person[14],
|
||||||
|
pose_description=person[15],
|
||||||
|
pose_skeleton=person[16],
|
||||||
source_refs=json.dumps([filename]), # original is the reference
|
source_refs=json.dumps([filename]), # original is the reference
|
||||||
)
|
)
|
||||||
|
_update_cached_file_meta(new_filename, exists=True)
|
||||||
_invalidate_static()
|
_invalidate_static()
|
||||||
return {"status": "success", "new_filename": new_filename, "new_url": f"/output/{new_filename}"}
|
return {"status": "success", "new_filename": new_filename, "new_url": f"/output/{new_filename}"}
|
||||||
|
|
||||||
@@ -4503,6 +4631,7 @@ def restore_background(filename: str):
|
|||||||
bg.save(buf, format="PNG")
|
bg.save(buf, format="PNG")
|
||||||
with open(path, "wb") as f:
|
with open(path, "wb") as f:
|
||||||
f.write(buf.getvalue())
|
f.write(buf.getvalue())
|
||||||
|
_invalidate_static()
|
||||||
return {"status": "success", "filename": filename}
|
return {"status": "success", "filename": filename}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ Anatomically precise, photo realistic, high detail, keep the characteristics of
|
|||||||
|
|
||||||
Masterpiece, you are in a black void, hyper-realistic, high quality. detailed. detailed skin.
|
Masterpiece, you are in a black void, hyper-realistic, high quality. detailed. detailed skin.
|
||||||
|
|
||||||
|
# Outpaint legs
|
||||||
|
|
||||||
|
Naturally outpaint draw the legs and feet, and arms of both female keeping the full image intacts.
|
||||||
|
|
||||||
# Feminine
|
# Feminine
|
||||||
|
|
||||||
1 female nude.
|
1 female nude.
|
||||||
@@ -36,7 +40,6 @@ Keep characteristics of person in Image 2
|
|||||||
|
|
||||||
replace the female in Image 1 by the teenage female in Image 2. Keep the Identical person in image 2. photo realistic
|
replace the female in Image 1 by the teenage female in Image 2. Keep the Identical person in image 2. photo realistic
|
||||||
|
|
||||||
|
|
||||||
# Undress
|
# Undress
|
||||||
|
|
||||||
masterpiece, high quality,
|
masterpiece, high quality,
|
||||||
@@ -4082,11 +4085,21 @@ The body is fully exposed for inspection.
|
|||||||
Eyes looking at the camera, facial characteristics as reference.
|
Eyes looking at the camera, facial characteristics as reference.
|
||||||
Anatomically precise, hyperrealistic, high detail, keep the characteristics of the reference image.
|
Anatomically precise, hyperrealistic, high detail, keep the characteristics of the reference image.
|
||||||
|
|
||||||
|
# Labial Lattice 2hand
|
||||||
|
|
||||||
|
seated, legs spread wide, one leg extended forward, the other bent at the knee with foot resting flat.
|
||||||
|
torso leaning slightly back.
|
||||||
|
arms extended along the thighs, fingers lightly brushing labia.
|
||||||
|
head tilted back, eyes closed, a subtle smile on the lips.
|
||||||
|
legs positioned to create a triangular frame around the genitals.
|
||||||
|
a sense of vulnerability and openness.
|
||||||
|
Anatomically precise, hyperrealistic, high detail, keep the characteristics of the reference image.
|
||||||
|
|
||||||
# Labial Lattice
|
# Labial Lattice
|
||||||
|
|
||||||
seated, legs spread wide, one leg extended forward, the other bent at the knee with foot resting flat.
|
seated, legs spread wide, one leg extended forward, the other bent at the knee with foot resting flat.
|
||||||
torso leaning slightly back, supported on hands placed beside hips.
|
torso leaning slightly back, supported on one hand placed beside hips.
|
||||||
arms extended along the thighs, fingers lightly brushing labia.
|
one arm extended along the thighs, fingers lightly brushing labia.
|
||||||
head tilted back, eyes closed, a subtle smile on the lips.
|
head tilted back, eyes closed, a subtle smile on the lips.
|
||||||
legs positioned to create a triangular frame around the genitals.
|
legs positioned to create a triangular frame around the genitals.
|
||||||
a sense of vulnerability and openness.
|
a sense of vulnerability and openness.
|
||||||
|
|||||||
394
tour-comfy/test_regression_api.py
Normal file
394
tour-comfy/test_regression_api.py
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import unittest
|
||||||
|
from datetime import datetime as _dt
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
# Ensure tour-comfy is in import path so we can import the FastAPI app and database module
|
||||||
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
import database
|
||||||
|
from edit_api import app, _load_output_dir
|
||||||
|
|
||||||
|
class TestAPIRegression(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
# Determine output directory
|
||||||
|
cls.output_dir = _load_output_dir()
|
||||||
|
cls.client = TestClient(app)
|
||||||
|
|
||||||
|
# Unique mock identifiers to avoid conflicts
|
||||||
|
cls.test_ref_filename = "test_regression_ref_image_123.png"
|
||||||
|
cls.test_other_filename = "test_regression_other_image_123.png"
|
||||||
|
cls.group_id = "test_regression_group_123"
|
||||||
|
cls.test_ref_path = os.path.join(cls.output_dir, cls.test_ref_filename)
|
||||||
|
cls.test_other_path = os.path.join(cls.output_dir, cls.test_other_filename)
|
||||||
|
|
||||||
|
# Ensure any leftover artifacts from past failed runs are cleaned
|
||||||
|
cls._cleanup_database()
|
||||||
|
cls._cleanup_files()
|
||||||
|
|
||||||
|
# Create dummy test files (100x100 pixels, RGB format)
|
||||||
|
img = Image.new("RGB", (100, 100), color=(255, 0, 0))
|
||||||
|
img.save(cls.test_ref_path, "PNG")
|
||||||
|
|
||||||
|
img_other = Image.new("RGB", (100, 100), color=(0, 255, 0))
|
||||||
|
img_other.save(cls.test_other_path, "PNG")
|
||||||
|
|
||||||
|
# Mock metadata
|
||||||
|
cls.name = "Regression Test Character"
|
||||||
|
cls.tags = ["VISIBLE", "LIKE", "21+"]
|
||||||
|
cls.embedding = [0.1] * 1024
|
||||||
|
cls.clip_description = "A dummy regression test image description"
|
||||||
|
cls.prompt = "masterpiece, high quality"
|
||||||
|
cls.pose = "standing"
|
||||||
|
cls.group_name = "Regression Group"
|
||||||
|
cls.hidden = False
|
||||||
|
cls.has_background = True
|
||||||
|
cls.has_clothing = False
|
||||||
|
cls.is_source = True
|
||||||
|
cls.pose_description = "The model is standing and looking directly at the camera."
|
||||||
|
cls.pose_skeleton = '{"keypoints": [1, 2, 3]}'
|
||||||
|
|
||||||
|
# Insert reference image into the database
|
||||||
|
database.upsert_person(
|
||||||
|
cls.test_ref_filename,
|
||||||
|
filepath=cls.test_ref_path,
|
||||||
|
name=cls.name,
|
||||||
|
group_id=cls.group_id,
|
||||||
|
tags=cls.tags,
|
||||||
|
embedding=cls.embedding,
|
||||||
|
clip_description=cls.clip_description,
|
||||||
|
prompt=cls.prompt,
|
||||||
|
pose=cls.pose,
|
||||||
|
sort_order=0,
|
||||||
|
group_name=cls.group_name,
|
||||||
|
hidden=cls.hidden,
|
||||||
|
has_background=cls.has_background,
|
||||||
|
has_clothing=cls.has_clothing,
|
||||||
|
is_source=cls.is_source,
|
||||||
|
pose_description=cls.pose_description,
|
||||||
|
pose_skeleton=cls.pose_skeleton
|
||||||
|
)
|
||||||
|
|
||||||
|
# Insert second image in same group for reordering test
|
||||||
|
database.upsert_person(
|
||||||
|
cls.test_other_filename,
|
||||||
|
filepath=cls.test_other_path,
|
||||||
|
name=cls.name,
|
||||||
|
group_id=cls.group_id,
|
||||||
|
tags=cls.tags,
|
||||||
|
embedding=cls.embedding,
|
||||||
|
clip_description=cls.clip_description,
|
||||||
|
prompt=cls.prompt,
|
||||||
|
pose=cls.pose,
|
||||||
|
sort_order=1,
|
||||||
|
group_name=cls.group_name,
|
||||||
|
hidden=cls.hidden,
|
||||||
|
has_background=cls.has_background,
|
||||||
|
has_clothing=cls.has_clothing,
|
||||||
|
is_source=cls.is_source,
|
||||||
|
pose_description=cls.pose_description,
|
||||||
|
pose_skeleton=cls.pose_skeleton
|
||||||
|
)
|
||||||
|
|
||||||
|
# Track dynamically created files and database rows for cleanup
|
||||||
|
cls.created_files = [cls.test_ref_path, cls.test_other_path]
|
||||||
|
cls.created_db_rows = [cls.test_ref_filename, cls.test_other_filename]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
# Cleanup
|
||||||
|
cls._cleanup_database()
|
||||||
|
cls._cleanup_files()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _cleanup_database(cls):
|
||||||
|
# Safely delete any inserted test rows from the person database table
|
||||||
|
conn = database.get_db_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
try:
|
||||||
|
# Delete any filenames starting with test_regression or containing ts_crop/ts_pad
|
||||||
|
cur.execute("""
|
||||||
|
DELETE FROM person
|
||||||
|
WHERE filename LIKE 'test_regression_%%'
|
||||||
|
OR filename LIKE '%%_crop_test_regression_%%'
|
||||||
|
OR filename LIKE '%%_pad_test_regression_%%'
|
||||||
|
OR group_id = %s
|
||||||
|
""", (cls.group_id,))
|
||||||
|
conn.commit()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error cleaning up database: {e}")
|
||||||
|
conn.rollback()
|
||||||
|
finally:
|
||||||
|
cur.close()
|
||||||
|
database._put_db_connection(conn)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _cleanup_files(cls):
|
||||||
|
# Clean up files matching test patterns
|
||||||
|
for f in os.listdir(cls.output_dir):
|
||||||
|
if "test_regression_" in f or "_crop_" in f or "_pad_" in f:
|
||||||
|
p = os.path.join(cls.output_dir, f)
|
||||||
|
try:
|
||||||
|
if os.path.exists(p):
|
||||||
|
os.remove(p)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error removing file {p}: {e}")
|
||||||
|
|
||||||
|
def assertEmbeddingEqual(self, val1, val2):
|
||||||
|
if isinstance(val1, str):
|
||||||
|
val1 = [float(x) for x in val1.strip("[]").split(",")]
|
||||||
|
if isinstance(val2, str):
|
||||||
|
val2 = [float(x) for x in val2.strip("[]").split(",")]
|
||||||
|
self.assertEqual(len(val1), len(val2))
|
||||||
|
for x, y in zip(val1, val2):
|
||||||
|
self.assertAlmostEqual(x, y, places=4)
|
||||||
|
|
||||||
|
def test_01_duplicate_copies_all_pose_and_meta_details(self):
|
||||||
|
# Send duplicate request to FastAPI
|
||||||
|
response = self.client.post(f"/images/{self.test_ref_filename}/duplicate")
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
res_data = response.json()
|
||||||
|
self.assertEqual(res_data["status"], "success")
|
||||||
|
|
||||||
|
new_filename = res_data["new_filename"]
|
||||||
|
self.created_db_rows.append(new_filename)
|
||||||
|
new_path = os.path.join(self.output_dir, os.path.basename(new_filename))
|
||||||
|
self.created_files.append(new_path)
|
||||||
|
|
||||||
|
# Assert duplicate file exists on disk
|
||||||
|
self.assertTrue(os.path.exists(new_path), f"Duplicated file {new_path} not found on disk")
|
||||||
|
|
||||||
|
# Assert all metadata has been accurately duplicated in DB
|
||||||
|
person = database.get_person(new_filename)
|
||||||
|
self.assertIsNotNone(person, "Duplicated database entry not found")
|
||||||
|
|
||||||
|
# Column mappings as in database.py get_person:
|
||||||
|
# SELECT name, group_id, tags, embedding, clip_description, filepath,
|
||||||
|
# prompt, pose, sort_order, group_name, hidden, has_background, source_refs,
|
||||||
|
# has_clothing, is_source, pose_description, pose_skeleton
|
||||||
|
self.assertEqual(person[0], self.name)
|
||||||
|
self.assertEqual(person[1], self.group_id)
|
||||||
|
|
||||||
|
# Tags (assert LIKE and 21+ are preserved)
|
||||||
|
tags_list = json.loads(person[2]) if isinstance(person[2], str) else person[2]
|
||||||
|
self.assertIn("LIKE", tags_list)
|
||||||
|
self.assertIn("21+", tags_list)
|
||||||
|
self.assertIn("VISIBLE", tags_list)
|
||||||
|
|
||||||
|
# Verify 21+ is just a standard tag, not a safety blocker
|
||||||
|
self.assertIn("21+", tags_list)
|
||||||
|
|
||||||
|
self.assertEmbeddingEqual(person[3], self.embedding)
|
||||||
|
self.assertEqual(person[4], self.clip_description)
|
||||||
|
self.assertEqual(person[5], new_path)
|
||||||
|
self.assertEqual(person[6], self.prompt)
|
||||||
|
self.assertEqual(person[7], self.pose)
|
||||||
|
self.assertEqual(person[9], self.group_name)
|
||||||
|
self.assertEqual(person[10], self.hidden)
|
||||||
|
self.assertEqual(person[11], self.has_background)
|
||||||
|
|
||||||
|
# source_refs should refer to original
|
||||||
|
source_refs = json.loads(person[12]) if isinstance(person[12], str) else person[12]
|
||||||
|
self.assertIn(self.test_ref_filename, source_refs)
|
||||||
|
|
||||||
|
self.assertEqual(person[13], self.has_clothing)
|
||||||
|
self.assertEqual(person[14], self.is_source)
|
||||||
|
self.assertEqual(person[15], self.pose_description)
|
||||||
|
self.assertEqual(person[16], self.pose_skeleton)
|
||||||
|
|
||||||
|
def test_02_crop_as_copy_copies_all_pose_and_meta_details(self):
|
||||||
|
# Crop region: (10, 10, 90, 90), as_copy=True
|
||||||
|
req_payload = {
|
||||||
|
"x1": 10,
|
||||||
|
"y1": 10,
|
||||||
|
"x2": 90,
|
||||||
|
"y2": 90,
|
||||||
|
"as_copy": True
|
||||||
|
}
|
||||||
|
response = self.client.post(f"/images/{self.test_ref_filename}/crop", json=req_payload)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
res_data = response.json()
|
||||||
|
self.assertEqual(res_data["status"], "success")
|
||||||
|
|
||||||
|
new_filename = res_data["new_filename"]
|
||||||
|
self.created_db_rows.append(new_filename)
|
||||||
|
new_path = os.path.join(self.output_dir, os.path.basename(new_filename))
|
||||||
|
self.created_files.append(new_path)
|
||||||
|
|
||||||
|
# Verify physical file existence and crop dimensions (should be 80x80)
|
||||||
|
self.assertTrue(os.path.exists(new_path), f"Cropped file {new_path} not found on disk")
|
||||||
|
cropped_img = Image.open(new_path)
|
||||||
|
self.assertEqual(cropped_img.size, (80, 80))
|
||||||
|
|
||||||
|
# Verify database entry has complete metadata
|
||||||
|
person = database.get_person(new_filename)
|
||||||
|
self.assertIsNotNone(person, "Cropped copy database entry not found")
|
||||||
|
|
||||||
|
self.assertEqual(person[0], self.name)
|
||||||
|
self.assertEqual(person[1], self.group_id)
|
||||||
|
|
||||||
|
tags_list = json.loads(person[2]) if isinstance(person[2], str) else person[2]
|
||||||
|
self.assertIn("LIKE", tags_list)
|
||||||
|
self.assertIn("21+", tags_list)
|
||||||
|
self.assertIn("VISIBLE", tags_list)
|
||||||
|
|
||||||
|
self.assertEmbeddingEqual(person[3], self.embedding)
|
||||||
|
self.assertEqual(person[4], self.clip_description)
|
||||||
|
self.assertEqual(person[5], new_path)
|
||||||
|
self.assertEqual(person[6], self.prompt)
|
||||||
|
self.assertEqual(person[7], self.pose)
|
||||||
|
self.assertEqual(person[9], self.group_name)
|
||||||
|
self.assertEqual(person[10], self.hidden)
|
||||||
|
self.assertEqual(person[11], self.has_background)
|
||||||
|
|
||||||
|
source_refs = json.loads(person[12]) if isinstance(person[12], str) else person[12]
|
||||||
|
self.assertIn(self.test_ref_filename, source_refs)
|
||||||
|
|
||||||
|
self.assertEqual(person[13], self.has_clothing)
|
||||||
|
self.assertEqual(person[14], self.is_source)
|
||||||
|
self.assertEqual(person[15], self.pose_description)
|
||||||
|
self.assertEqual(person[16], self.pose_skeleton)
|
||||||
|
|
||||||
|
def test_03_crop_in_place_keeps_all_meta_information(self):
|
||||||
|
# Crop region: (20, 20, 80, 80), as_copy=False (in-place)
|
||||||
|
req_payload = {
|
||||||
|
"x1": 20,
|
||||||
|
"y1": 20,
|
||||||
|
"x2": 80,
|
||||||
|
"y2": 80,
|
||||||
|
"as_copy": False
|
||||||
|
}
|
||||||
|
|
||||||
|
# First verify original size is 100x100
|
||||||
|
orig_img = Image.open(self.test_ref_path)
|
||||||
|
self.assertEqual(orig_img.size, (100, 100))
|
||||||
|
|
||||||
|
response = self.client.post(f"/images/{self.test_ref_filename}/crop", json=req_payload)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
res_data = response.json()
|
||||||
|
self.assertEqual(res_data["status"], "success")
|
||||||
|
self.assertEqual(res_data["new_filename"], self.test_ref_filename)
|
||||||
|
|
||||||
|
# Verify physical file size updated to 60x60
|
||||||
|
cropped_img = Image.open(self.test_ref_path)
|
||||||
|
self.assertEqual(cropped_img.size, (60, 60))
|
||||||
|
|
||||||
|
# Verify database entry has complete metadata untouched
|
||||||
|
person = database.get_person(self.test_ref_filename)
|
||||||
|
self.assertIsNotNone(person, "Database entry not found after in-place crop")
|
||||||
|
|
||||||
|
self.assertEqual(person[0], self.name)
|
||||||
|
self.assertEqual(person[1], self.group_id)
|
||||||
|
|
||||||
|
tags_list = json.loads(person[2]) if isinstance(person[2], str) else person[2]
|
||||||
|
self.assertIn("LIKE", tags_list)
|
||||||
|
self.assertIn("21+", tags_list)
|
||||||
|
|
||||||
|
self.assertEmbeddingEqual(person[3], self.embedding)
|
||||||
|
self.assertEqual(person[4], self.clip_description)
|
||||||
|
self.assertEqual(person[6], self.prompt)
|
||||||
|
self.assertEqual(person[7], self.pose)
|
||||||
|
self.assertEqual(person[9], self.group_name)
|
||||||
|
self.assertEqual(person[10], self.hidden)
|
||||||
|
self.assertEqual(person[11], self.has_background)
|
||||||
|
self.assertEqual(person[13], self.has_clothing)
|
||||||
|
self.assertEqual(person[14], self.is_source)
|
||||||
|
self.assertEqual(person[15], self.pose_description)
|
||||||
|
self.assertEqual(person[16], self.pose_skeleton)
|
||||||
|
|
||||||
|
def test_04_pad_as_copy_copies_all_pose_and_meta_details(self):
|
||||||
|
# Pad canvas: expand top and bottom by 10 pixels, as_copy=True
|
||||||
|
req_payload = {
|
||||||
|
"top": 10,
|
||||||
|
"right": 0,
|
||||||
|
"bottom": 10,
|
||||||
|
"left": 0,
|
||||||
|
"as_copy": True,
|
||||||
|
"fill": "transparent",
|
||||||
|
"outpaint": False
|
||||||
|
}
|
||||||
|
# Original size at this point is 60x60 (due to test_03 crop)
|
||||||
|
response = self.client.post(f"/images/{self.test_ref_filename}/pad", json=req_payload)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
res_data = response.json()
|
||||||
|
self.assertEqual(res_data["status"], "success")
|
||||||
|
|
||||||
|
new_filename = res_data["new_filename"]
|
||||||
|
self.created_db_rows.append(new_filename)
|
||||||
|
new_path = os.path.join(self.output_dir, os.path.basename(new_filename))
|
||||||
|
self.created_files.append(new_path)
|
||||||
|
|
||||||
|
# Verify physical file existence and pad dimensions (should be 60x80)
|
||||||
|
self.assertTrue(os.path.exists(new_path), f"Padded file {new_path} not found on disk")
|
||||||
|
padded_img = Image.open(new_path)
|
||||||
|
self.assertEqual(padded_img.size, (60, 80))
|
||||||
|
|
||||||
|
# Verify database entry has complete metadata
|
||||||
|
person = database.get_person(new_filename)
|
||||||
|
self.assertIsNotNone(person, "Padded copy database entry not found")
|
||||||
|
|
||||||
|
self.assertEqual(person[0], self.name)
|
||||||
|
self.assertEqual(person[1], self.group_id)
|
||||||
|
|
||||||
|
tags_list = json.loads(person[2]) if isinstance(person[2], str) else person[2]
|
||||||
|
self.assertIn("LIKE", tags_list)
|
||||||
|
self.assertIn("21+", tags_list)
|
||||||
|
self.assertIn("VISIBLE", tags_list)
|
||||||
|
|
||||||
|
self.assertEmbeddingEqual(person[3], self.embedding)
|
||||||
|
self.assertEqual(person[4], self.clip_description)
|
||||||
|
self.assertEqual(person[5], new_path)
|
||||||
|
self.assertEqual(person[6], self.prompt)
|
||||||
|
self.assertEqual(person[7], self.pose)
|
||||||
|
self.assertEqual(person[9], self.group_name)
|
||||||
|
self.assertEqual(person[10], self.hidden)
|
||||||
|
self.assertEqual(person[11], self.has_background)
|
||||||
|
|
||||||
|
source_refs = json.loads(person[12]) if isinstance(person[12], str) else person[12]
|
||||||
|
self.assertIn(self.test_ref_filename, source_refs)
|
||||||
|
|
||||||
|
self.assertEqual(person[13], self.has_clothing)
|
||||||
|
self.assertEqual(person[14], self.is_source)
|
||||||
|
self.assertEqual(person[15], self.pose_description)
|
||||||
|
self.assertEqual(person[16], self.pose_skeleton)
|
||||||
|
|
||||||
|
def test_05_reorder_group_updates_sort_orders(self):
|
||||||
|
# Verify original order
|
||||||
|
order_rows_before = database.get_group_order(self.group_id)
|
||||||
|
filenames_before = [r[0] for r in order_rows_before]
|
||||||
|
self.assertIn(self.test_ref_filename, filenames_before)
|
||||||
|
self.assertIn(self.test_other_filename, filenames_before)
|
||||||
|
|
||||||
|
# Reverse the order of files and submit
|
||||||
|
new_order = [self.test_other_filename, self.test_ref_filename]
|
||||||
|
req_payload = {
|
||||||
|
"filenames": new_order
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.client.post(f"/groups/{self.group_id}/order", json=req_payload)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
res_data = response.json()
|
||||||
|
self.assertEqual(res_data["group_id"], self.group_id)
|
||||||
|
self.assertEqual(res_data["filenames"], new_order)
|
||||||
|
|
||||||
|
# Get order from database and verify state reflects the updates
|
||||||
|
order_rows_after = database.get_group_order(self.group_id)
|
||||||
|
filenames_after = [r[0] for r in order_rows_after]
|
||||||
|
|
||||||
|
# Verify custom sort orders are explicitly 0, 1
|
||||||
|
self.assertEqual(filenames_after[:2], new_order)
|
||||||
|
|
||||||
|
person_other = database.get_person(self.test_other_filename)
|
||||||
|
person_ref = database.get_person(self.test_ref_filename)
|
||||||
|
|
||||||
|
# Column 8 in database.get_person is sort_order
|
||||||
|
self.assertEqual(person_other[8], 0)
|
||||||
|
self.assertEqual(person_ref[8], 1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user