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.
1874
tour-comfy/car.html
1874
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",
|
||||
"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": [
|
||||
"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",
|
||||
@@ -19,5 +19,9 @@
|
||||
"output_dir": "/mnt/zim/tour-comfy/output",
|
||||
"failed_dir": "/mnt/zim/tour-comfy/failed",
|
||||
"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 source_refs TEXT",
|
||||
"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)
|
||||
conn.commit()
|
||||
@@ -36,22 +38,24 @@ def migrate_schema():
|
||||
def upsert_person(filename, filepath=None, name=None, group_id=None, tags=None,
|
||||
embedding=None, clip_description=None, prompt=None, pose=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()
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
cur.execute("""
|
||||
INSERT INTO person (filename, filepath, name, group_id, tags, embedding,
|
||||
clip_description, prompt, pose, sort_order, group_name, hidden,
|
||||
has_background, source_refs, has_clothing)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
has_background, source_refs, has_clothing,
|
||||
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
|
||||
SET filepath = COALESCE(EXCLUDED.filepath, person.filepath),
|
||||
name = COALESCE(EXCLUDED.name, person.name),
|
||||
group_id = COALESCE(EXCLUDED.group_id, person.group_id),
|
||||
tags = COALESCE(EXCLUDED.tags, person.tags),
|
||||
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),
|
||||
pose = COALESCE(EXCLUDED.pose, person.pose),
|
||||
sort_order = COALESCE(EXCLUDED.sort_order, person.sort_order),
|
||||
@@ -59,11 +63,14 @@ def upsert_person(filename, filepath=None, name=None, group_id=None, tags=None,
|
||||
hidden = COALESCE(EXCLUDED.hidden, person.hidden),
|
||||
has_background = COALESCE(EXCLUDED.has_background, person.has_background),
|
||||
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,
|
||||
json.dumps(tags) if tags else None,
|
||||
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()
|
||||
finally:
|
||||
cur.close()
|
||||
@@ -101,7 +108,7 @@ def list_persons():
|
||||
cur.execute("""
|
||||
SELECT filename, name, group_id, clip_description,
|
||||
prompt, pose, sort_order, group_name, hidden, has_background, source_refs,
|
||||
has_clothing
|
||||
has_clothing, content_type, faceswap_source_video
|
||||
FROM person
|
||||
""")
|
||||
return cur.fetchall()
|
||||
|
||||
@@ -32,6 +32,7 @@ from PIL import Image
|
||||
from fastapi import FastAPI, UploadFile, File, Form, HTTPException, BackgroundTasks
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import Response
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from pydantic import BaseModel
|
||||
import shutil
|
||||
import re
|
||||
@@ -59,6 +60,8 @@ NODE_SAVE = "10"
|
||||
|
||||
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
|
||||
ROTATE_180_POSES = {"the dragon", "dragon", "the draak", "draak"}
|
||||
|
||||
@@ -115,6 +118,18 @@ def _watch_car_html():
|
||||
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")
|
||||
def on_startup():
|
||||
try:
|
||||
@@ -123,6 +138,14 @@ def on_startup():
|
||||
print(f"DB migration warning: {e}")
|
||||
_sync_car_html()
|
||||
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 ----------------------------------------------------------------
|
||||
@@ -311,6 +334,379 @@ def _apply_transparency(png_bytes: bytes) -> 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 ---------------------------------------------------------
|
||||
|
||||
def _load_poses():
|
||||
@@ -691,22 +1087,23 @@ def get_batch(job_id: str):
|
||||
@app.get("/images")
|
||||
def list_images():
|
||||
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 to get from DB first
|
||||
try:
|
||||
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 = []
|
||||
for p in persons:
|
||||
fpath = os.path.join(output_dir, p[0])
|
||||
if not os.path.exists(fpath):
|
||||
continue # skip orphan DB records whose file no longer exists
|
||||
continue
|
||||
db_images.append({
|
||||
"filename": p[0],
|
||||
"name": p[1],
|
||||
"group_id": p[2],
|
||||
"clip_description":p[3],
|
||||
"clip_description": p[3],
|
||||
"prompt": p[4],
|
||||
"pose": p[5],
|
||||
"sort_order": p[6],
|
||||
@@ -715,6 +1112,8 @@ def list_images():
|
||||
"has_background": bool(p[9]) if p[9] is not None else True,
|
||||
"source_refs": p[10],
|
||||
"has_clothing": p[11],
|
||||
"content_type": p[12] or "image",
|
||||
"faceswap_source_video":p[13],
|
||||
})
|
||||
db_images.sort(
|
||||
key=lambda x: os.path.getmtime(os.path.join(output_dir, x["filename"])),
|
||||
@@ -723,14 +1122,181 @@ def list_images():
|
||||
return {"images": db_images}
|
||||
except Exception as db_err:
|
||||
print(f"DB error in list_images: {db_err}")
|
||||
# Fallback to filesystem
|
||||
files = [f for f in os.listdir(output_dir) if f.lower().endswith(extensions)]
|
||||
listing = os.listdir(output_dir)
|
||||
# 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)
|
||||
return {"images": [{"filename": f} for f in files]}
|
||||
except Exception as 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 ----------------------------------------------------------
|
||||
|
||||
class TagRequest(BaseModel):
|
||||
@@ -1167,6 +1733,203 @@ def remove_background_group(group_id: str, background_tasks: BackgroundTasks):
|
||||
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__":
|
||||
import uvicorn
|
||||
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
|
||||
1 female nude.
|
||||
Hyper realistic.
|
||||
realistic.
|
||||
|
||||
|
||||
# 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"
|
||||
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 HOST="0.0.0.0"
|
||||
export PORT="8500"
|
||||
|
||||
Reference in New Issue
Block a user