223 lines
8.3 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""
CHAR-004 诸葛风 · VA-05-001-R2 · 纯文生图执行脚本
分两步先锁服装 再调面部
禁用任何图像参考全程纯文字prompt
"""
import os, sys, json, time, base64, subprocess
from pathlib import Path
sys.path.insert(0, os.path.dirname(__file__))
from secrets_loader import secret, endpoint
# ── API 配置 ──
API_KEY = secret("SC-001")
BASE_URL = endpoint("EPT-001")
IMAGE_MODEL = "doubao-seedream-4-0-250828" # 已验证通过
# ── 路径配置 ──
REPO_ROOT = Path(__file__).resolve().parents[1]
JZAO_BASE = Path("/Volumes/JZAO/铸渊-ICE-GL-ZY001/OUT-输出/图片/zai-fu-fei-xiu-xian/ep01")
CANDIDATES_DIR = REPO_ROOT / "assets/candidates/D157-VA-05-001-R2" / "CHAR-004-ZhugeFeng"
# ── R2规范 · 全文字prompt ──
CHAR004_PROMPT = """
3D动漫卡通渲染国风玄幻动态漫风格画面质感与光影水平统一
男性16清瘦挺拔全身立绘正面站立姿势身高约175cm肩宽偏窄身形单薄但有力量感无佝偻萎靡感
面部小麦色肤色面部轮廓棱角分明剑眉星目眼神锐利有韧劲鼻梁挺直嘴唇偏薄下颌线清晰气质偏冷硬少年气强与温润型角色明确区分
发型纯黑色短发高束半马尾发尾有零散碎发用一根普通深棕色木簪固定无华丽发饰额前散落几缕碎发整体整洁不凌乱
上衣灰蓝色粗布交领短打右衽袖口收窄贴合手腕衣服洗得微微褪色领口袖口边缘有轻微磨毛痕迹无破洞无污渍无补丁干净整洁腰间系一根深棕色粗麻绳腰带系法简单
下装深灰蓝色同色系粗布长裤裤脚收紧塞进靴筒膝盖位置有轻微磨白痕迹无破损无污渍
鞋子黑色粗布短靴厚鞋底鞋头有轻微磨损痕迹干净无泥污
配饰无玉佩香囊首饰仅腰间麻绳上挂一个拳头大的深灰色旧布包布包平整无破损
整体清贫但不落魄整洁有骨气纯白背景全身从头到鞋完整展示画面干净
负面不要破洞不要污渍不要补丁不要乞丐不要乞丐服不要麻绳缠身不要短袖不要破损衣服不要无鞋不要半身
""".strip().replace("\n", " ")
def call_seedream(prompt_text, seed=None):
"""调用Seedream文生图API"""
url = f"{BASE_URL}/images/generations"
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
}
payload = {
"model": IMAGE_MODEL,
"prompt": prompt_text,
"size": "1440x2560",
"n": 1,
}
if seed:
payload["seed"] = seed
print(f" → POST {url}")
print(f" → model: {IMAGE_MODEL}")
# 使用curl调用避免requests SSL问题
payload_json = json.dumps(payload)
cmd = [
"curl", "-s", "-m", "120", "--noproxy", "*",
"-X", "POST", url,
"-H", f"Authorization: Bearer {API_KEY}",
"-H", "Content-Type: application/json",
"-d", payload_json
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=130)
if result.returncode != 0:
print(f" ✗ curl error: {result.stderr}")
return None
try:
data = json.loads(result.stdout)
except json.JSONDecodeError:
print(f" ✗ JSON parse error: {result.stdout[:200]}")
return None
if "error" in data:
print(f" ✗ API error: {data['error']}")
return None
# Seedream4 响应结构data[0].url
if "data" in data and len(data["data"]) > 0:
img_url = data["data"][0].get("url", "")
print(f" ✓ 生成成功: {img_url[:80]}...")
return img_url
print(f" ✗ 未找到图片URL. 响应: {json.dumps(data)[:300]}")
return None
def download_image(url, filepath):
"""下载图片到指定路径"""
Path(filepath).parent.mkdir(parents=True, exist_ok=True)
print(f" ↓ 下载到 {filepath}")
cmd = ["curl", "-s", "-m", "60", "-L", "--noproxy", "*", "-o", filepath, url]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=65)
if result.returncode != 0:
print(f" ✗ 下载失败: {result.stderr}")
return False
size = os.path.getsize(filepath) if os.path.exists(filepath) else 0
print(f" ✓ 已保存 {size/1024:.0f}KB")
return size > 1024 # 至少1KB
def scale_to_1080(src_path, dst_path):
"""1440x2560 → 1080x1920 等比缩放"""
print(f" ⇄ 缩放 {src_path}{dst_path}")
cmd = [
"sips", "-z", "1920", "1080",
str(src_path),
"--out", str(dst_path)
]
result = subprocess.run(cmd, capture_output=True, text=True)
return result.returncode == 0
def main():
print("=" * 60)
print("CHAR-004 诸葛风 · VA-05-001-R2 · 纯文生图")
print(f"模型: {IMAGE_MODEL}")
print(f"分辨率: 1440x2560 → 1080x1920")
print(f"备选数: 4")
print(f"策略: 全程纯文字 · 禁用图像参考")
print("=" * 60)
if not API_KEY:
print("✗ 未找到JIMENG_API_KEY退出")
sys.exit(1)
# ── 第一步生成4张锁服装身形 ──
print("\n── 第一步生成4张候选 ──")
timestamp = time.strftime("%Y%m%d-%H%M%S")
run_dir = JZAO_BASE / "D157-R2" / timestamp
run_dir.mkdir(parents=True, exist_ok=True)
candidates_dir = CANDIDATES_DIR
candidates_dir.mkdir(parents=True, exist_ok=True)
results = []
seeds = [42, 137, 256, 399] # 不同seed产生不同变体
for i, seed in enumerate(seeds):
print(f"\n[{i+1}/4] 生成候选 {i+1} (seed={seed})")
# 微调prompt增加面部变化
variations = [
"面部角度:正脸直视镜头",
"面部角度:微微侧头,目光坚毅",
"面部角度:略微仰视,眼神向上",
"面部角度3/4侧脸侧视左前方",
]
full_prompt = f"{CHAR004_PROMPT} {variations[i]}"
img_url = call_seedream(full_prompt, seed=seed)
if not img_url:
print(f" ✗ 候选 {i+1} 生成失败")
results.append({"candidate": i+1, "status": "failed", "error": "API call failed"})
continue
# 保存原始1440×2560
raw_name = f"CHAR-004-R2-candidate-{i+1:02d}-1440x2560.jpg"
raw_path = run_dir / raw_name
if not download_image(img_url, raw_path):
results.append({"candidate": i+1, "status": "failed", "error": "download failed"})
continue
# 缩放至1080×1920
scaled_name = f"CHAR-004-R2-candidate-{i+1:02d}-1080x1920.jpg"
scaled_path = candidates_dir / scaled_name
if not scale_to_1080(raw_path, scaled_path):
results.append({"candidate": i+1, "status": "failed", "error": "scale failed"})
continue
results.append({
"candidate": i+1,
"status": "generated",
"raw": str(raw_path),
"scaled": str(scaled_path),
"seed": seed
})
print(f" ✓ 候选 {i+1} 完成")
time.sleep(2) # 避免请求过快
# ── 汇总 ──
print("\n── 生成汇总 ──")
success = sum(1 for r in results if r["status"] == "generated")
failed = len(results) - success
print(f"成功: {success}/4")
print(f"失败: {failed}/4")
if failed > 0:
for r in results:
if r["status"] != "generated":
print(f" ✗ 候选{r['candidate']}: {r.get('error', 'unknown')}")
# 写入元数据
meta = {
"spec": "VA-05-001-R2",
"asset_id": "CHAR-004-ZhugeFeng",
"timestamp": timestamp,
"model": IMAGE_MODEL,
"resolution_raw": "1440x2560",
"resolution_final": "1080x1920",
"strategy": "pure_text_prompt_no_image_reference",
"results": results
}
meta_path = candidates_dir / "generation-meta.json"
with open(meta_path, "w") as f:
json.dump(meta, f, ensure_ascii=False, indent=2)
print(f"\n元数据已保存: {meta_path}")
return 0 if success >= 4 else 1
if __name__ == "__main__":
sys.exit(main())