#!/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())