aa
This commit is contained in:
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "devstral24b-junie:latest",
|
|
||||||
"baseUrl": "http://localhost:11434/v1/chat/completions",
|
|
||||||
"apiType": "OpenAICompletion",
|
|
||||||
"temperature": 0.05,
|
|
||||||
"primaryModel": {
|
|
||||||
"id": "devstral24b-junie:latest",
|
|
||||||
"temperature": 0.05
|
|
||||||
},
|
|
||||||
"fasterModel": {
|
|
||||||
"id": "devstral24b-junie:latest",
|
|
||||||
"temperature": 0.05
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"baseUrl": "http://localhost:11434/v1/responses",
|
|
||||||
"id": "qwen2.5-coder:32b",
|
|
||||||
"apiType": "OpenAIResponses",
|
|
||||||
"temperature": 0.1,
|
|
||||||
"primaryModel": {
|
|
||||||
"id": "qwen2.5-coder:32b",
|
|
||||||
"temperature": 0.1
|
|
||||||
},
|
|
||||||
"fasterModel": {
|
|
||||||
"id": "qwen2.5-coder:1.5b"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"baseUrl": "http://localhost:11440/v1/responses",
|
|
||||||
"id": "qwen3-coder:30b",
|
|
||||||
"apiType": "OpenAIResponses",
|
|
||||||
"temperature": 0.3,
|
|
||||||
"primaryModel": {
|
|
||||||
"id": "qwen3-coder:30b",
|
|
||||||
"temperature": 0.3
|
|
||||||
},
|
|
||||||
"fasterModel": {
|
|
||||||
"id": "qwen2.5-coder:1.5b"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
151
.trash/architecture.svg
Normal file
151
.trash/architecture.svg
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800">
|
||||||
|
<style>
|
||||||
|
.component {
|
||||||
|
fill: #e3f2fd;
|
||||||
|
stroke: #1976d2;
|
||||||
|
stroke-width: 2;
|
||||||
|
rx: 10;
|
||||||
|
ry: 10;
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-anchor: middle;
|
||||||
|
}
|
||||||
|
.sublabel {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
text-anchor: middle;
|
||||||
|
}
|
||||||
|
.connection {
|
||||||
|
stroke: #757575;
|
||||||
|
stroke-width: 2;
|
||||||
|
marker-end: url(#arrowhead);
|
||||||
|
}
|
||||||
|
.database {
|
||||||
|
fill: #ffebee;
|
||||||
|
stroke: #d32f2f;
|
||||||
|
stroke-width: 2;
|
||||||
|
rx: 10;
|
||||||
|
ry: 10;
|
||||||
|
}
|
||||||
|
.service {
|
||||||
|
fill: #e8f5e9;
|
||||||
|
stroke: #388e3c;
|
||||||
|
stroke-width: 2;
|
||||||
|
rx: 10;
|
||||||
|
ry: 10;
|
||||||
|
}
|
||||||
|
.system {
|
||||||
|
fill: #fff3e0;
|
||||||
|
stroke: #ef6c00;
|
||||||
|
stroke-width: 2;
|
||||||
|
rx: 10;
|
||||||
|
ry: 10;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- Main system container -->
|
||||||
|
<rect x="50" y="50" width="1100" height="700" class="system" fill="#fff3e0"/>
|
||||||
|
<text x="600" y="40" class="label" text-anchor="middle">Qwen Image Edit Rapid-AIO v23 System</text>
|
||||||
|
|
||||||
|
<!-- Database Component -->
|
||||||
|
<rect x="850" y="150" width="200" height="100" class="database"/>
|
||||||
|
<text x="950" y="170" class="label">PostgreSQL Database</text>
|
||||||
|
<text x="950" y="190" class="sublabel">host: 192.168.1.160</text>
|
||||||
|
<text x="950" y="210" class="sublabel">port: 5433</text>
|
||||||
|
<text x="950" y="230" class="sublabel">dbname: dv</text>
|
||||||
|
|
||||||
|
<!-- ComfyUI Backend Component -->
|
||||||
|
<rect x="850" y="300" width="200" height="100" class="component"/>
|
||||||
|
<text x="950" y="320" class="label">ComfyUI Backend</text>
|
||||||
|
<text x="950" y="340" class="sublabel">Qwen-Rapid-NSFW-v23</text>
|
||||||
|
<text x="950" y="360" class="sublabel">GGUF Model (21.8GB)</text>
|
||||||
|
|
||||||
|
<!-- API Service Component -->
|
||||||
|
<rect x="850" y="450" width="200" height="100" class="service"/>
|
||||||
|
<text x="950" y="470" class="label">FastAPI Service</text>
|
||||||
|
<text x="950" y="490" class="sublabel">edit_api.py</text>
|
||||||
|
<text x="950" y="510" class="sublabel">Image Processing</text>
|
||||||
|
|
||||||
|
<!-- Watcher Service Component -->
|
||||||
|
<rect x="850" y="600" width="200" height="100" class="service"/>
|
||||||
|
<text x="950" y="620" class="label">Folder Watcher</text>
|
||||||
|
<text x="950" y="640" class="sublabel">watcher.py</text>
|
||||||
|
<text x="950" y="660" class="sublabel">car.html Generator</text>
|
||||||
|
|
||||||
|
<!-- Input/Output Directories -->
|
||||||
|
<rect x="50" y="150" width="200" height="100" class="component"/>
|
||||||
|
<text x="150" y="170" class="label">Stage Directory</text>
|
||||||
|
<text x="150" y="190" class="sublabel">/mnt/zim/tour-comfy/stage</text>
|
||||||
|
<text x="150" y="210" class="sublabel">Input images</text>
|
||||||
|
|
||||||
|
<rect x="50" y="300" width="200" height="100" class="component"/>
|
||||||
|
<text x="150" y="320" class="label">Output Directory</text>
|
||||||
|
<text x="150" y="340" class="sublabel">/mnt/zim/tour-comfy/output</text>
|
||||||
|
<text x="150" y="360" class="sublabel">Processed images</text>
|
||||||
|
|
||||||
|
<rect x="50" y="450" width="200" height="100" class="component"/>
|
||||||
|
<text x="150" y="470" class="label">Failed Directory</text>
|
||||||
|
<text x="150" y="490" class="sublabel">/mnt/zim/tour-comfy/failed</text>
|
||||||
|
<text x="150" y="510" class="sublabel">Failed images</text>
|
||||||
|
|
||||||
|
<!-- System Services -->
|
||||||
|
<rect x="50" y="600" width="200" height="100" class="component"/>
|
||||||
|
<text x="150" y="620" class="label">System Services</text>
|
||||||
|
<text x="150" y="640" class="sublabel">comfyui.target</text>
|
||||||
|
<text x="150" y="660" class="sublabel">backend, api, watcher</text>
|
||||||
|
|
||||||
|
<!-- Connections -->
|
||||||
|
<line x1="350" y1="200" x2="850" y2="200" class="connection"/>
|
||||||
|
<text x="600" y="190" class="sublabel">Image Upload</text>
|
||||||
|
|
||||||
|
<line x1="350" y1="350" x2="850" y2="350" class="connection"/>
|
||||||
|
<text x="600" y="340" class="sublabel">Processing Request</text>
|
||||||
|
|
||||||
|
<line x1="350" y1="500" x2="850" y2="500" class="connection"/>
|
||||||
|
<text x="600" y="490" class="sublabel">Database Access</text>
|
||||||
|
|
||||||
|
<line x1="350" y1="650" x2="850" y2="650" class="connection"/>
|
||||||
|
<text x="600" y="640" class="sublabel">File Monitoring</text>
|
||||||
|
|
||||||
|
<!-- Arrowhead marker definition -->
|
||||||
|
<defs>
|
||||||
|
<marker id="arrowhead" markerWidth="10" markerHeight="7"
|
||||||
|
refX="0" refY="3.5" orient="auto">
|
||||||
|
<polygon points="0 0, 10 3.5, 0 7" fill="#757575"/>
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Configuration Files -->
|
||||||
|
<rect x="400" y="50" width="200" height="80" class="component"/>
|
||||||
|
<text x="500" y="70" class="label">Configuration</text>
|
||||||
|
<text x="500" y="90" class="sublabel">config.json</text>
|
||||||
|
<text x="500" y="110" class="sublabel">API Settings</text>
|
||||||
|
|
||||||
|
<!-- Prompt Management -->
|
||||||
|
<rect x="400" y="200" width="200" height="80" class="component"/>
|
||||||
|
<text x="500" y="220" class="label">Prompt Management</text>
|
||||||
|
<text x="500" y="240" class="sublabel">prompt_pose_book.md</text>
|
||||||
|
<text x="500" y="260" class="sublabel">Base prompts</text>
|
||||||
|
|
||||||
|
<!-- GFPGAN Component -->
|
||||||
|
<rect x="400" y="350" width="200" height="80" class="component"/>
|
||||||
|
<text x="500" y="370" class="label">GFPGAN Face Restoration</text>
|
||||||
|
<text x="500" y="390" class="sublabel">gfpgan directory</text>
|
||||||
|
<text x="500" y="410" class="sublabel">Face swap functionality</text>
|
||||||
|
|
||||||
|
<!-- System Target -->
|
||||||
|
<rect x="400" y="600" width="200" height="80" class="component"/>
|
||||||
|
<text x="500" y="620" class="label">System Target</text>
|
||||||
|
<text x="500" y="640" class="sublabel">comfyui.target</text>
|
||||||
|
<text x="500" y="660" class="sublabel">Service orchestration</text>
|
||||||
|
|
||||||
|
<!-- Connection between system components -->
|
||||||
|
<line x1="600" y1="130" x2="600" y2="150" class="connection"/>
|
||||||
|
<line x1="600" y1="280" x2="600" y2="300" class="connection"/>
|
||||||
|
<line x1="600" y1="430" x2="600" y2="450" class="connection"/>
|
||||||
|
<line x1="600" y1="580" x2="600" y2="600" class="connection"/>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.6 KiB |
11
.trash/table-1781832877269.csv
Normal file
11
.trash/table-1781832877269.csv
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#,Pose,Base,Key Action,Mood
|
||||||
|
9,**The Bridge**,Supine,"Hips lifted high, body arched","Strong, open"
|
||||||
|
10,**The Lotus (recline)**,Supine,"Legs crossed in lotus, knees open","Centered, tranquil"
|
||||||
|
11,**The Archer**,Side-lying,"Top leg bent, hand grasps ankle, bow shape","Tense, focused"
|
||||||
|
12,**The Crescent**,Kneeling/standing,"Deep backbend, arms overhead, hips forward","Expansive, vulnerable"
|
||||||
|
13,**The Cocoon**,Side-lying,"Fetal curl, arms wrapped around legs","Protected, intimate"
|
||||||
|
14,**The Swan Dive**,Prone,"Chest and legs lifted, arms forward","Graceful, soaring"
|
||||||
|
15,**The Suspended Split**,Supine,Legs vertical and spread wide in full split,"Flexible, exposed"
|
||||||
|
16,**The Embrace**,Seated,"Folded forward over legs, arms wrapped around","Intimate, yielding"
|
||||||
|
17,**The Cascade**,Seated,Twisted fold over one extended leg,"Fluid, surrendered"
|
||||||
|
18,**The Flame**,Standing,"One-legged balance, arms overhead","Poised, burning"
|
||||||
|
57
AGENTS.md
Normal file
57
AGENTS.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# Agents in Qwen-Image-Edit Rapid-AIO v23
|
||||||
|
|
||||||
|
This document describes the agents and components that make up the Qwen image editing system running on A6000 hardware.
|
||||||
|
|
||||||
|
## System Overview
|
||||||
|
|
||||||
|
The Qwen-Image-Edit Rapid-AIO v23 system is a headless image editing API that runs Phr00t's Rapid-AIO NSFW v23 model as a Q8 GGUF. The system consists of two main components:
|
||||||
|
1. ComfyUI backend that executes the Qwen models
|
||||||
|
2. FastAPI service that provides HTTP API endpoints
|
||||||
|
|
||||||
|
## Key Agents and Components
|
||||||
|
|
||||||
|
### 1. ComfyUI Backend Agent
|
||||||
|
- **Role**: Core image editing backend that executes the Qwen models
|
||||||
|
- **Service Name**: `comfyui-backend`
|
||||||
|
- **Port**: 8188
|
||||||
|
- **Components**:
|
||||||
|
- Qwen model (Qwen-Rapid-NSFW-v23_Q8_0.gguf)
|
||||||
|
- Text encoder (qwen_2.5_vl_7b_fp8_scaled.safetensors)
|
||||||
|
- VAE decoder (qwen_image_vae.safetensors)
|
||||||
|
- Workflow execution engine
|
||||||
|
|
||||||
|
### 2. FastAPI Edit Agent
|
||||||
|
- **Role**: Provides HTTP API endpoint for image editing requests
|
||||||
|
- **Service Name**: `comfyui-api`
|
||||||
|
- **Port**: 8500
|
||||||
|
- **Functionality**:
|
||||||
|
- Accepts image and prompt input via POST request
|
||||||
|
- Processes edits using ComfyUI backend
|
||||||
|
- Returns edited PNG output
|
||||||
|
|
||||||
|
## Communication Flow
|
||||||
|
|
||||||
|
1. Client sends POST request to FastAPI Edit Agent (port 8500) with image and prompt
|
||||||
|
2. FastAPI Edit Agent forwards request to ComfyUI Backend Agent (port 8188)
|
||||||
|
3. ComfyUI Backend Agent processes the image using Qwen models via workflow
|
||||||
|
4. Result is returned through FastAPI Edit Agent to client
|
||||||
|
|
||||||
|
## Service Management
|
||||||
|
|
||||||
|
### Backend Service
|
||||||
|
- **Service Name**: `comfyui-backend`
|
||||||
|
- **Port**: 8188
|
||||||
|
- **Command**: `/home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/a6000-comfy/run_comfyui.sh`
|
||||||
|
|
||||||
|
### API Service
|
||||||
|
- **Service Name**: `comfyui-api`
|
||||||
|
- **Port**: 8500
|
||||||
|
- **Command**: `/home/mike/dev/qwen-image-edit-rapid-aio-nsfw-v23/a6000-comfy/start_api.sh`
|
||||||
|
|
||||||
|
## Configuration Notes
|
||||||
|
|
||||||
|
The system uses the following environment variables:
|
||||||
|
- COMFY_URL: Set to "http://127.0.0.1:8188"
|
||||||
|
- HOST: Set to "0.0.0.0"
|
||||||
|
- PORT: Set to "8500"
|
||||||
|
- MAX_AREA: Default 1048576 (1MP output budget)
|
||||||
93
architecture2.svg
Normal file
93
architecture2.svg
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg width="800" height="600" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style>
|
||||||
|
.component {
|
||||||
|
fill: #e6f3ff;
|
||||||
|
stroke: #003366;
|
||||||
|
stroke-width: 2;
|
||||||
|
rx: 10;
|
||||||
|
ry: 10;
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-anchor: middle;
|
||||||
|
}
|
||||||
|
.sublabel {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
text-anchor: middle;
|
||||||
|
}
|
||||||
|
.connection {
|
||||||
|
stroke: #003366;
|
||||||
|
stroke-width: 2;
|
||||||
|
marker-end: url(#arrowhead);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- Arrowhead definition -->
|
||||||
|
<defs>
|
||||||
|
<marker id="arrowhead" markerWidth="10" markerHeight="7"
|
||||||
|
refX="0" refY="3.5" orient="auto">
|
||||||
|
<polygon points="0 0, 10 3.5, 0 7" fill="#003366"/>
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- FastAPI Edit Agent -->
|
||||||
|
<rect x="50" y="200" width="200" height="150" class="component"/>
|
||||||
|
<text x="150" y="220" class="label">FastAPI Edit Agent</text>
|
||||||
|
<text x="150" y="240" class="sublabel">Service: comfyui-api</text>
|
||||||
|
<text x="150" y="260" class="sublabel">Port: 8500</text>
|
||||||
|
<text x="150" y="280" class="sublabel">HTTP API Endpoint</text>
|
||||||
|
|
||||||
|
<!-- ComfyUI Backend Agent -->
|
||||||
|
<rect x="550" y="200" width="200" height="150" class="component"/>
|
||||||
|
<text x="650" y="220" class="label">ComfyUI Backend</text>
|
||||||
|
<text x="650" y="240" class="sublabel">Service: comfyui-backend</text>
|
||||||
|
<text x="650" y="260" class="sublabel">Port: 8188</text>
|
||||||
|
<text x="650" y="280" class="sublabel">Qwen Model Execution</text>
|
||||||
|
|
||||||
|
<!-- Client -->
|
||||||
|
<rect x="300" y="50" width="200" height="80" class="component"/>
|
||||||
|
<text x="400" y="70" class="label">Client</text>
|
||||||
|
<text x="400" y="90" class="sublabel">Image + Prompt Request</text>
|
||||||
|
|
||||||
|
<!-- Connection: Client to API -->
|
||||||
|
<line x1="400" y1="130" x2="150" y2="200" class="connection"/>
|
||||||
|
|
||||||
|
<!-- Connection: API to ComfyUI -->
|
||||||
|
<line x1="250" y1="275" x2="550" y2="275" class="connection"/>
|
||||||
|
|
||||||
|
<!-- Model Components inside ComfyUI -->
|
||||||
|
<rect x="570" y="370" width="160" height="100" class="component"/>
|
||||||
|
<text x="650" y="390" class="label">Qwen Model</text>
|
||||||
|
<text x="650" y="410" class="sublabel">Q8 GGUF (Qwen-Rapid-NSFW-v23)</text>
|
||||||
|
|
||||||
|
<!-- Text Encoder -->
|
||||||
|
<rect x="570" y="480" width="160" height="60" class="component"/>
|
||||||
|
<text x="650" y="500" class="label">Text Encoder</text>
|
||||||
|
<text x="650" y="520" class="sublabel">qwen_2.5_vl_7b_fp8_scaled.safetensors</text>
|
||||||
|
|
||||||
|
<!-- VAE Decoder -->
|
||||||
|
<rect x="570" y="550" width="160" height="60" class="component"/>
|
||||||
|
<text x="650" y="570" class="label">VAE Decoder</text>
|
||||||
|
<text x="650" y="590" class="sublabel">qwen_image_vae.safetensors</text>
|
||||||
|
|
||||||
|
<!-- Workflow -->
|
||||||
|
<rect x="350" y="370" width="120" height="80" class="component"/>
|
||||||
|
<text x="410" y="390" class="label">Workflow</text>
|
||||||
|
<text x="410" y="410" class="sublabel">workflow_qwen_edit.json</text>
|
||||||
|
|
||||||
|
<!-- Connection: API to Workflow -->
|
||||||
|
<line x1="250" y1="300" x2="410" y2="450" class="connection"/>
|
||||||
|
|
||||||
|
<!-- Connection: Workflow to Model -->
|
||||||
|
<line x1="470" y1="410" x2="570" y2="420" class="connection"/>
|
||||||
|
|
||||||
|
<!-- Connection: Model to VAE -->
|
||||||
|
<line x1="730" y1="420" x2="730" y2="560" class="connection"/>
|
||||||
|
|
||||||
|
<!-- Connection: VAE to Output -->
|
||||||
|
<line x1="730" y1="610" x2="150" y2="350" class="connection"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.3 KiB |
235
architecturev2.svg
Normal file
235
architecturev2.svg
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1400" height="900">
|
||||||
|
<style>
|
||||||
|
.component {
|
||||||
|
fill: #e3f2fd;
|
||||||
|
stroke: #1976d2;
|
||||||
|
stroke-width: 2;
|
||||||
|
rx: 15;
|
||||||
|
ry: 15;
|
||||||
|
filter: url(#shadow);
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-anchor: middle;
|
||||||
|
fill: #1a237e;
|
||||||
|
}
|
||||||
|
.sublabel {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
text-anchor: middle;
|
||||||
|
fill: #333;
|
||||||
|
}
|
||||||
|
.connection {
|
||||||
|
stroke: #757575;
|
||||||
|
stroke-width: 2;
|
||||||
|
marker-end: url(#arrowhead);
|
||||||
|
}
|
||||||
|
.database {
|
||||||
|
fill: #ffebee;
|
||||||
|
stroke: #d32f2f;
|
||||||
|
stroke-width: 2;
|
||||||
|
rx: 15;
|
||||||
|
ry: 15;
|
||||||
|
filter: url(#shadow);
|
||||||
|
}
|
||||||
|
.service {
|
||||||
|
fill: #e8f5e9;
|
||||||
|
stroke: #388e3c;
|
||||||
|
stroke-width: 2;
|
||||||
|
rx: 15;
|
||||||
|
ry: 15;
|
||||||
|
filter: url(#shadow);
|
||||||
|
}
|
||||||
|
.system {
|
||||||
|
fill: #fff3e0;
|
||||||
|
stroke: #ef6c00;
|
||||||
|
stroke-width: 2;
|
||||||
|
rx: 15;
|
||||||
|
ry: 15;
|
||||||
|
filter: url(#shadow);
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-anchor: middle;
|
||||||
|
fill: #1a237e;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
text-anchor: middle;
|
||||||
|
fill: #555;
|
||||||
|
}
|
||||||
|
.group-title {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-anchor: start;
|
||||||
|
fill: #1a237e;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- Drop shadow filter -->
|
||||||
|
<defs>
|
||||||
|
<filter id="shadow" x="-50%" y="-50%" width="200%" height="200%">
|
||||||
|
<feDropShadow dx="3" dy="3" stdDeviation="4" flood-color="#000000" flood-opacity="0.3"/>
|
||||||
|
</filter>
|
||||||
|
|
||||||
|
<!-- Arrowhead marker definition -->
|
||||||
|
<marker id="arrowhead" markerWidth="10" markerHeight="7"
|
||||||
|
refX="0" refY="3.5" orient="auto">
|
||||||
|
<polygon points="0 0, 10 3.5, 0 7" fill="#757575"/>
|
||||||
|
</marker>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Main system container -->
|
||||||
|
<rect x="50" y="50" width="1300" height="800" class="system"/>
|
||||||
|
<text x="700" y="40" class="title">Qwen Image Edit Rapid-AIO v23 System Architecture</text>
|
||||||
|
<text x="700" y="65" class="subtitle">Complete System Overview with All Components and Interactions</text>
|
||||||
|
|
||||||
|
<!-- Group Title: Input/Processing Pipeline -->
|
||||||
|
<text x="100" y="100" class="group-title">Input/Processing Pipeline</text>
|
||||||
|
|
||||||
|
<!-- Stage Directory -->
|
||||||
|
<rect x="100" y="120" width="200" height="100" class="component"/>
|
||||||
|
<text x="200" y="140" class="label">Stage Directory</text>
|
||||||
|
<text x="200" y="160" class="sublabel">/mnt/zim/tour-comfy/stage</text>
|
||||||
|
<text x="200" y="180" class="sublabel">Input images</text>
|
||||||
|
|
||||||
|
<!-- Output Directory -->
|
||||||
|
<rect x="100" y="250" width="200" height="100" class="component"/>
|
||||||
|
<text x="200" y="270" class="label">Output Directory</text>
|
||||||
|
<text x="200" y="290" class="sublabel">/mnt/zim/tour-comfy/output</text>
|
||||||
|
<text x="200" y="310" class="sublabel">Processed images</text>
|
||||||
|
|
||||||
|
<!-- Failed Directory -->
|
||||||
|
<rect x="100" y="380" width="200" height="100" class="component"/>
|
||||||
|
<text x="200" y="400" class="label">Failed Directory</text>
|
||||||
|
<text x="200" y="420" class="sublabel">/mnt/zim/tour-comfy/failed</text>
|
||||||
|
<text x="200" y="440" class="sublabel">Failed images</text>
|
||||||
|
|
||||||
|
<!-- Group Title: System Services -->
|
||||||
|
<text x="100" y="520" class="group-title">System Services</text>
|
||||||
|
|
||||||
|
<!-- System Target -->
|
||||||
|
<rect x="100" y="540" width="200" height="100" class="component"/>
|
||||||
|
<text x="200" y="560" class="label">System Target</text>
|
||||||
|
<text x="200" y="580" class="sublabel">comfyui.target</text>
|
||||||
|
<text x="200" y="600" class="sublabel">Service orchestration</text>
|
||||||
|
|
||||||
|
<!-- Group Title: Backend Components -->
|
||||||
|
<text x="500" y="100" class="group-title">Backend Components</text>
|
||||||
|
|
||||||
|
<!-- ComfyUI Backend Component -->
|
||||||
|
<rect x="500" y="120" width="250" height="120" class="component"/>
|
||||||
|
<text x="625" y="140" class="label">ComfyUI Backend</text>
|
||||||
|
<text x="625" y="160" class="sublabel">Qwen-Rapid-NSFW-v23</text>
|
||||||
|
<text x="625" y="180" class="sublabel">GGUF Model (21.8GB)</text>
|
||||||
|
<text x="625" y="200" class="sublabel">ComfyUI Workflow Engine</text>
|
||||||
|
|
||||||
|
<!-- GFPGAN Component -->
|
||||||
|
<rect x="500" y="270" width="250" height="100" class="component"/>
|
||||||
|
<text x="625" y="290" class="label">GFPGAN Face Restoration</text>
|
||||||
|
<text x="625" y="310" class="sublabel">gfpgan directory</text>
|
||||||
|
<text x="625" y="330" class="sublabel">Face swap functionality</text>
|
||||||
|
|
||||||
|
<!-- Prompt Management -->
|
||||||
|
<rect x="500" y="400" width="250" height="100" class="component"/>
|
||||||
|
<text x="625" y="420" class="label">Prompt Management</text>
|
||||||
|
<text x="625" y="440" class="sublabel">prompt_pose_book.md</text>
|
||||||
|
<text x="625" y="460" class="sublabel">Base prompts & pose book</text>
|
||||||
|
|
||||||
|
<!-- Configuration Files -->
|
||||||
|
<rect x="500" y="530" width="250" height="100" class="component"/>
|
||||||
|
<text x="625" y="550" class="label">Configuration</text>
|
||||||
|
<text x="625" y="570" class="sublabel">config.json</text>
|
||||||
|
<text x="625" y="590" class="sublabel">API Settings & Paths</text>
|
||||||
|
|
||||||
|
<!-- Group Title: API and Database -->
|
||||||
|
<text x="1000" y="100" class="group-title">API and Database</text>
|
||||||
|
|
||||||
|
<!-- FastAPI Service Component -->
|
||||||
|
<rect x="1000" y="120" width="250" height="100" class="service"/>
|
||||||
|
<text x="1125" y="140" class="label">FastAPI Service</text>
|
||||||
|
<text x="1125" y="160" class="sublabel">edit_api.py</text>
|
||||||
|
<text x="1125" y="180" class="sublabel">Image Processing API</text>
|
||||||
|
|
||||||
|
<!-- PostgreSQL Database -->
|
||||||
|
<rect x="1000" y="250" width="250" height="100" class="database"/>
|
||||||
|
<text x="1125" y="270" class="label">PostgreSQL Database</text>
|
||||||
|
<text x="1125" y="290" class="sublabel">host: 192.168.1.160</text>
|
||||||
|
<text x="1125" y="310" class="sublabel">port: 5433, dbname: dv</text>
|
||||||
|
|
||||||
|
<!-- Group Title: Watcher Service -->
|
||||||
|
<text x="1000" y="400" class="group-title">Watcher Service</text>
|
||||||
|
|
||||||
|
<!-- Folder Watcher Component -->
|
||||||
|
<rect x="1000" y="420" width="250" height="100" class="service"/>
|
||||||
|
<text x="1125" y="440" class="label">Folder Watcher</text>
|
||||||
|
<text x="1125" y="460" class="sublabel">watcher.py</text>
|
||||||
|
<text x="1125" y="480" class="sublabel">car.html Generator</text>
|
||||||
|
|
||||||
|
<!-- Group Title: System Integration -->
|
||||||
|
<text x="1000" y="550" class="group-title">System Integration</text>
|
||||||
|
|
||||||
|
<!-- System Services -->
|
||||||
|
<rect x="1000" y="570" width="250" height="100" class="component"/>
|
||||||
|
<text x="1125" y="590" class="label">System Services</text>
|
||||||
|
<text x="1125" y="610" class="sublabel">comfyui-backend.service</text>
|
||||||
|
<text x="1125" y="630" class="sublabel">comfyui-api.service</text>
|
||||||
|
<text x="1125" y="650" class="sublabel">comfyui-watcher.service</text>
|
||||||
|
|
||||||
|
<!-- Connections between components -->
|
||||||
|
|
||||||
|
<!-- Stage to ComfyUI Backend -->
|
||||||
|
<line x1="300" y1="170" x2="500" y2="170" class="connection"/>
|
||||||
|
<text x="400" y="160" class="sublabel">Image Upload</text>
|
||||||
|
|
||||||
|
<!-- ComfyUI Backend to Output Directory -->
|
||||||
|
<line x1="750" y1="180" x2="950" y2="180" class="connection"/>
|
||||||
|
<text x="850" y="170" class="sublabel">Processing Result</text>
|
||||||
|
|
||||||
|
<!-- ComfyUI Backend to Database -->
|
||||||
|
<line x1="750" y1="300" x2="1000" y2="300" class="connection"/>
|
||||||
|
<text x="875" y="290" class="sublabel">Database Access</text>
|
||||||
|
|
||||||
|
<!-- FastAPI to Database -->
|
||||||
|
<line x1="1250" y1="170" x2="1250" y2="300" class="connection"/>
|
||||||
|
<text x="1260" y="235" class="sublabel">Database Access</text>
|
||||||
|
|
||||||
|
<!-- Folder Watcher to Output Directory -->
|
||||||
|
<line x1="1250" y1="470" x2="950" y2="470" class="connection"/>
|
||||||
|
<text x="1100" y="460" class="sublabel">car.html Generation</text>
|
||||||
|
|
||||||
|
<!-- Folder Watcher to Database -->
|
||||||
|
<line x1="1250" y1="520" x2="1250" y2="350" class="connection"/>
|
||||||
|
<text x="1260" y="435" class="sublabel">Database Updates</text>
|
||||||
|
|
||||||
|
<!-- System Target to Services -->
|
||||||
|
<line x1="300" y1="590" x2="1000" y2="590" class="connection"/>
|
||||||
|
<text x="650" y="580" class="sublabel">Service Orchestration</text>
|
||||||
|
|
||||||
|
<!-- ComfyUI Backend to Prompt Management -->
|
||||||
|
<line x1="750" y1="240" x2="750" y2="400" class="connection"/>
|
||||||
|
<text x="760" y="320" class="sublabel">Prompt Usage</text>
|
||||||
|
|
||||||
|
<!-- ComfyUI Backend to GFPGAN -->
|
||||||
|
<line x1="750" y1="390" x2="750" y2="270" class="connection"/>
|
||||||
|
<text x="760" y="330" class="sublabel">Face Restoration</text>
|
||||||
|
|
||||||
|
<!-- System Target to ComfyUI Backend -->
|
||||||
|
<line x1="300" y1="590" x2="500" y2="180" class="connection"/>
|
||||||
|
<text x="400" y="500" class="sublabel">Start Backend</text>
|
||||||
|
|
||||||
|
<!-- System Target to FastAPI -->
|
||||||
|
<line x1="300" y1="590" x2="1000" y2="170" class="connection"/>
|
||||||
|
<text x="650" y="500" class="sublabel">Start API</text>
|
||||||
|
|
||||||
|
<!-- System Target to Watcher -->
|
||||||
|
<line x1="300" y1="590" x2="1000" y2="470" class="connection"/>
|
||||||
|
<text x="650" y="500" class="sublabel">Start Watcher</text>
|
||||||
|
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 8.6 KiB |
1
backlog.md
Normal file
1
backlog.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
privacy feature not work when overlay
|
||||||
BIN
gfpgan/weights/parsing_parsenet.pth
Normal file
BIN
gfpgan/weights/parsing_parsenet.pth
Normal file
Binary file not shown.
1924
tour-comfy/car.html
1924
tour-comfy/car.html
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"api_url": "http://127.0.0.1:8500/edit",
|
"api_url": "http://127.0.0.1:8500/edit",
|
||||||
"prompt": "high quality. detailed, female nude, high quality. realistic. detailed. female nude",
|
"prompt": "high quality. realistic. detailed, female nude. realistic, high quality. realistic. detailed. female nude. realistic",
|
||||||
"base_prompts": [
|
"base_prompts": [
|
||||||
"Head-on a full-body three-quarter full-nude-body female portrait, realistic, transparent background",
|
"Head-on a full-body three-quarter full-nude-body female portrait, realistic, transparent background",
|
||||||
"Head-on straight-on full-nude-body female portrait, realistic, transparent background",
|
"Head-on straight-on full-nude-body female portrait, realistic, transparent background",
|
||||||
@@ -19,5 +19,9 @@
|
|||||||
"output_dir": "/mnt/zim/tour-comfy/output",
|
"output_dir": "/mnt/zim/tour-comfy/output",
|
||||||
"failed_dir": "/mnt/zim/tour-comfy/failed",
|
"failed_dir": "/mnt/zim/tour-comfy/failed",
|
||||||
"processed_file": "./tour-comfy/processed.json",
|
"processed_file": "./tour-comfy/processed.json",
|
||||||
"log_file": "./tour-comfy/watcher.log"
|
"log_file": "./tour-comfy/watcher.log",
|
||||||
|
"wireframe_dir": "/mnt/zim/tour-comfy/wireframe",
|
||||||
|
"faceswap_model": "~/.insightface/models/inswapper_128.onnx",
|
||||||
|
"facefusion_dir": "~/facefusion",
|
||||||
|
"facefusion_venv": "~/facefusion-venv"
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,8 @@ def migrate_schema():
|
|||||||
"ALTER TABLE person ADD COLUMN IF NOT EXISTS has_background BOOLEAN DEFAULT TRUE",
|
"ALTER TABLE person ADD COLUMN IF NOT EXISTS has_background BOOLEAN DEFAULT TRUE",
|
||||||
"ALTER TABLE person ADD COLUMN IF NOT EXISTS source_refs TEXT",
|
"ALTER TABLE person ADD COLUMN IF NOT EXISTS source_refs TEXT",
|
||||||
"ALTER TABLE person ADD COLUMN IF NOT EXISTS has_clothing BOOLEAN DEFAULT NULL",
|
"ALTER TABLE person ADD COLUMN IF NOT EXISTS has_clothing BOOLEAN DEFAULT NULL",
|
||||||
|
"ALTER TABLE person ADD COLUMN IF NOT EXISTS content_type TEXT DEFAULT 'image'",
|
||||||
|
"ALTER TABLE person ADD COLUMN IF NOT EXISTS faceswap_source_video TEXT",
|
||||||
]:
|
]:
|
||||||
cur.execute(sql)
|
cur.execute(sql)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@@ -36,34 +38,39 @@ def migrate_schema():
|
|||||||
def upsert_person(filename, filepath=None, name=None, group_id=None, tags=None,
|
def upsert_person(filename, filepath=None, name=None, group_id=None, tags=None,
|
||||||
embedding=None, clip_description=None, prompt=None, pose=None,
|
embedding=None, clip_description=None, prompt=None, pose=None,
|
||||||
sort_order=None, group_name=None, hidden=None,
|
sort_order=None, group_name=None, hidden=None,
|
||||||
has_background=None, source_refs=None, has_clothing=None):
|
has_background=None, source_refs=None, has_clothing=None,
|
||||||
|
content_type=None, faceswap_source_video=None):
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
try:
|
try:
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
INSERT INTO person (filename, filepath, name, group_id, tags, embedding,
|
INSERT INTO person (filename, filepath, name, group_id, tags, embedding,
|
||||||
clip_description, prompt, pose, sort_order, group_name, hidden,
|
clip_description, prompt, pose, sort_order, group_name, hidden,
|
||||||
has_background, source_refs, has_clothing)
|
has_background, source_refs, has_clothing,
|
||||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
content_type, faceswap_source_video)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
ON CONFLICT (filename) DO UPDATE
|
ON CONFLICT (filename) DO UPDATE
|
||||||
SET filepath = COALESCE(EXCLUDED.filepath, person.filepath),
|
SET filepath = COALESCE(EXCLUDED.filepath, person.filepath),
|
||||||
name = COALESCE(EXCLUDED.name, person.name),
|
name = COALESCE(EXCLUDED.name, person.name),
|
||||||
group_id = COALESCE(EXCLUDED.group_id, person.group_id),
|
group_id = COALESCE(EXCLUDED.group_id, person.group_id),
|
||||||
tags = COALESCE(EXCLUDED.tags, person.tags),
|
tags = COALESCE(EXCLUDED.tags, person.tags),
|
||||||
embedding = COALESCE(EXCLUDED.embedding, person.embedding),
|
embedding = COALESCE(EXCLUDED.embedding, person.embedding),
|
||||||
clip_description= COALESCE(EXCLUDED.clip_description,person.clip_description),
|
clip_description = COALESCE(EXCLUDED.clip_description, person.clip_description),
|
||||||
prompt = COALESCE(EXCLUDED.prompt, person.prompt),
|
prompt = COALESCE(EXCLUDED.prompt, person.prompt),
|
||||||
pose = COALESCE(EXCLUDED.pose, person.pose),
|
pose = COALESCE(EXCLUDED.pose, person.pose),
|
||||||
sort_order = COALESCE(EXCLUDED.sort_order, person.sort_order),
|
sort_order = COALESCE(EXCLUDED.sort_order, person.sort_order),
|
||||||
group_name = COALESCE(EXCLUDED.group_name, person.group_name),
|
group_name = COALESCE(EXCLUDED.group_name, person.group_name),
|
||||||
hidden = COALESCE(EXCLUDED.hidden, person.hidden),
|
hidden = COALESCE(EXCLUDED.hidden, person.hidden),
|
||||||
has_background = COALESCE(EXCLUDED.has_background, person.has_background),
|
has_background = COALESCE(EXCLUDED.has_background, person.has_background),
|
||||||
source_refs = COALESCE(EXCLUDED.source_refs, person.source_refs),
|
source_refs = COALESCE(EXCLUDED.source_refs, person.source_refs),
|
||||||
has_clothing = COALESCE(EXCLUDED.has_clothing, person.has_clothing);
|
has_clothing = COALESCE(EXCLUDED.has_clothing, person.has_clothing),
|
||||||
|
content_type = COALESCE(EXCLUDED.content_type, person.content_type),
|
||||||
|
faceswap_source_video = COALESCE(EXCLUDED.faceswap_source_video, person.faceswap_source_video);
|
||||||
""", (filename, filepath, name, group_id,
|
""", (filename, filepath, name, group_id,
|
||||||
json.dumps(tags) if tags else None,
|
json.dumps(tags) if tags else None,
|
||||||
embedding, clip_description, prompt, pose, sort_order, group_name, hidden,
|
embedding, clip_description, prompt, pose, sort_order, group_name, hidden,
|
||||||
has_background, source_refs, has_clothing))
|
has_background, source_refs, has_clothing,
|
||||||
|
content_type, faceswap_source_video))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
finally:
|
finally:
|
||||||
cur.close()
|
cur.close()
|
||||||
@@ -101,7 +108,7 @@ def list_persons():
|
|||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT filename, name, group_id, clip_description,
|
SELECT filename, name, group_id, clip_description,
|
||||||
prompt, pose, sort_order, group_name, hidden, has_background, source_refs,
|
prompt, pose, sort_order, group_name, hidden, has_background, source_refs,
|
||||||
has_clothing
|
has_clothing, content_type, faceswap_source_video
|
||||||
FROM person
|
FROM person
|
||||||
""")
|
""")
|
||||||
return cur.fetchall()
|
return cur.fetchall()
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ from PIL import Image
|
|||||||
from fastapi import FastAPI, UploadFile, File, Form, HTTPException, BackgroundTasks
|
from fastapi import FastAPI, UploadFile, File, Form, HTTPException, BackgroundTasks
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import Response
|
from fastapi.responses import Response
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
import shutil
|
import shutil
|
||||||
import re
|
import re
|
||||||
@@ -59,6 +60,8 @@ NODE_SAVE = "10"
|
|||||||
|
|
||||||
MAX_SEED = 2**32 - 1
|
MAX_SEED = 2**32 - 1
|
||||||
|
|
||||||
|
VIDEO_EXTENSIONS = ('.mp4', '.mov', '.avi', '.webm', '.mkv')
|
||||||
|
|
||||||
# Poses where the source image should be rotated 180° before pipeline for better results
|
# Poses where the source image should be rotated 180° before pipeline for better results
|
||||||
ROTATE_180_POSES = {"the dragon", "dragon", "the draak", "draak"}
|
ROTATE_180_POSES = {"the dragon", "dragon", "the draak", "draak"}
|
||||||
|
|
||||||
@@ -115,6 +118,18 @@ def _watch_car_html():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _load_wireframe_dir() -> str:
|
||||||
|
with open(CONFIG_PATH, "r") as f:
|
||||||
|
conf = json.load(f)
|
||||||
|
return conf.get("wireframe_dir", "/mnt/zim/tour-comfy/wireframe")
|
||||||
|
|
||||||
|
|
||||||
|
def _load_faceswap_model_path() -> str:
|
||||||
|
with open(CONFIG_PATH, "r") as f:
|
||||||
|
conf = json.load(f)
|
||||||
|
return os.path.expanduser(conf.get("faceswap_model", "~/.insightface/models/inswapper_128.onnx"))
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
def on_startup():
|
def on_startup():
|
||||||
try:
|
try:
|
||||||
@@ -123,6 +138,14 @@ def on_startup():
|
|||||||
print(f"DB migration warning: {e}")
|
print(f"DB migration warning: {e}")
|
||||||
_sync_car_html()
|
_sync_car_html()
|
||||||
threading.Thread(target=_watch_car_html, daemon=True).start()
|
threading.Thread(target=_watch_car_html, daemon=True).start()
|
||||||
|
# Mount wireframe static dir for browser video preview
|
||||||
|
try:
|
||||||
|
wf_dir = _load_wireframe_dir()
|
||||||
|
if os.path.isdir(wf_dir):
|
||||||
|
app.mount("/wireframe", StaticFiles(directory=wf_dir), name="wireframe")
|
||||||
|
print(f"[wireframe] mounted {wf_dir} → /wireframe")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[wireframe] mount warning: {e}")
|
||||||
|
|
||||||
|
|
||||||
# --- helpers ----------------------------------------------------------------
|
# --- helpers ----------------------------------------------------------------
|
||||||
@@ -311,6 +334,379 @@ def _apply_transparency(png_bytes: bytes) -> bytes:
|
|||||||
return png_bytes
|
return png_bytes
|
||||||
|
|
||||||
|
|
||||||
|
# --- faceswapper (insightface + inswapper_128) --------------------------------
|
||||||
|
# Setup: pip install insightface onnxruntime-gpu opencv-python-headless
|
||||||
|
# Download: place inswapper_128.onnx at ~/.insightface/models/inswapper_128.onnx
|
||||||
|
# Source: https://huggingface.co/deepinsight/inswapper
|
||||||
|
|
||||||
|
_faceswapper = None
|
||||||
|
_faceswapper_lock = threading.Lock()
|
||||||
|
|
||||||
|
_gfpgan = None
|
||||||
|
_gfpgan_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def _load_faceswapper():
|
||||||
|
global _faceswapper
|
||||||
|
if _faceswapper is not None:
|
||||||
|
return _faceswapper
|
||||||
|
with _faceswapper_lock:
|
||||||
|
if _faceswapper is not None:
|
||||||
|
return _faceswapper
|
||||||
|
try:
|
||||||
|
import insightface
|
||||||
|
from insightface.app import FaceAnalysis
|
||||||
|
except ImportError:
|
||||||
|
raise RuntimeError("insightface not installed. Run: pip install insightface onnxruntime-gpu")
|
||||||
|
|
||||||
|
providers = ['CUDAExecutionProvider', 'CPUExecutionProvider']
|
||||||
|
app = FaceAnalysis(name='buffalo_l', providers=providers)
|
||||||
|
app.prepare(ctx_id=0, det_size=(640, 640))
|
||||||
|
|
||||||
|
model_path = _load_faceswap_model_path()
|
||||||
|
if not os.path.exists(model_path):
|
||||||
|
# Try HuggingFace download as fallback
|
||||||
|
try:
|
||||||
|
import huggingface_hub
|
||||||
|
model_path = huggingface_hub.hf_hub_download(
|
||||||
|
'deepinsight/inswapper', 'inswapper_128.onnx',
|
||||||
|
local_dir=os.path.dirname(model_path),
|
||||||
|
)
|
||||||
|
print(f"[faceswap] Downloaded inswapper_128.onnx to {model_path}")
|
||||||
|
except Exception as de:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"inswapper_128.onnx not found at {model_path}. "
|
||||||
|
f"Download from https://huggingface.co/deepinsight/inswapper and place it there. "
|
||||||
|
f"Download error: {de}"
|
||||||
|
)
|
||||||
|
|
||||||
|
swapper = insightface.model_zoo.get_model(model_path, providers=providers)
|
||||||
|
_faceswapper = (app, swapper)
|
||||||
|
print(f"[faceswap] loaded insightface buffalo_l + inswapper_128 from {model_path}")
|
||||||
|
return _faceswapper
|
||||||
|
|
||||||
|
|
||||||
|
def _load_gfpgan():
|
||||||
|
"""Lazy-load GFPGAN face restorer. Returns restorer or False if unavailable."""
|
||||||
|
global _gfpgan
|
||||||
|
if _gfpgan is not None:
|
||||||
|
return _gfpgan
|
||||||
|
with _gfpgan_lock:
|
||||||
|
if _gfpgan is not None:
|
||||||
|
return _gfpgan
|
||||||
|
try:
|
||||||
|
from gfpgan import GFPGANer
|
||||||
|
# Main GFPGAN model
|
||||||
|
model_path = os.path.expanduser('~/.gfpgan/weights/GFPGANv1.4.pth')
|
||||||
|
os.makedirs(os.path.dirname(model_path), exist_ok=True)
|
||||||
|
if not os.path.exists(model_path):
|
||||||
|
import urllib.request
|
||||||
|
print('[gfpgan] Downloading GFPGANv1.4.pth (~333 MB)...')
|
||||||
|
tmp = model_path + '.tmp'
|
||||||
|
urllib.request.urlretrieve(
|
||||||
|
'https://github.com/TencentARC/GFPGAN/releases/download/v1.3.4/GFPGANv1.4.pth',
|
||||||
|
tmp
|
||||||
|
)
|
||||||
|
os.rename(tmp, model_path)
|
||||||
|
# GFPGANer hardcodes facexlib download path to CWD/gfpgan/weights/
|
||||||
|
# → change CWD to ~ so models land at ~/gfpgan/weights/ (stable across runs)
|
||||||
|
home = os.path.expanduser('~')
|
||||||
|
os.makedirs(os.path.join(home, 'gfpgan', 'weights'), exist_ok=True)
|
||||||
|
_orig_cwd = os.getcwd()
|
||||||
|
os.chdir(home)
|
||||||
|
try:
|
||||||
|
restorer = GFPGANer(model_path=model_path, upscale=1, arch='clean',
|
||||||
|
channel_multiplier=2, bg_upsampler=None)
|
||||||
|
finally:
|
||||||
|
os.chdir(_orig_cwd)
|
||||||
|
_gfpgan = restorer
|
||||||
|
print('[gfpgan] GFPGANv1.4 loaded')
|
||||||
|
except Exception as e:
|
||||||
|
print(f'[gfpgan] not available: {e}')
|
||||||
|
_gfpgan = False
|
||||||
|
return _gfpgan
|
||||||
|
|
||||||
|
|
||||||
|
def _make_video_poster(video_path: str) -> str | None:
|
||||||
|
"""Extract a poster JPG (sibling `<stem>.jpg`) so the gallery can show a
|
||||||
|
thumbnail for a video via a plain <img> (file:// can't render <video> as a
|
||||||
|
thumb). Returns the poster path on success, else None."""
|
||||||
|
import subprocess
|
||||||
|
poster_path = os.path.splitext(video_path)[0] + '.jpg'
|
||||||
|
try:
|
||||||
|
r = subprocess.run([
|
||||||
|
'ffmpeg', '-y', '-ss', '1', '-i', video_path,
|
||||||
|
'-frames:v', '1', '-q:v', '3', poster_path,
|
||||||
|
], capture_output=True, timeout=120)
|
||||||
|
if r.returncode == 0 and os.path.exists(poster_path):
|
||||||
|
return poster_path
|
||||||
|
# -ss 1 can overshoot very short clips; retry from the first frame
|
||||||
|
r = subprocess.run([
|
||||||
|
'ffmpeg', '-y', '-i', video_path,
|
||||||
|
'-frames:v', '1', '-q:v', '3', poster_path,
|
||||||
|
], capture_output=True, timeout=120)
|
||||||
|
if r.returncode == 0 and os.path.exists(poster_path):
|
||||||
|
return poster_path
|
||||||
|
except Exception as pe:
|
||||||
|
print(f'[poster] failed for {video_path}: {pe}')
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _faceswap_worker(job_id: str, model_filename: str, video_name: str, enhance: bool = True):
|
||||||
|
"""Frame-by-frame face swap: model face → every face in template video."""
|
||||||
|
output_dir = _load_output_dir()
|
||||||
|
wireframe_dir = _load_wireframe_dir()
|
||||||
|
|
||||||
|
try:
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
app, swapper = _load_faceswapper()
|
||||||
|
except Exception as e:
|
||||||
|
jobs[job_id]["status"] = "error"
|
||||||
|
jobs[job_id]["error"] = str(e)
|
||||||
|
return
|
||||||
|
|
||||||
|
gfpgan_restorer = None
|
||||||
|
if enhance:
|
||||||
|
try:
|
||||||
|
r = _load_gfpgan()
|
||||||
|
if r is not False:
|
||||||
|
gfpgan_restorer = r
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. Load source (model) face
|
||||||
|
src_path = os.path.join(output_dir, model_filename)
|
||||||
|
src_bgr = cv2.imread(src_path)
|
||||||
|
if src_bgr is None:
|
||||||
|
raise ValueError(f"Cannot read model image: {model_filename}")
|
||||||
|
src_faces = app.get(src_bgr)
|
||||||
|
if not src_faces:
|
||||||
|
raise ValueError(f"No face detected in: {model_filename}")
|
||||||
|
# Use the largest face as source
|
||||||
|
src_face = max(src_faces, key=lambda f: (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1]))
|
||||||
|
|
||||||
|
# 2. Open template video
|
||||||
|
video_path = os.path.join(wireframe_dir, video_name)
|
||||||
|
cap = cv2.VideoCapture(video_path)
|
||||||
|
if not cap.isOpened():
|
||||||
|
raise ValueError(f"Cannot open video: {video_name}")
|
||||||
|
|
||||||
|
fps = cap.get(cv2.CAP_PROP_FPS) or 25.0
|
||||||
|
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||||
|
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||||
|
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||||
|
jobs[job_id]["total"] = max(total, 1)
|
||||||
|
|
||||||
|
# 3. Write frame-swapped temp video
|
||||||
|
ts = time.strftime("%Y%m%d_%H%M%S")
|
||||||
|
vid_stem = os.path.splitext(video_name)[0]
|
||||||
|
base_name = naming.get_base_name(model_filename)
|
||||||
|
tmp_name = f"{ts}_fs_tmp_{vid_stem}_{base_name}.mp4"
|
||||||
|
out_name = f"{ts}_fs_{vid_stem}_{base_name}.mp4"
|
||||||
|
tmp_path = os.path.join(output_dir, tmp_name)
|
||||||
|
out_path = os.path.join(output_dir, out_name)
|
||||||
|
|
||||||
|
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
||||||
|
writer = cv2.VideoWriter(tmp_path, fourcc, fps, (width, height))
|
||||||
|
|
||||||
|
frame_idx = 0
|
||||||
|
while True:
|
||||||
|
ret, frame = cap.read()
|
||||||
|
if not ret:
|
||||||
|
break
|
||||||
|
tgt_faces = app.get(frame)
|
||||||
|
result = frame
|
||||||
|
if tgt_faces:
|
||||||
|
result = frame.copy()
|
||||||
|
for face in tgt_faces:
|
||||||
|
try:
|
||||||
|
result = swapper.get(result, face, src_face, paste_back=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if gfpgan_restorer is not None:
|
||||||
|
try:
|
||||||
|
_, _, result = gfpgan_restorer.enhance(
|
||||||
|
result, has_aligned=False, only_center_face=False, paste_back=True
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
writer.write(result)
|
||||||
|
frame_idx += 1
|
||||||
|
jobs[job_id]["done"] = frame_idx
|
||||||
|
|
||||||
|
cap.release()
|
||||||
|
writer.release()
|
||||||
|
|
||||||
|
# 4. Remux with original audio via ffmpeg
|
||||||
|
try:
|
||||||
|
import subprocess
|
||||||
|
r = subprocess.run([
|
||||||
|
'ffmpeg', '-y',
|
||||||
|
'-i', tmp_path,
|
||||||
|
'-i', video_path,
|
||||||
|
'-map', '0:v:0', '-map', '1:a?',
|
||||||
|
'-c:v', 'libx264', '-preset', 'fast', '-crf', '18',
|
||||||
|
'-c:a', 'aac', '-movflags', '+faststart',
|
||||||
|
out_path,
|
||||||
|
], capture_output=True, timeout=600)
|
||||||
|
if r.returncode == 0:
|
||||||
|
os.remove(tmp_path)
|
||||||
|
else:
|
||||||
|
os.rename(tmp_path, out_path)
|
||||||
|
print(f"[faceswap] ffmpeg failed ({r.returncode}), using raw mp4v output")
|
||||||
|
except Exception as fe:
|
||||||
|
os.rename(tmp_path, out_path)
|
||||||
|
print(f"[faceswap] ffmpeg error: {fe}")
|
||||||
|
|
||||||
|
# 5. Snapshot poster + register output in DB under same group as model
|
||||||
|
_make_video_poster(out_path)
|
||||||
|
person = database.get_person(model_filename)
|
||||||
|
group_id = (person[1] if person and person[1] else naming.get_base_name(model_filename))
|
||||||
|
database.upsert_person(
|
||||||
|
out_name,
|
||||||
|
filepath=out_path,
|
||||||
|
group_id=group_id,
|
||||||
|
content_type='video',
|
||||||
|
faceswap_source_video=video_name,
|
||||||
|
source_refs=json.dumps([model_filename]),
|
||||||
|
)
|
||||||
|
|
||||||
|
jobs[job_id]["status"] = "done"
|
||||||
|
jobs[job_id]["output"] = out_name
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[faceswap] error: {e}")
|
||||||
|
jobs[job_id]["status"] = "error"
|
||||||
|
jobs[job_id]["error"] = str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def _faceswap_worker_ff(job_id: str, model_filename: str, video_name: str,
|
||||||
|
hair: bool = True, enhance: bool = True):
|
||||||
|
"""High-quality faceswap via FaceFusion CLI (supports hair_swapper + ghost model)."""
|
||||||
|
import subprocess as sp
|
||||||
|
import sys
|
||||||
|
import re as _re
|
||||||
|
|
||||||
|
output_dir = _load_output_dir()
|
||||||
|
wireframe_dir = _load_wireframe_dir()
|
||||||
|
|
||||||
|
with open(CONFIG_PATH, 'r') as f:
|
||||||
|
conf = json.load(f)
|
||||||
|
ff_dir = os.path.expanduser(conf.get('facefusion_dir', '~/facefusion'))
|
||||||
|
ff_venv = os.path.expanduser(conf.get('facefusion_venv', '~/facefusion-venv'))
|
||||||
|
|
||||||
|
ff_script = os.path.join(ff_dir, 'facefusion.py')
|
||||||
|
ff_py = os.path.join(ff_venv, 'bin', 'python')
|
||||||
|
if not os.path.exists(ff_py):
|
||||||
|
ff_py = sys.executable
|
||||||
|
|
||||||
|
if not os.path.exists(ff_script):
|
||||||
|
jobs[job_id]['status'] = 'error'
|
||||||
|
jobs[job_id]['error'] = (
|
||||||
|
f'FaceFusion not found at {ff_dir}. '
|
||||||
|
'Run: bash tour-comfy/install_facefusion.sh'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
src_path = os.path.join(output_dir, model_filename)
|
||||||
|
video_path = os.path.join(wireframe_dir, video_name)
|
||||||
|
ts = time.strftime('%Y%m%d_%H%M%S')
|
||||||
|
vid_stem = os.path.splitext(video_name)[0]
|
||||||
|
base_name = naming.get_base_name(model_filename)
|
||||||
|
out_name = f'{ts}_fs_{vid_stem}_{base_name}.mp4'
|
||||||
|
out_path = os.path.join(output_dir, out_name)
|
||||||
|
|
||||||
|
processors = ['face_swapper']
|
||||||
|
# hair_swapper is not available in this FaceFusion version; use face_enhancer for quality
|
||||||
|
if enhance:
|
||||||
|
processors.append('face_enhancer')
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
ff_py, ff_script, 'headless-run',
|
||||||
|
'--source-paths', src_path,
|
||||||
|
'--target-path', video_path,
|
||||||
|
'--output-path', out_path,
|
||||||
|
'--processors', *processors,
|
||||||
|
'--execution-providers', 'cuda',
|
||||||
|
'--face-swapper-model', 'ghost_3_256',
|
||||||
|
# The default yolo_face detector at score 0.5 misses the extreme-angle /
|
||||||
|
# cropped close-up faces common in these POV template clips, so the swap
|
||||||
|
# silently no-ops. scrfd at a lower score + multi-angle detection reliably
|
||||||
|
# finds them; 'many' selector swaps every detected face per frame.
|
||||||
|
'--face-detector-model', 'scrfd',
|
||||||
|
'--face-detector-score', '0.3',
|
||||||
|
'--face-detector-angles', '0', '90', '270',
|
||||||
|
'--face-selector-mode', 'many',
|
||||||
|
]
|
||||||
|
if enhance:
|
||||||
|
cmd += ['--face-enhancer-model', 'gfpgan_1.4']
|
||||||
|
|
||||||
|
jobs[job_id]['total'] = 100
|
||||||
|
jobs[job_id]['done'] = 0
|
||||||
|
|
||||||
|
# Ensure CUDA libs are on LD_LIBRARY_PATH for the subprocess (inherited from parent,
|
||||||
|
# but also add nvidia package libs as fallback if running outside start_api.sh)
|
||||||
|
import site as _site
|
||||||
|
_sp_pkgs = next((p for p in _site.getsitepackages() if 'site-packages' in p), '')
|
||||||
|
_nv_base = os.path.join(_sp_pkgs, 'nvidia')
|
||||||
|
_extra_libs = ':'.join(
|
||||||
|
os.path.join(_nv_base, pkg, 'lib')
|
||||||
|
for pkg in ('cuda_runtime', 'cublas', 'cudnn', 'curand', 'cufft', 'cusolver', 'cusparse', 'nvjitlink', 'cuda_nvrtc')
|
||||||
|
if os.path.isdir(os.path.join(_nv_base, pkg, 'lib'))
|
||||||
|
)
|
||||||
|
_env = os.environ.copy()
|
||||||
|
if _extra_libs:
|
||||||
|
_env['LD_LIBRARY_PATH'] = _extra_libs + (':' + _env['LD_LIBRARY_PATH'] if _env.get('LD_LIBRARY_PATH') else '')
|
||||||
|
|
||||||
|
try:
|
||||||
|
output_lines = []
|
||||||
|
proc = sp.Popen(
|
||||||
|
cmd, cwd=ff_dir, env=_env,
|
||||||
|
stdout=sp.PIPE, stderr=sp.PIPE,
|
||||||
|
text=True, errors='replace',
|
||||||
|
)
|
||||||
|
# Read stdout for progress, stderr for error info
|
||||||
|
import threading as _thr
|
||||||
|
def _drain_stderr():
|
||||||
|
for ln in proc.stderr:
|
||||||
|
output_lines.append(ln.rstrip())
|
||||||
|
print(f'[facefusion] {ln.rstrip()}')
|
||||||
|
_thr.Thread(target=_drain_stderr, daemon=True).start()
|
||||||
|
for line in proc.stdout:
|
||||||
|
print(f'[facefusion] {line.rstrip()}')
|
||||||
|
m = _re.search(r'(\d+)\s*/\s*(\d+)', line)
|
||||||
|
if m:
|
||||||
|
done, total = int(m.group(1)), int(m.group(2))
|
||||||
|
if total > 0:
|
||||||
|
jobs[job_id]['done'] = done
|
||||||
|
jobs[job_id]['total'] = total
|
||||||
|
proc.wait()
|
||||||
|
|
||||||
|
if proc.returncode != 0:
|
||||||
|
tail = '\n'.join(output_lines[-10:])
|
||||||
|
raise RuntimeError(f'FaceFusion exited with code {proc.returncode}: {tail}')
|
||||||
|
if not os.path.exists(out_path):
|
||||||
|
raise RuntimeError('FaceFusion produced no output file')
|
||||||
|
|
||||||
|
_make_video_poster(out_path)
|
||||||
|
person = database.get_person(model_filename)
|
||||||
|
group_id = (person[1] if person and person[1] else naming.get_base_name(model_filename))
|
||||||
|
database.upsert_person(
|
||||||
|
out_name, filepath=out_path, group_id=group_id,
|
||||||
|
content_type='video', faceswap_source_video=video_name,
|
||||||
|
source_refs=json.dumps([model_filename]),
|
||||||
|
)
|
||||||
|
jobs[job_id]['status'] = 'done'
|
||||||
|
jobs[job_id]['output'] = out_name
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f'[faceswap-ff] error: {e}')
|
||||||
|
jobs[job_id]['status'] = 'error'
|
||||||
|
jobs[job_id]['error'] = str(e)
|
||||||
|
|
||||||
|
|
||||||
# --- pipeline helper ---------------------------------------------------------
|
# --- pipeline helper ---------------------------------------------------------
|
||||||
|
|
||||||
def _load_poses():
|
def _load_poses():
|
||||||
@@ -691,30 +1087,33 @@ def get_batch(job_id: str):
|
|||||||
@app.get("/images")
|
@app.get("/images")
|
||||||
def list_images():
|
def list_images():
|
||||||
output_dir = _load_output_dir()
|
output_dir = _load_output_dir()
|
||||||
extensions = ('.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg')
|
all_extensions = ('.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg') + VIDEO_EXTENSIONS
|
||||||
try:
|
try:
|
||||||
# Try to get from DB first
|
|
||||||
try:
|
try:
|
||||||
persons = database.list_persons()
|
persons = database.list_persons()
|
||||||
# persons: (filename, name, group_id, clip_description, prompt, pose, sort_order, group_name, hidden, has_background, source_refs, has_clothing)
|
# list_persons cols: filename, name, group_id, clip_description,
|
||||||
|
# prompt, pose, sort_order, group_name, hidden, has_background,
|
||||||
|
# source_refs, has_clothing, content_type, faceswap_source_video
|
||||||
db_images = []
|
db_images = []
|
||||||
for p in persons:
|
for p in persons:
|
||||||
fpath = os.path.join(output_dir, p[0])
|
fpath = os.path.join(output_dir, p[0])
|
||||||
if not os.path.exists(fpath):
|
if not os.path.exists(fpath):
|
||||||
continue # skip orphan DB records whose file no longer exists
|
continue
|
||||||
db_images.append({
|
db_images.append({
|
||||||
"filename": p[0],
|
"filename": p[0],
|
||||||
"name": p[1],
|
"name": p[1],
|
||||||
"group_id": p[2],
|
"group_id": p[2],
|
||||||
"clip_description":p[3],
|
"clip_description": p[3],
|
||||||
"prompt": p[4],
|
"prompt": p[4],
|
||||||
"pose": p[5],
|
"pose": p[5],
|
||||||
"sort_order": p[6],
|
"sort_order": p[6],
|
||||||
"group_name": p[7],
|
"group_name": p[7],
|
||||||
"hidden": bool(p[8]) if p[8] else False,
|
"hidden": bool(p[8]) if p[8] else False,
|
||||||
"has_background": bool(p[9]) if p[9] is not None else True,
|
"has_background": bool(p[9]) if p[9] is not None else True,
|
||||||
"source_refs": p[10],
|
"source_refs": p[10],
|
||||||
"has_clothing": p[11],
|
"has_clothing": p[11],
|
||||||
|
"content_type": p[12] or "image",
|
||||||
|
"faceswap_source_video":p[13],
|
||||||
})
|
})
|
||||||
db_images.sort(
|
db_images.sort(
|
||||||
key=lambda x: os.path.getmtime(os.path.join(output_dir, x["filename"])),
|
key=lambda x: os.path.getmtime(os.path.join(output_dir, x["filename"])),
|
||||||
@@ -723,14 +1122,181 @@ def list_images():
|
|||||||
return {"images": db_images}
|
return {"images": db_images}
|
||||||
except Exception as db_err:
|
except Exception as db_err:
|
||||||
print(f"DB error in list_images: {db_err}")
|
print(f"DB error in list_images: {db_err}")
|
||||||
# Fallback to filesystem
|
listing = os.listdir(output_dir)
|
||||||
files = [f for f in os.listdir(output_dir) if f.lower().endswith(extensions)]
|
# video poster snapshots share a video sibling's stem — don't list them as items
|
||||||
|
video_stems = {os.path.splitext(f)[0] for f in listing if f.lower().endswith(VIDEO_EXTENSIONS)}
|
||||||
|
files = [
|
||||||
|
f for f in listing
|
||||||
|
if f.lower().endswith(all_extensions)
|
||||||
|
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: os.path.getmtime(os.path.join(output_dir, x)), 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))
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/videos")
|
||||||
|
def list_videos():
|
||||||
|
"""Return available wireframe template videos."""
|
||||||
|
wireframe_dir = _load_wireframe_dir()
|
||||||
|
if not os.path.isdir(wireframe_dir):
|
||||||
|
return {"videos": []}
|
||||||
|
videos = [
|
||||||
|
f for f in sorted(os.listdir(wireframe_dir))
|
||||||
|
if f.lower().endswith(VIDEO_EXTENSIONS) and not f.startswith('.')
|
||||||
|
]
|
||||||
|
return {"videos": videos, "wireframe_dir": wireframe_dir}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/wireframe/duration/{video_name}")
|
||||||
|
def wireframe_duration(video_name: str):
|
||||||
|
"""Return duration (seconds) of a wireframe video via ffprobe."""
|
||||||
|
import subprocess
|
||||||
|
wireframe_dir = _load_wireframe_dir()
|
||||||
|
video_path = os.path.join(wireframe_dir, video_name)
|
||||||
|
if not os.path.exists(video_path):
|
||||||
|
raise HTTPException(404, f"Video not found: {video_name}")
|
||||||
|
try:
|
||||||
|
r = subprocess.run(
|
||||||
|
['ffprobe', '-v', 'error', '-show_entries', 'format=duration',
|
||||||
|
'-of', 'json', video_path],
|
||||||
|
capture_output=True, timeout=10,
|
||||||
|
)
|
||||||
|
info = json.loads(r.stdout)
|
||||||
|
duration = float(info.get('format', {}).get('duration', 0))
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(500, f"ffprobe error: {e}")
|
||||||
|
return {'video_name': video_name, 'duration': duration}
|
||||||
|
|
||||||
|
|
||||||
|
class TrimRequest(BaseModel):
|
||||||
|
video_name: str
|
||||||
|
start: float # seconds
|
||||||
|
end: float # seconds
|
||||||
|
output_name: str | None = None # auto-generated if None
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/wireframe/trim")
|
||||||
|
def trim_wireframe(req: TrimRequest):
|
||||||
|
"""Trim a wireframe video to [start, end] seconds using ffmpeg stream copy."""
|
||||||
|
import subprocess
|
||||||
|
wireframe_dir = _load_wireframe_dir()
|
||||||
|
video_path = os.path.join(wireframe_dir, req.video_name)
|
||||||
|
if not os.path.exists(video_path):
|
||||||
|
raise HTTPException(404, f"Video not found: {req.video_name}")
|
||||||
|
if req.start < 0 or req.end <= req.start:
|
||||||
|
raise HTTPException(400, "Invalid start/end: end must be > start ≥ 0")
|
||||||
|
|
||||||
|
stem = os.path.splitext(req.video_name)[0]
|
||||||
|
if req.output_name:
|
||||||
|
out_name = req.output_name if req.output_name.lower().endswith('.mp4') else req.output_name + '.mp4'
|
||||||
|
else:
|
||||||
|
out_name = f"{stem}_{int(req.start)}s-{int(req.end)}s.mp4"
|
||||||
|
|
||||||
|
out_path = os.path.join(wireframe_dir, out_name)
|
||||||
|
if os.path.exists(out_path):
|
||||||
|
raise HTTPException(409, f"File already exists: {out_name}")
|
||||||
|
|
||||||
|
r = subprocess.run(
|
||||||
|
['ffmpeg', '-y',
|
||||||
|
'-ss', str(req.start), '-to', str(req.end),
|
||||||
|
'-i', video_path,
|
||||||
|
'-c', 'copy',
|
||||||
|
out_path],
|
||||||
|
capture_output=True, timeout=120,
|
||||||
|
)
|
||||||
|
if r.returncode != 0:
|
||||||
|
raise HTTPException(500, f"ffmpeg error: {r.stderr.decode(errors='replace')[:500]}")
|
||||||
|
|
||||||
|
return {'output_name': out_name, 'start': req.start, 'end': req.end}
|
||||||
|
|
||||||
|
|
||||||
|
class FrameExtractRequest(BaseModel):
|
||||||
|
video_name: str
|
||||||
|
time: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/wireframe/frame")
|
||||||
|
def wireframe_extract_frame(req: FrameExtractRequest):
|
||||||
|
"""Extract a single frame at a given timestamp and return it as base64 PNG."""
|
||||||
|
import base64
|
||||||
|
wireframe_dir = _load_wireframe_dir()
|
||||||
|
video_path = os.path.join(wireframe_dir, req.video_name)
|
||||||
|
if not os.path.exists(video_path):
|
||||||
|
raise HTTPException(404, f"Video not found: {req.video_name}")
|
||||||
|
try:
|
||||||
|
img = _extract_frame_at(video_path, req.time)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(500, str(e))
|
||||||
|
buf = io.BytesIO()
|
||||||
|
img.save(buf, format="PNG")
|
||||||
|
return {"frame_b64": base64.b64encode(buf.getvalue()).decode()}
|
||||||
|
|
||||||
|
|
||||||
|
class FaceswapRequest(BaseModel):
|
||||||
|
model_filename: str # image from output_dir to use as face source
|
||||||
|
video_name: str # filename of template video in wireframe_dir
|
||||||
|
enhance: bool = True # GFPGAN face restoration after each frame swap
|
||||||
|
hair: bool = False # use FaceFusion with hair_swapper (requires FaceFusion install)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/faceswap/check")
|
||||||
|
def faceswap_check():
|
||||||
|
"""Report which enhancement backends are available."""
|
||||||
|
gfpgan_ok = False
|
||||||
|
try:
|
||||||
|
import gfpgan # noqa
|
||||||
|
gfpgan_ok = True
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
with open(CONFIG_PATH, 'r') as f:
|
||||||
|
conf = json.load(f)
|
||||||
|
ff_dir = os.path.expanduser(conf.get('facefusion_dir', '~/facefusion'))
|
||||||
|
ff_script = os.path.join(ff_dir, 'facefusion.py')
|
||||||
|
|
||||||
|
return {'gfpgan': gfpgan_ok, 'facefusion': os.path.exists(ff_script)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/faceswap")
|
||||||
|
def start_faceswap(req: FaceswapRequest):
|
||||||
|
output_dir = _load_output_dir()
|
||||||
|
wireframe_dir = _load_wireframe_dir()
|
||||||
|
|
||||||
|
src_path = os.path.join(output_dir, req.model_filename)
|
||||||
|
video_path = os.path.join(wireframe_dir, req.video_name)
|
||||||
|
|
||||||
|
if not os.path.exists(src_path):
|
||||||
|
raise HTTPException(404, f"Model image not found: {req.model_filename}")
|
||||||
|
if not os.path.exists(video_path):
|
||||||
|
raise HTTPException(404, f"Template video not found: {req.video_name}")
|
||||||
|
|
||||||
|
job_id = uuid.uuid4().hex[:8]
|
||||||
|
jobs[job_id] = {
|
||||||
|
"status": "running", "type": "faceswap",
|
||||||
|
"total": 1, "done": 0, "failed": 0,
|
||||||
|
"model": req.model_filename, "video": req.video_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.hair:
|
||||||
|
t = threading.Thread(
|
||||||
|
target=_faceswap_worker_ff,
|
||||||
|
args=(job_id, req.model_filename, req.video_name),
|
||||||
|
kwargs={'hair': True, 'enhance': req.enhance},
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
t = threading.Thread(
|
||||||
|
target=_faceswap_worker,
|
||||||
|
args=(job_id, req.model_filename, req.video_name),
|
||||||
|
kwargs={'enhance': req.enhance},
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
t.start()
|
||||||
|
return {"job_id": job_id, "model": req.model_filename, "video": req.video_name}
|
||||||
|
|
||||||
|
|
||||||
# --- tagging routes ----------------------------------------------------------
|
# --- tagging routes ----------------------------------------------------------
|
||||||
|
|
||||||
class TagRequest(BaseModel):
|
class TagRequest(BaseModel):
|
||||||
@@ -1167,6 +1733,203 @@ def remove_background_group(group_id: str, background_tasks: BackgroundTasks):
|
|||||||
return {"status": "processing", "group_id": group_id}
|
return {"status": "processing", "group_id": group_id}
|
||||||
|
|
||||||
|
|
||||||
|
# --- scenery generation -------------------------------------------------------
|
||||||
|
|
||||||
|
def _extract_frame_at(video_path: str, t: float) -> Image.Image:
|
||||||
|
"""Extract a single frame at time t (seconds) from a video via ffmpeg."""
|
||||||
|
import subprocess as _sp
|
||||||
|
r = _sp.run(
|
||||||
|
['ffmpeg', '-y', '-ss', str(t), '-i', video_path,
|
||||||
|
'-frames:v', '1', '-f', 'image2pipe', '-vcodec', 'png', 'pipe:1'],
|
||||||
|
capture_output=True, timeout=15,
|
||||||
|
)
|
||||||
|
if r.returncode != 0 or not r.stdout:
|
||||||
|
raise ValueError(f"ffmpeg frame extract failed: {r.stderr.decode(errors='replace')[:300]}")
|
||||||
|
return Image.open(io.BytesIO(r.stdout)).convert("RGB")
|
||||||
|
|
||||||
|
|
||||||
|
class SceneryRequest(BaseModel):
|
||||||
|
model_filename: str # person image in output_dir
|
||||||
|
scene_bytes: str | None = None # base64-encoded PNG/JPEG of the reference scene
|
||||||
|
scene_video: str | None = None # wireframe video name to extract frame from
|
||||||
|
scene_time: float = 0.0 # timestamp (seconds) to extract from video
|
||||||
|
prompt: str | None = None # override; auto-built if None
|
||||||
|
seed: int = -1
|
||||||
|
|
||||||
|
|
||||||
|
def _scenery_worker(job_id: str, model_filename: str, scene_pil: Image.Image,
|
||||||
|
prompt: str, seed: int):
|
||||||
|
output_dir = _load_output_dir()
|
||||||
|
try:
|
||||||
|
model_path = os.path.join(output_dir, model_filename)
|
||||||
|
model_pil = Image.open(model_path).convert("RGB")
|
||||||
|
png_bytes = _run_pipeline(model_pil, prompt, seed, MAX_AREA,
|
||||||
|
extra_images=[scene_pil])
|
||||||
|
ts = time.strftime("%Y%m%d_%H%M%S")
|
||||||
|
base_name = naming.get_base_name(model_filename)
|
||||||
|
out_name = f"{ts}_sc_{base_name}"
|
||||||
|
out_path = os.path.join(output_dir, out_name)
|
||||||
|
with open(out_path, "wb") as f:
|
||||||
|
f.write(png_bytes)
|
||||||
|
person = database.get_person(model_filename)
|
||||||
|
group_id = person[1] if person and person[1] else naming.get_base_name(model_filename)
|
||||||
|
try:
|
||||||
|
embedding = embeddings.generate_embedding(out_path)
|
||||||
|
next_order = database.get_next_sort_order(group_id)
|
||||||
|
database.upsert_person(out_name, filepath=out_path, embedding=embedding,
|
||||||
|
group_id=group_id, prompt=prompt,
|
||||||
|
sort_order=next_order,
|
||||||
|
source_refs=json.dumps([model_filename]))
|
||||||
|
except Exception as db_err:
|
||||||
|
print(f"[scenery] DB error: {db_err}")
|
||||||
|
jobs[job_id]["status"] = "done"
|
||||||
|
jobs[job_id]["output"] = out_name
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[scenery] error: {e}")
|
||||||
|
jobs[job_id]["status"] = "error"
|
||||||
|
jobs[job_id]["error"] = str(e)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/generate-scenery")
|
||||||
|
def generate_scenery(req: SceneryRequest):
|
||||||
|
output_dir = _load_output_dir()
|
||||||
|
wireframe_dir = _load_wireframe_dir()
|
||||||
|
|
||||||
|
model_path = os.path.join(output_dir, req.model_filename)
|
||||||
|
if not os.path.exists(model_path):
|
||||||
|
raise HTTPException(404, f"Model image not found: {req.model_filename}")
|
||||||
|
|
||||||
|
# Resolve scene image
|
||||||
|
if req.scene_bytes:
|
||||||
|
import base64
|
||||||
|
raw = base64.b64decode(req.scene_bytes)
|
||||||
|
scene_pil = Image.open(io.BytesIO(raw)).convert("RGB")
|
||||||
|
elif req.scene_video:
|
||||||
|
video_path = os.path.join(wireframe_dir, req.scene_video)
|
||||||
|
if not os.path.exists(video_path):
|
||||||
|
raise HTTPException(404, f"Scene video not found: {req.scene_video}")
|
||||||
|
try:
|
||||||
|
scene_pil = _extract_frame_at(video_path, req.scene_time)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(500, f"Frame extraction failed: {e}")
|
||||||
|
else:
|
||||||
|
raise HTTPException(400, "Provide scene_bytes or scene_video")
|
||||||
|
|
||||||
|
prompt = req.prompt or (
|
||||||
|
"Place this person into the provided scene. Keep the person's appearance, "
|
||||||
|
"lighting, and pose natural and consistent with the environment. "
|
||||||
|
"Photorealistic, high quality."
|
||||||
|
)
|
||||||
|
|
||||||
|
job_id = uuid.uuid4().hex[:8]
|
||||||
|
jobs[job_id] = {"status": "running", "type": "scenery", "total": 1, "done": 0, "failed": 0}
|
||||||
|
threading.Thread(
|
||||||
|
target=_scenery_worker,
|
||||||
|
args=(job_id, req.model_filename, scene_pil, prompt, req.seed),
|
||||||
|
daemon=True,
|
||||||
|
).start()
|
||||||
|
return {"job_id": job_id, "model": req.model_filename}
|
||||||
|
|
||||||
|
|
||||||
|
# --- SAM2 background removal --------------------------------------------------
|
||||||
|
|
||||||
|
_sam2_predictor = None
|
||||||
|
_sam2_predictor_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def _load_sam2():
|
||||||
|
"""Lazy-load SAM2 AutomaticMaskGenerator. Returns generator or False if unavailable."""
|
||||||
|
global _sam2_predictor
|
||||||
|
if _sam2_predictor is not None:
|
||||||
|
return _sam2_predictor
|
||||||
|
with _sam2_predictor_lock:
|
||||||
|
if _sam2_predictor is not None:
|
||||||
|
return _sam2_predictor
|
||||||
|
try:
|
||||||
|
from sam2.build_sam import build_sam2
|
||||||
|
from sam2.automatic_mask_generator import SAM2AutomaticMaskGenerator
|
||||||
|
with open(CONFIG_PATH) as f:
|
||||||
|
conf = json.load(f)
|
||||||
|
ckpt = os.path.expanduser(conf.get("sam2_checkpoint", "~/.sam2/sam2.1_hiera_tiny.pt"))
|
||||||
|
cfg = conf.get("sam2_config", "configs/sam2.1/sam2.1_hiera_t.yaml")
|
||||||
|
if not os.path.exists(ckpt):
|
||||||
|
raise FileNotFoundError(f"SAM2 checkpoint not found: {ckpt}")
|
||||||
|
model = build_sam2(cfg, ckpt, device="cuda")
|
||||||
|
_sam2_predictor = SAM2AutomaticMaskGenerator(model)
|
||||||
|
print(f"[sam2] loaded from {ckpt}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[sam2] not available: {e}")
|
||||||
|
_sam2_predictor = False
|
||||||
|
return _sam2_predictor
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_transparency_sam2(png_bytes: bytes) -> bytes:
|
||||||
|
"""Remove background using SAM2 (largest-area mask = subject), fallback to rembg."""
|
||||||
|
predictor = _load_sam2()
|
||||||
|
if predictor is False:
|
||||||
|
return _apply_transparency(png_bytes)
|
||||||
|
try:
|
||||||
|
import numpy as np
|
||||||
|
img = Image.open(io.BytesIO(png_bytes)).convert("RGB")
|
||||||
|
arr = np.array(img)
|
||||||
|
masks = predictor.generate(arr)
|
||||||
|
if not masks:
|
||||||
|
return _apply_transparency(png_bytes)
|
||||||
|
best = max(masks, key=lambda m: m["area"])
|
||||||
|
mask_np = (best["segmentation"].astype(np.uint8) * 255)
|
||||||
|
rgba = img.convert("RGBA")
|
||||||
|
r, g, b, _ = rgba.split()
|
||||||
|
alpha = Image.fromarray(mask_np, mode="L")
|
||||||
|
out = Image.merge("RGBA", (r, g, b, alpha))
|
||||||
|
buf = io.BytesIO()
|
||||||
|
out.save(buf, format="PNG")
|
||||||
|
return buf.getvalue()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[sam2] inference error, falling back to rembg: {e}")
|
||||||
|
return _apply_transparency(png_bytes)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/remove-background-sam/{filename}")
|
||||||
|
def remove_background_sam(filename: str):
|
||||||
|
"""SAM2-based background removal (RGBA PNG). Falls back to rembg if SAM2 unavailable."""
|
||||||
|
person = database.get_person(filename)
|
||||||
|
if not person or not person[5] or not os.path.exists(person[5]):
|
||||||
|
raise HTTPException(404, "Image file not found")
|
||||||
|
path = person[5]
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
png_bytes = f.read()
|
||||||
|
transparent_png = _apply_transparency_sam2(png_bytes)
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(transparent_png)
|
||||||
|
used_sam2 = _sam2_predictor is not False and _sam2_predictor is not None
|
||||||
|
return {"status": "success", "filename": filename, "used_sam2": used_sam2}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/restore-background/{filename}")
|
||||||
|
def restore_background(filename: str):
|
||||||
|
"""Flatten RGBA → RGB (white composite), making the image opaque again."""
|
||||||
|
person = database.get_person(filename)
|
||||||
|
if not person or not person[5] or not os.path.exists(person[5]):
|
||||||
|
raise HTTPException(404, "Image file not found")
|
||||||
|
path = person[5]
|
||||||
|
img = Image.open(path)
|
||||||
|
if img.mode == "RGBA":
|
||||||
|
bg = Image.new("RGB", img.size, (255, 255, 255))
|
||||||
|
bg.paste(img, mask=img.split()[3])
|
||||||
|
buf = io.BytesIO()
|
||||||
|
bg.save(buf, format="PNG")
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(buf.getvalue())
|
||||||
|
return {"status": "success", "filename": filename}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/sam2/check")
|
||||||
|
def sam2_check():
|
||||||
|
"""Return whether SAM2 is available."""
|
||||||
|
predictor = _load_sam2()
|
||||||
|
return {"sam2": predictor is not False and predictor is not None}
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(app, host=os.environ.get("HOST", "0.0.0.0"),
|
uvicorn.run(app, host=os.environ.get("HOST", "0.0.0.0"),
|
||||||
|
|||||||
52
tour-comfy/install_facefusion.sh
Normal file
52
tour-comfy/install_facefusion.sh
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Install FaceFusion 3.x for high-quality face+hair swap.
|
||||||
|
# Clones into ~/facefusion and creates a dedicated venv at ~/facefusion-venv.
|
||||||
|
# Usage: bash tour-comfy/install_facefusion.sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
FF_DIR="${FACEFUSION_DIR:-$HOME/facefusion}"
|
||||||
|
FF_VENV="${FACEFUSION_VENV:-$HOME/facefusion-venv}"
|
||||||
|
FF_REPO="https://github.com/facefusion/facefusion"
|
||||||
|
|
||||||
|
echo "[facefusion] Installing to $FF_DIR (venv: $FF_VENV)"
|
||||||
|
|
||||||
|
# 1. Clone or update
|
||||||
|
if [ -d "$FF_DIR/.git" ]; then
|
||||||
|
echo "[facefusion] Updating existing clone ..."
|
||||||
|
git -C "$FF_DIR" pull --ff-only
|
||||||
|
else
|
||||||
|
echo "[facefusion] Cloning $FF_REPO ..."
|
||||||
|
git clone "$FF_REPO" "$FF_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Create dedicated venv (avoids dependency conflicts with ComfyUI)
|
||||||
|
if [ ! -d "$FF_VENV" ]; then
|
||||||
|
echo "[facefusion] Creating venv at $FF_VENV ..."
|
||||||
|
python3 -m venv "$FF_VENV"
|
||||||
|
fi
|
||||||
|
|
||||||
|
PIP="$FF_VENV/bin/pip"
|
||||||
|
PY="$FF_VENV/bin/python"
|
||||||
|
|
||||||
|
"$PIP" install --upgrade pip wheel
|
||||||
|
|
||||||
|
# 3. Install FaceFusion requirements
|
||||||
|
cd "$FF_DIR"
|
||||||
|
"$PIP" install -r requirements.txt \
|
||||||
|
--extra-index-url https://download.pytorch.org/whl/cu124
|
||||||
|
|
||||||
|
# 4. Download base models (ghost_3_1_256 + gfpgan_1.4 for enhance)
|
||||||
|
echo "[facefusion] Downloading default models via FaceFusion model manager ..."
|
||||||
|
"$PY" facefusion.py \
|
||||||
|
--processors face_swapper hair_swapper face_enhancer \
|
||||||
|
--face-swapper-model ghost_3_1_256 \
|
||||||
|
--face-enhancer-model gfpgan_1.4 \
|
||||||
|
--execution-providers cpu \
|
||||||
|
download-models 2>/dev/null || true
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[facefusion] Installation complete."
|
||||||
|
echo " Binary: $PY $FF_DIR/facefusion.py"
|
||||||
|
echo " Config: set facefusion_dir/facefusion_venv in tour-comfy/config.json"
|
||||||
|
echo ""
|
||||||
|
echo "Restart the API (start_api.sh) and the 'Hair swap' toggle will activate."
|
||||||
83
tour-comfy/install_gfpgan.sh
Normal file
83
tour-comfy/install_gfpgan.sh
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Install GFPGAN face enhancement into the ComfyUI venv.
|
||||||
|
# Applies two patches for Python 3.13 + newer torchvision compatibility.
|
||||||
|
# Usage: bash tour-comfy/install_gfpgan.sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
source "$SCRIPT_DIR/env.sh"
|
||||||
|
source "$VENV/bin/activate"
|
||||||
|
|
||||||
|
PYTHON="$VENV/bin/python"
|
||||||
|
PIP="$VENV/bin/pip"
|
||||||
|
|
||||||
|
echo "[gfpgan] Step 1 — install basicsr (with Python 3.13 patch) ..."
|
||||||
|
TMPDIR=$(mktemp -d)
|
||||||
|
curl -sL "https://pypi.io/packages/source/b/basicsr/basicsr-1.4.2.tar.gz" -o "$TMPDIR/basicsr-1.4.2.tar.gz"
|
||||||
|
tar -xzf "$TMPDIR/basicsr-1.4.2.tar.gz" -C "$TMPDIR"
|
||||||
|
|
||||||
|
# Patch 1: fix get_version() — exec() doesn't update locals() in Python 3
|
||||||
|
"$PYTHON" - <<'PYPATCH'
|
||||||
|
import sys, re
|
||||||
|
setup = sys.argv[1]
|
||||||
|
with open(setup) as f:
|
||||||
|
content = f.read()
|
||||||
|
old = "def get_version():\n with open(version_file, 'r') as f:\n exec(compile(f.read(), version_file, 'exec'))\n return locals()['__version__']"
|
||||||
|
new = "def get_version():\n if not os.path.exists(version_file):\n write_version_py()\n globs = {}\n with open(version_file, 'r') as f:\n exec(compile(f.read(), version_file, 'exec'), globs)\n return globs['__version__']"
|
||||||
|
if old in content:
|
||||||
|
content = content.replace(old, new)
|
||||||
|
with open(setup, 'w') as f:
|
||||||
|
f.write(content)
|
||||||
|
print(' Patched setup.py get_version()')
|
||||||
|
else:
|
||||||
|
print(' setup.py pattern not found, skipping patch')
|
||||||
|
PYPATCH
|
||||||
|
"$TMPDIR/basicsr-1.4.2/setup.py" -- "$TMPDIR/basicsr-1.4.2/setup.py" 2>/dev/null || true
|
||||||
|
"$PYTHON" - "$TMPDIR/basicsr-1.4.2/setup.py" <<'PYPATCH'
|
||||||
|
import sys, re, os
|
||||||
|
setup = sys.argv[1]
|
||||||
|
with open(setup) as f:
|
||||||
|
content = f.read()
|
||||||
|
old = "def get_version():\n with open(version_file, 'r') as f:\n exec(compile(f.read(), version_file, 'exec'))\n return locals()['__version__']"
|
||||||
|
new = "def get_version():\n if not os.path.exists(version_file):\n write_version_py()\n globs = {}\n with open(version_file, 'r') as f:\n exec(compile(f.read(), version_file, 'exec'), globs)\n return globs['__version__']"
|
||||||
|
if old in content:
|
||||||
|
content = content.replace(old, new)
|
||||||
|
with open(setup, 'w') as f:
|
||||||
|
f.write(content)
|
||||||
|
print(' Patched setup.py get_version()')
|
||||||
|
else:
|
||||||
|
print(' setup.py already patched or pattern changed, skipping')
|
||||||
|
PYPATCH
|
||||||
|
|
||||||
|
"$PIP" install "$TMPDIR/basicsr-1.4.2/" --no-build-isolation --no-deps -q
|
||||||
|
rm -rf "$TMPDIR"
|
||||||
|
|
||||||
|
echo "[gfpgan] Step 2 — install facexlib and gfpgan ..."
|
||||||
|
"$PIP" install facexlib gfpgan -q
|
||||||
|
|
||||||
|
# Patch 2: fix torchvision functional_tensor import (removed in newer torchvision)
|
||||||
|
DEGR_PY="$VENV/lib/python3.13/site-packages/basicsr/data/degradations.py"
|
||||||
|
if [ -f "$DEGR_PY" ]; then
|
||||||
|
if grep -q "functional_tensor" "$DEGR_PY"; then
|
||||||
|
sed -i 's/from torchvision.transforms.functional_tensor import rgb_to_grayscale/from torchvision.transforms.functional import rgb_to_grayscale/' "$DEGR_PY"
|
||||||
|
echo "[gfpgan] Patched degradations.py functional_tensor import"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pre-download the model
|
||||||
|
MODEL_DIR="$HOME/.gfpgan/weights"
|
||||||
|
MODEL_PATH="$MODEL_DIR/GFPGANv1.4.pth"
|
||||||
|
mkdir -p "$MODEL_DIR"
|
||||||
|
if [ ! -f "$MODEL_PATH" ]; then
|
||||||
|
echo "[gfpgan] Downloading GFPGANv1.4.pth (~333 MB) ..."
|
||||||
|
wget -q --show-progress \
|
||||||
|
"https://github.com/TencentARC/GFPGAN/releases/download/v1.3.4/GFPGANv1.4.pth" \
|
||||||
|
-O "$MODEL_PATH.tmp"
|
||||||
|
mv "$MODEL_PATH.tmp" "$MODEL_PATH"
|
||||||
|
echo "[gfpgan] Model saved to $MODEL_PATH"
|
||||||
|
else
|
||||||
|
echo "[gfpgan] Model already present: $MODEL_PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "[gfpgan] Done. Restart the API (start_api.sh) to enable face enhancement."
|
||||||
@@ -7,7 +7,7 @@ realistic, transparent background
|
|||||||
|
|
||||||
# Feminine
|
# Feminine
|
||||||
1 female nude.
|
1 female nude.
|
||||||
Hyper realistic.
|
realistic.
|
||||||
|
|
||||||
|
|
||||||
# Upscale HQ Feminine
|
# Upscale HQ Feminine
|
||||||
|
|||||||
85
tour-comfy/prompt_pose_book.md
Normal file
85
tour-comfy/prompt_pose_book.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# Prompt-Pose Book — Sample Set
|
||||||
|
|
||||||
|
> Pure pose and anatomy descriptors. Lighting, nudity, and background are derived from the input image.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Butterfly (recline):
|
||||||
|
lying flat on back in reclined butterfly pose.
|
||||||
|
soles of feet pressed together, knees relaxed and falling open outward to sides, hips fully open.
|
||||||
|
arms resting loosely overhead or at sides.
|
||||||
|
Looking directly into camera. Serene, open.
|
||||||
|
Perfect anatomy, realistic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Celestial:
|
||||||
|
lying on back.
|
||||||
|
arms extended wide to sides in celestial spread.
|
||||||
|
legs raised toward torso, crossed at ankles, knees deeply bent, drawn close to chest.
|
||||||
|
hands reach inward to clasp around feet.
|
||||||
|
settled back slightly, showcasing deep knee bend and intricate hand-foot clasp.
|
||||||
|
Looking directly into camera. Ethereal, serene.
|
||||||
|
Perfect anatomy, realistic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Grounded vs. Vulnerable S-Curve:
|
||||||
|
kneeling on all fours, hands planted under shoulders, knees under hips.
|
||||||
|
one side grounded and stable — arm and leg firmly planted, muscles taut.
|
||||||
|
opposite arm and leg lifted off the ground, extended and vulnerable.
|
||||||
|
back arched upward in smooth S-curve, hips lowered, head lifted.
|
||||||
|
Looking directly into camera. Powerful, alluring.
|
||||||
|
Perfect anatomy, realistic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# The Kneeling (Dynamic):
|
||||||
|
kneeling on all fours, hands under shoulders, knees under hips.
|
||||||
|
one arm and opposite leg lifted off the ground, creating diagonal tension.
|
||||||
|
hips lowered toward the ground, back forming a smooth curved arc.
|
||||||
|
muscular definition in lifted limbs, continuous curved lines of torso.
|
||||||
|
Looking directly into camera. Strong, dynamic.
|
||||||
|
Perfect anatomy, realistic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# The Sphinx:
|
||||||
|
lying on stomach, propped up on forearms.
|
||||||
|
elbows under shoulders, chest lifted, back arched deeply.
|
||||||
|
legs extended straight behind, toes pointed.
|
||||||
|
head lifted, neck elongated.
|
||||||
|
Looking directly into camera. Regal, poised.
|
||||||
|
Perfect anatomy, realistic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# The Offering:
|
||||||
|
kneeling upright, sitting back on heels.
|
||||||
|
arms extended straight overhead, hands open and reaching upward.
|
||||||
|
back arched slightly, chest open, shoulders down.
|
||||||
|
hips anchored heavy on heels, spine long and stretched.
|
||||||
|
Looking directly into camera. Devotional, open.
|
||||||
|
Perfect anatomy, realistic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# The Reclining Twist:
|
||||||
|
lying on side, propped up on one elbow.
|
||||||
|
top leg bent and raised, foot resting on ground in front of bottom leg.
|
||||||
|
bottom leg extended straight, toes pointed.
|
||||||
|
top arm resting on bent knee or reaching overhead.
|
||||||
|
torso twisted slightly toward camera, hips stacked.
|
||||||
|
Looking directly into camera. Relaxed, composed.
|
||||||
|
Perfect anatomy, realistic
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# The Ascension:
|
||||||
|
lying on back.
|
||||||
|
arms extended overhead, hands clasped or reaching past the head.
|
||||||
|
legs lifted straight up together, fully extended toward ceiling.
|
||||||
|
lower back pressed flat to the ground, shoulders relaxed.
|
||||||
|
vertical line from shoulders through heels.
|
||||||
|
Looking directly into camera. Weightless, transcendent.
|
||||||
|
Perfect anatomy, realistic
|
||||||
@@ -6,6 +6,11 @@ source "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/env.sh"
|
|||||||
source "$VENV/bin/activate"
|
source "$VENV/bin/activate"
|
||||||
cd "$API_DIR"
|
cd "$API_DIR"
|
||||||
|
|
||||||
|
# Add all nvidia CUDA library paths bundled with the venv (needed by onnxruntime-gpu / insightface)
|
||||||
|
_NV_BASE="$VENV/lib/python3.13/site-packages/nvidia"
|
||||||
|
_NV_LIBPATH="$_NV_BASE/cuda_runtime/lib:$_NV_BASE/cublas/lib:$_NV_BASE/cudnn/lib:$_NV_BASE/curand/lib:$_NV_BASE/cufft/lib:$_NV_BASE/cusolver/lib:$_NV_BASE/cusparse/lib:$_NV_BASE/nvjitlink/lib:$_NV_BASE/cuda_nvrtc/lib"
|
||||||
|
export LD_LIBRARY_PATH="${_NV_LIBPATH}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||||
|
|
||||||
export COMFY_URL="http://127.0.0.1:8188"
|
export COMFY_URL="http://127.0.0.1:8188"
|
||||||
export HOST="0.0.0.0"
|
export HOST="0.0.0.0"
|
||||||
export PORT="8500"
|
export PORT="8500"
|
||||||
|
|||||||
Reference in New Issue
Block a user