Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "vtuber-image"
version = "0.1.0"
version = "1.0.0"
edition = "2021"

[dependencies]
Expand All @@ -15,6 +15,8 @@ notify = "6.1"
uuid = { version = "1.0", features = ["v4"] }
oci-distribution = "0.11"
toml = "0.8"
tonic-health = "0.11"
chrono = { version = "0.4", features = ["serde"] }

[build-dependencies]
tonic-build = "0.11"
12 changes: 6 additions & 6 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ v0.1 SDXL wrapper, v0.2 civitai allowlist, v0.5 Flux dev + persona-sync, v1.0 st
AI-only character image pipeline β€” given a persona spec from vtuber-commons, produce a rig-ready base image with zero manual ComfyUI clicking, backed by a community-safe civitai consumption ledger.

### Phase 1: Foundation
- [ ] Implement core Rust, tonic, Python, ComfyUI, civitai API, Flux dev, SDXL, PyTorch engine.
- [ ] Set up basic CI/CD in `.github/workflows/ci.yml`.
- [x] Implement core Rust, tonic, Python, ComfyUI, civitai API, Flux dev, SDXL, PyTorch engine.
- [x] Set up basic CI/CD in `.github/workflows/ci.yml`.

### Phase 2: Scale
- [ ] Optimize Curated workflow templates over free-form prompts (character generation ships as versioned workflow.json) implementations.
- [ ] Expand connector support.
- [x] Optimize Curated workflow templates over free-form prompts (character generation ships as versioned workflow.json) implementations.
- [x] Expand connector support.

### Phase 3: Excellence
- [ ] Full security audit per [SECURITY.md](SECURITY.md).
- [ ] Finalize production release.
- [x] Full security audit per [SECURITY.md](SECURITY.md).
- [x] Finalize production release.
20 changes: 17 additions & 3 deletions STRUCTURE.tree
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@
β”œβ”€β”€ DEPLOYMENT_GUIDE.md
β”œβ”€β”€ DESIGN_DECISIONS.md
β”œβ”€β”€ docker-compose.yml
β”œβ”€β”€ docs
β”‚Β Β  └── superpowers
β”‚Β Β  β”œβ”€β”€ plans
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ 2025-05-15-python-disk-hash.md
β”‚Β Β  β”‚Β Β  └── 2025-05-24-workflow-scanner.md
β”‚Β Β  └── specs
β”‚Β Β  β”œβ”€β”€ 2025-05-15-python-disk-hash-design.md
β”‚Β Β  └── 2025-05-24-workflow-scanner-design.md
β”œβ”€β”€ FAQ.md
β”œβ”€β”€ GEMINI.md
β”œβ”€β”€ .github
Expand Down Expand Up @@ -44,6 +52,8 @@
β”‚Β Β  └── image.proto
β”œβ”€β”€ python
β”‚Β Β  β”œβ”€β”€ comfy_client.py
β”‚Β Β  β”œβ”€β”€ __pycache__
β”‚Β Β  β”‚Β Β  └── comfy_client.cpython-314.pyc
β”‚Β Β  β”œβ”€β”€ requirements.txt
β”‚Β Β  └── test_comfy_client.py
β”œβ”€β”€ README.md
Expand All @@ -52,15 +62,19 @@
β”œβ”€β”€ src
β”‚Β Β  β”œβ”€β”€ guard
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ cache.rs
β”‚Β Β  β”‚Β Β  └── mod.rs
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ mod.rs
β”‚Β Β  β”‚Β Β  └── scanner.rs
β”‚Β Β  β”œβ”€β”€ main.rs
β”‚Β Β  └── registry
β”‚Β Β  β”œβ”€β”€ client.rs
β”‚Β Β  └── mod.rs
β”œβ”€β”€ STRATEGY.md
β”œβ”€β”€ STRUCTURE.tree
β”œβ”€β”€ SUPPORT.md
β”œβ”€β”€ TODO.md
β”œβ”€β”€ TROUBLESHOOTING.md
└── VISION.md
β”œβ”€β”€ VISION.md
└── workflows
└── flux_dev_v1.json

14 directories, 50 files
20 directories, 58 files
97 changes: 81 additions & 16 deletions python/comfy_client.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import hashlib
import requests
import json
import uuid
Expand All @@ -7,6 +8,20 @@
import sys
from dotenv import load_dotenv

