- CURRENT-STATE 更新至D158 - 生成脚本: char004/char004_r3/prop002/prop002_r3/prop003 全量 - QC脚本: qc_char004_r2/r3 + qc_prop_r2 - QC报告: CHAR-004 R2~R5 + PROP全量 - generation-meta: 所有候选批次元数据 - 候选图(JPG)保留本地不入库
223 lines
8.3 KiB
Python
223 lines
8.3 KiB
Python
#!/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())
|