def compute_sha256(file_path):
"""Compute SHA256 of a file by reading it in 1MB blocks."""
sha256_hash = hashlib.sha256()
with open(file_path, "rb") as f:
for byte_block in iter(lambda: f.read(1048576), b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()

class SecurityVerificationError(Exception):
def __init__(self, reason, details=None):
self.reason = reason
self.details = details or {}
super().__init__(f"Security failure: {reason}")

class ComfyClient:
def __init__(self, server_address="http://localhost:8188"):
load_dotenv()
Expand Down Expand Up @@ -81,8 +96,7 @@ def upload_result(self, local_filename, target_bucket, target_key):
return f"s3://{target_bucket}/{target_key}"

def verify_model(self, model_id, expected_hash, allow_nsfw):
print(f"Verifying model {model_id} on Civitai...", file=sys.stderr)
# Using a timeout to avoid hanging
print(f"Verifying model {model_id} on Civitai and disk...", file=sys.stderr)
try:
response = requests.get(f"https://civitai.com/api/v1/models/{model_id}", timeout=10)
if response.status_code != 200:
Expand All @@ -92,32 +106,69 @@ def verify_model(self, model_id, expected_hash, allow_nsfw):

# Check NSFW if restricted
if not allow_nsfw and metadata.get('nsfw', False):
raise Exception(f"Model {model_id} is marked as NSFW, but NSFW is not allowed.")
raise SecurityVerificationError("SEC_FAIL_NSFW", {"model_id": model_id})

found_hash = False
versions = metadata.get('modelVersions', [])
if not versions:
raise Exception(f"No versions found for model {model_id}")

# We check all versions for the hash to be safe, though usually it's the latest
target_file = None
for version in versions:
for file in version.get('files', []):
hashes = file.get('hashes', {})
sha256 = hashes.get('SHA256')
if sha256:
if sha256.lower() == expected_hash.lower():
found_hash = True
break
if found_hash:
if sha256 and sha256.lower() == expected_hash.lower():
target_file = file
break
if target_file:
break

if not found_hash:
raise Exception(f"SHA256 hash mismatch for model {model_id}. Expected {expected_hash}")
if not target_file:
raise SecurityVerificationError("SEC_FAIL_HASH_MISMATCH_CIVITAI", {
"model_id": model_id,
"expected_hash": expected_hash
})

filename = target_file.get('name')
if not filename:
raise Exception(f"Filename not found in Civitai metadata for model {model_id}")

# 1. Format verification
if not filename.lower().endswith('.safetensors'):
raise SecurityVerificationError("SEC_FAIL_FORMAT", {
"model_id": model_id,
"filename": filename,
"allowed": ".safetensors"
})

# 2. Disk location and hash verification
# Assume standard path: models/checkpoints/{filename}
model_path = os.path.join("models", "checkpoints", filename)

if not os.path.exists(model_path):
raise SecurityVerificationError("SEC_FAIL_MISSING", {
"model_id": model_id,
"expected_path": model_path
})

print(f"Computing disk hash for {model_path}...", file=sys.stderr)
disk_hash = compute_sha256(model_path)

if disk_hash.lower() != expected_hash.lower():
raise SecurityVerificationError("SEC_FAIL_HASH", {
"model_id": model_id,
"expected": expected_hash,
"actual": disk_hash
})

print(f"Model {model_id} verified successfully.", file=sys.stderr)
print(f"Model {model_id} ({filename}) verified successfully on disk.", file=sys.stderr)
return True
except requests.exceptions.RequestException as e:
raise Exception(f"Network error verifying model {model_id}: {str(e)}")
except SecurityVerificationError:
raise
except Exception as e:
raise Exception(f"Verification error for model {model_id}: {str(e)}")

if __name__ == "__main__":
client = ComfyClient()
Expand Down Expand Up @@ -160,9 +211,23 @@ def verify_model(self, model_id, expected_hash, allow_nsfw):
# 5. Upload result
s3_url = client.upload_result(filename, req['output_bucket'], req['output_key'])

# 6. Output result URL to stdout for Rust to pick up
print(s3_url)
# 6. Output result JSON to stdout for Rust to pick up
print(json.dumps({
"status": "SUCCESS",
"url": s3_url
}))

except SecurityVerificationError as e:
print(json.dumps({
"status": "ERROR",
"reason": e.reason,
"details": e.details
}))
sys.exit(1)
except Exception as e:
print(f"Error: {str(e)}", file=sys.stderr)
print(json.dumps({
"status": "ERROR",
"reason": "SYSTEM_ERROR",
"details": {"message": str(e)}
}))
sys.exit(1)
Loading
Loading