💰 积分余额
打开 codebuddy.cn/profile/plans-usage 登录后,看"套餐总额"和"已用",填进来蛋蛋帮你算剩余:
@@ -297,7 +377,8 @@ const KEY='eed_conversations';
let convs=JSON.parse(localStorage.getItem(KEY)||'[]');
function save(){localStorage.setItem(KEY,JSON.stringify(convs));}
function newConv(){
- cur={id:'c'+Date.now(),title:'新对话',messages:[],model:modelSel.value,pinned:false};
+ cur={id:'c'+Date.now(),title:'新对话',messages:[],model:modelSel.value,pinned:false,
+ sid:'eed_'+Date.now().toString(36)+Math.random().toString(36).slice(2,8),started:false};
convs.unshift(cur);save();renderSide();renderLog();showHint();
}
function sortedConvs(){return [...convs].sort((a,b)=>(b.pinned?1:0)-(a.pinned?1:0));}
@@ -343,6 +424,8 @@ function renderMD(src){
s=s.replace(/^>\s?(.*)$/gm,'
$1
');
s=s.replace(/^###\s+(.*)$/gm,'
$1
').replace(/^##\s+(.*)$/gm,'
$1
').replace(/^#\s+(.*)$/gm,'
$1
');
s=s.replace(/\*\*([^*]+)\*\*/g,'
$1').replace(/\*([^*]+)\*/g,'
$1').replace(/`([^`]+)`/g,'
$1');
+ s=s.replace(/!\[([^\]]*)\]\((\/media\/[^)]+\.(?:png|jpe?g|gif|webp))\)/g,'

');
+ s=s.replace(/\[([^\]]+)\]\((\/media\/[^)]+\.(?:mp4|webm|mov))\)/g,'
');
s=s.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g,'
$1');
s=s.replace(/^\s*[-*]\s+\[([ xX])\]\s+(.*)$/gm,(m,chk,t)=>'
'+(chk.toLowerCase()==='x'?'☑':'☐')+' '+t+'');
s=s.replace(/^\s*[-*]\s+(.*)$/gm,'
$1');
@@ -403,6 +486,7 @@ function delMsg(idx){
if(busy)return;
cur.messages.splice(idx,1);
if(!cur.messages.length)cur.title='新对话';
+ resetSid();
save();renderLog();renderSide();
}
function editMsg(idx){
@@ -422,11 +506,14 @@ function editMsg(idx){
cx.onclick=()=>renderLog();
sv.onclick=()=>{
const v=ta.value.trim();if(!v){renderLog();return;}
- m.text=v;cur.messages=cur.messages.slice(0,idx+1);save();renderLog();runStream(v);
+ m.text=v;cur.messages=cur.messages.slice(0,idx+1);
+ const keptHist=cur.messages.slice(0,cur.messages.length-1).map(x=>({role:x.role,text:x.text}));
+ resetSid();save();renderLog();runStream(v,keptHist);
};
}
/* ---------- 发送 / 流式 ---------- */
+function resetSid(){cur.sid='eed_'+Date.now().toString(36)+Math.random().toString(36).slice(2,8);cur.started=false;}
function buildHistory(){
const msgs=cur.messages;
const lastUserIdx=[...msgs].reverse().findIndex(m=>m.role==='me');
@@ -434,9 +521,9 @@ function buildHistory(){
const idx=msgs.length-1-lastUserIdx;
return msgs.slice(0,idx).map(m=>({role:m.role,text:m.text}));
}
-async function runStream(text){
+async function runStream(text, extraHistory){
if(busy)return;
- busy=true;$('#stop').style.display='';
+ busy=true;$('#stop').classList.add('live');
const wrap=addBubble('egg','',true);
const bub=wrap.bub;
const bw=wrap.row.querySelector('.bubwrap');
@@ -471,8 +558,9 @@ async function runStream(text){
typing.textContent='💭 思考中…';
let acc='';abortCtl=new AbortController();let thinkingParts=[];let thinkEl=null;
try{
+ const hist=extraHistory!==undefined?extraHistory:buildHistory();
const res=await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},
- body:JSON.stringify({message:text,model:cur.model,history:buildHistory()}),signal:abortCtl.signal});
+ body:JSON.stringify({message:text,model:cur.model,history:hist,session_id:cur.sid,started:!!cur.started}),signal:abortCtl.signal});
if(!res.ok)throw new Error('服务返回 '+res.status);
const reader=res.body.getReader();const dec=new TextDecoder();let buf='';
while(true){
@@ -501,7 +589,9 @@ async function runStream(text){
const nm=ev.id&&toolNames[ev.id]?toolNames[ev.id]:'';
addItem('📥','返回'+(nm?' · '+nm:''), ev.id?'id:'+ev.id:'', ev.content||'', true);}
else if(t==='delta'){acc+=ev.text;bub.innerHTML=renderMD(acc);log.scrollTop=log.scrollHeight;}
- else if(t==='done'){acc=ev.text||acc;typing.textContent='';}
+ else if(t==='done'){acc=ev.text||acc;typing.textContent='';
+ if(ev.session_id){cur.sid=ev.session_id;cur.started=true;save();}
+ if(ev.compacted)toast('📦 上下文过长已自动压缩,记忆已衔接(新场次)');}
else if(t==='usage'){
const u=ev.usage||{};const cost=ev.cost;
totalIn+=(u.input_tokens||0);totalOut+=(u.output_tokens||0);
@@ -513,7 +603,7 @@ async function runStream(text){
else if(t==='error'){acc='(出错了:'+ev.text+')';typing.textContent='';}
}
}
- }catch(e){ if(e.name!=='AbortError'){acc='(连接中断:'+e+')';} }
+ }catch(e){ if(e.name!=='AbortError'){acc='(连接中断:'+e+')';} else {acc='(已停止)';} }
if(!acc)acc='(没有回复)';
bub.innerHTML=renderMD(acc);
/* 思考过程固化:折叠在气泡上方 */
@@ -525,7 +615,7 @@ async function runStream(text){
}
if(tcount===0)trace.style.display='none';
cur.messages.push({role:'egg',text:acc,ts:Date.now()});save();renderSide();
- typing.textContent='';busy=false;$('#stop').style.display='none';abortCtl=null;
+ typing.textContent='';busy=false;$('#stop').classList.remove('live');abortCtl=null;
pump();
}
function addMe(text){
@@ -561,7 +651,9 @@ function regen(){
if(ui<0)return;
while(msgs.length&&msgs[msgs.length-1].role!=='egg')msgs.pop();
if(msgs[msgs.length-1].role==='egg')msgs.pop();
- const text=msgs[ui].text;save();renderLog();runStream(text);
+ const text=msgs[ui].text;
+ const keptHist=msgs.slice(0,ui).map(x=>({role:x.role,text:x.text}));
+ resetSid();save();renderLog();runStream(text,keptHist);
}
/* ---------- 导出 / 导入 ---------- */
@@ -611,11 +703,11 @@ function toggleMenu(){
/* ---------- 按钮 / 事件 ---------- */
$('#send').onclick=doSend;
-$('#stop').onclick=()=>{if(abortCtl)abortCtl.abort();};
+$('#stop').onclick=()=>{if(!busy){toast('当前没有在跑的任务 🥚');return;}if(abortCtl)abortCtl.abort();fetch('/api/stop',{method:'POST'}).catch(()=>{});toast('已发送停止指令,正在终止…');};
$('#newchat').onclick=newConv;
$('#collapse').onclick=()=>{$('#side').classList.toggle('collapsed');};
$('#theme').onclick=()=>{document.body.classList.toggle('light');localStorage.setItem('eed_theme',document.body.classList.contains('light')?'light':'dark');};
-$('#clear').onclick=()=>{if(!cur)return;if(!confirm('清空当前对话上下文?'))return;cur.messages=[];save();renderLog();showHint();};
+$('#clear').onclick=()=>{if(!cur)return;if(!confirm('清空当前对话上下文?'))return;cur.messages=[];resetSid();save();renderLog();showHint();};
$('#export').onclick=event=>{event.stopPropagation();toggleMenu();};
$('#import').onclick=doImport;
$('#about').onclick=()=>{$('#mModel').textContent=modelSel.value; $('#modal').style.display='flex';};
@@ -649,6 +741,65 @@ $('#btnRead').onclick=()=>{const p=prompt('要读 cang-ying 下哪个文件?\n
$('#btnSearch').onclick=()=>{const q=prompt('想联网搜什么?');if(q)userSend('【联网搜索】请使用 WebSearch 工具搜索以下问题并汇总要点:'+q.trim());};
$('#btnTrace').onclick=()=>document.body.classList.toggle('hidetrace');
$('#btnAttach').onclick=()=>$('#fileinp').click();
+$('#btnDrama').onclick=()=>{$('#dramaLog').textContent='';$('#dramaResult').innerHTML='';$('#dramaModal').style.display='flex';};
+$('#btnSkill').onclick=async()=>{
+ try{
+ const r=await fetch('/api/skills',{method:'POST'});
+ const d=await r.json();
+ const list=d.skills||[];
+ if(!list.length){toast('技能库为空');return;}
+ $('#skillList').innerHTML=list.map(s=>
+ `
+
${s.name} ${s.type==='prompt'?'💉 提示词型':'⚙️ 工具型'}
+
${s.desc||''}
+
`).join('');
+ $('#skillModal').style.display='flex';
+ document.querySelectorAll('#skillList .skill').forEach(el=>{
+ el.onclick=async()=>{
+ const id=el.dataset.id, type=el.dataset.type;
+ try{
+ const r2=await fetch('/api/skill',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action:'load',skill:id})});
+ const s=await r2.json();
+ if(type==='prompt'&&s.prompt){
+ $('#msg').value='【技能注入:'+s.name+'】\n'+s.prompt+'\n\n---\n'+ (s.usage||'请用以上方法论') +' 请开始:';
+ $('#msg').focus();toast('已注入提示词,可编辑后发送');
+ }else if(type==='tool'){
+ $('#msg').value='【工具技能:'+s.name+'】\n执行命令:'+s.tool+' <参数>\n\n请告诉我参数(如提示词/图片路径),我就跑。';
+ $('#msg').focus();toast('工具技能就位,说明参数即可');
+ }else{toast('技能无提示词内容');}
+ }catch(e){toast('技能加载失败:'+e);}
+ $('#skillModal').style.display='none';
+ };
+ });
+ }catch(e){toast('技能库加载失败:'+e);}
+};
+$('#dramaGo').onclick=async()=>{
+ const input=$('#dramaInput').value.trim();
+ if(!input){toast('先贴入剧本或分镜JSON 🥚');return;}
+ const type=$('#dramaType').value;
+ if(type==='script'&&!$('#dramaAuth').checked){toast('剧本分镜需豆包(约¥0.01/集),请勾选授权');return;}
+ const log=$('#dramaLog');log.textContent='🚀 开始…\n';
+ const btn=$('#dramaGo');btn.disabled=true;
+ try{
+ const res=await fetch('/api/agent',{method:'POST',headers:{'Content-Type':'application/json'},
+ body:JSON.stringify({input_text:input,input_type:type,episode:parseInt($('#dramaEp').value)||1,
+ frames:parseInt($('#dramaFrames').value)||49,style:'',doubao_auth:$('#dramaAuth').checked})});
+ if(!res.ok){const e=await res.json().catch(()=>({}));toast(e.reply||'请求失败');btn.disabled=false;return;}
+ const rd=res.body.getReader();const dec=new TextDecoder();let buf='';
+ while(true){const {done,value}=await rd.read();if(done)break;
+ buf+=dec.decode(value,{stream:true});
+ let idx;while((idx=buf.indexOf('\n\n'))>=0){const ev=buf.slice(0,idx);buf=buf.slice(idx+2);
+ const dm=ev.match(/data: (.+)/);if(!dm)continue;
+ try{const d=JSON.parse(dm[1]);
+ if(d.type==='agent_log'){log.textContent+=d.text+'\n';log.scrollTop=log.scrollHeight;}
+ else if(d.type==='agent_done'){log.textContent+='\n✅ 完成\n';if(d.url){$('#dramaResult').innerHTML='
';}else if(d.reply){log.textContent+=d.reply+'\n';}}
+ else if(d.type==='error'){log.textContent+='\n❌ '+d.text+'\n';}
+ }catch(e2){}
+ }
+ }
+ }catch(err){toast('出错了:'+err.message);}
+ btn.disabled=false;
+};
$('#fileinp').onchange=e=>{const f=e.target.files[0];if(!f)return;const r=new FileReader();r.onload=()=>{attach={name:f.name,text:r.result};const c=$('#attachchip');c.style.display='block';c.textContent='📎 已附:'+f.name+'(发送时一并发给蛋蛋)';};r.readAsText(f);};
modelSel.onchange=()=>{if(cur)cur.model=modelSel.value;save();};
$('#search').oninput=renderSide;
@@ -665,16 +816,30 @@ msg.addEventListener('keydown',e=>{
if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();doSend();}
else if(e.key==='Escape'&&busy){if(abortCtl)abortCtl.abort();}
});
+function uploadMedia(file,cb){
+ const r=new FileReader();
+ r.onload=()=>{
+ fetch('/api/upload',{method:'POST',headers:{'Content-Type':'application/json'},
+ body:JSON.stringify({name:file.name,data:r.result.split(',')[1]})})
+ .then(x=>x.json()).then(d=>{if(d.url)cb(d.url);else toast('上传失败');})
+ .catch(()=>toast('上传失败'));
+ };
+ r.readAsDataURL(file);
+}
msg.addEventListener('paste',e=>{
const item=[...e.clipboardData.items].find(i=>i.type.startsWith('image/'));
- if(item){e.preventDefault();toast('图片暂不支持发(蛋蛋是文本模型)');}
+ if(item){e.preventDefault();const f=item.getAsFile();if(!f)return;
+ uploadMedia(f,url=>{msg.value+=(msg.value?'\n':'')+'【附图】['+f.name+']('+url+')';autoGrow();updateCount();});}
});
/* 拖拽文件到窗口 -> 作为附件 */
['dragover','drop'].forEach(ev=>log.addEventListener(ev,e=>{if(ev==='dragover'){e.preventDefault();}}));
log.addEventListener('drop',e=>{
e.preventDefault();const f=e.dataTransfer.files[0];if(!f)return;
- if(/\.(png|jpe?g|gif|webp)$/i.test(f.name)){toast('图片暂不支持发(文本模型)');return;}
+ if(/\.(png|jpe?g|gif|webp|mp4|webm|mov)$/i.test(f.name)){
+ uploadMedia(f,url=>{msg.value+=(msg.value?'\n':'')+'【附图】['+f.name+']('+url+')';autoGrow();updateCount();});
+ return;
+ }
const r=new FileReader();r.onload=()=>{attach={name:f.name,text:r.result};const c=$('#attachchip');c.style.display='block';c.textContent='📎 已附:'+f.name+'(发送时一并发给蛋蛋)';};r.readAsText(f);
});
@@ -703,19 +868,126 @@ else{cur=convs[0];modelSel.value=cur.model||DEFAULT;renderSide();renderLog();}
"""
import os
+import uuid
+import time
+import base64
+import re
+import signal
+import sys
-def build_prompt(history, message):
+def build_prompt(history, message, limit=80000):
+ """拼对话历史:按【字节数】硬截断(默认80KB),确保 -p 参数永不超内核 128KB 单参数上限(E2BIG)。
+ 注意:内核按字节计,中文一个字占3字节,所以不能用字符数当上限。"""
lines = ["以下是你和苍耳爸爸的对话记录:"]
+ total = len(lines[0].encode("utf-8"))
+ skipped = 0
for h in history:
who = "苍耳" if h.get("role") == "me" else "蛋蛋"
- lines.append(f"{who}: {h.get('text','')}")
+ line = f"{who}: {h.get('text','')}"
+ n = len(line.encode("utf-8"))
+ if total + n > limit:
+ skipped += 1
+ continue
+ total += n
+ lines.append(line)
+ if skipped:
+ lines.insert(1, f"[较早的 {skipped} 条对话已省略,如需细节可提问]")
lines.append("")
lines.append(f"苍耳: {message}")
lines.append("蛋蛋:")
return "\n".join(lines)
+COMPACT_THRESHOLD = 180 * 1024 # 会话文件超过 180KB 触发自动压缩
+COMPACT_RESUME_MAX = 4 * 1024 * 1024 # 超过4MB的会话不尝试模型摘要(必超时),直接读尾部降级
+
+
+def maybe_compact(sid, model):
+ """会话文件过大时:先 resume 出一份摘要,再开新场次衔接。返回 dict 或 None。
+ 若模型摘要失败/文件超大,自动降级为直接读文件尾部生成原始摘要,保证永远有衔接。"""
+ f = os.path.join(os.path.expanduser("~/.codebuddy/projects/home-ls"), sid + ".jsonl")
+ if not os.path.exists(f):
+ return None
+ size = os.path.getsize(f)
+ if size < COMPACT_THRESHOLD:
+ return None
+ summary = None
+ if size <= COMPACT_RESUME_MAX:
+ summary_cmd = [CODEBUDDY, "--print", "--model", model, "--output-format", "json",
+ "--tools", "Read", "--system-prompt", EED_SYS,
+ "--resume", sid, "-p",
+ "请把当前对话的所有重要信息压缩成不超过500字的结构化摘要,包含:①关键事实 ②已做的决策 ③进行中的任务/下一步 ④爸爸的偏好。只输出摘要正文,不要任何其他内容。"]
+ try:
+ r = subprocess.run(summary_cmd, capture_output=True, text=True, timeout=120)
+ summary = ""
+ for line in r.stdout.splitlines():
+ try:
+ ev = json.loads(line)
+ except Exception:
+ continue
+ if ev.get("type") == "result":
+ summary = ev.get("result", "") or ""
+ break
+ except Exception:
+ summary = None
+ if not summary:
+ # 降级:不调模型,直接读文件尾部最近消息(永不超时、永不卡死)
+ summary = _tail_summary(f)
+ if not summary:
+ return None
+ new_sid = "eed_" + uuid.uuid4().hex[:12]
+ return {"new_sid": new_sid, "summary": summary, "old_sid": sid, "size": size}
+
+
+def _tail_summary(path, max_items=40, max_bytes=80000):
+ """不调模型:从 jsonl 尾部读最近消息,生成原始截断摘要(兜底用)。
+ codebuddy 会话格式:role 在顶层,文本块 type 为 output_text/input_text/text。"""
+ try:
+ with open(path, "rb") as fh:
+ fh.seek(0, os.SEEK_END)
+ size = fh.tell()
+ fh.seek(max(0, size - 2 * 1024 * 1024)) # 只读尾部最多2MB
+ tail = fh.read().decode("utf-8", errors="replace")
+ items = []
+ for line in tail.splitlines():
+ line = line.strip()
+ if not line:
+ continue
+ try:
+ ev = json.loads(line)
+ except Exception:
+ continue
+ if ev.get("type") != "message":
+ continue # 跳过 reasoning/snapshot 等非消息行
+ role = ev.get("role", "")
+ if role not in ("user", "assistant"):
+ continue
+ txt = ""
+ content = ev.get("content") or []
+ if isinstance(content, str):
+ txt = content
+ elif isinstance(content, list):
+ for c in content:
+ if isinstance(c, dict):
+ ct = c.get("type", "")
+ if "text" in ct: # output_text / input_text / text
+ txt += c.get("text", "")
+ if not txt.strip():
+ continue
+ who = "苍耳" if role == "user" else "蛋蛋"
+ items.append(f"{who}: {txt.strip()[:300]}")
+ if not items:
+ return ""
+ head = "[本会话文件过大,以下为自动截取的最近对话(作背景记忆):]\n"
+ body = "\n".join(items[-max_items:])
+ if len(head + body) > max_bytes:
+ body = body[-(max_bytes - len(head)):]
+ return head + body
+ except Exception:
+ return ""
+
+
def _text_of(content):
"""把工具返回内容统一成字符串(兼容 str / list[block] / dict)。"""
if content is None:
@@ -758,43 +1030,134 @@ class Handler(http.server.BaseHTTPRequestHandler):
self.wfile.write(b"0\r\n\r\n")
self.wfile.flush()
+ def _media_type(self, path):
+ if path.endswith(".png"): return "image/png"
+ if path.endswith((".jpg", ".jpeg")): return "image/jpeg"
+ if path.endswith(".gif"): return "image/gif"
+ if path.endswith(".webp"): return "image/webp"
+ if path.endswith(".mp4"): return "video/mp4"
+ if path.endswith(".webm"): return "video/webm"
+ if path.endswith(".mov"): return "video/quicktime"
+ return "application/octet-stream"
+
def do_GET(self):
- if self.path.split("?")[0] in ("/", "/index.html"):
+ p = self.path.split("?")[0]
+ if p in ("/", "/index.html"):
self._send(200, PAGE.encode("utf-8"), "text/html; charset=utf-8")
+ elif p.startswith("/media/"):
+ fpath = os.path.join(os.path.expanduser("~/cang-ying"), p[len("/media/"):])
+ if os.path.isfile(fpath):
+ with open(fpath, "rb") as fh:
+ self._send(200, fh.read(), self._media_type(fpath))
+ else:
+ self._send(404, b"not found")
else:
self._send(404, b"not found")
- def do_POST(self):
- if self.path.split("?")[0] != "/api/chat":
- self._send(404, b"not found"); return
+ def do_POST_upload(self):
+ """接收 base64 图片/视频,存入 ~/cang-ying/inbox/,返回 /media/ 访问路径。"""
try:
length = int(self.headers.get("Content-Length", 0))
raw = self.rfile.read(length) if length else b"{}"
data = json.loads(raw or b"{}")
- message = str(data.get("message", "")).strip()
- model = str(data.get("model", DEFAULT_MODEL)).strip()
- if model not in ALLOWED:
- model = DEFAULT_MODEL
- history = data.get("history", [])
- if not message:
- self._send(400, json.dumps({"reply": "(没收到内容)"}).encode("utf-8")); return
- prompt = build_prompt(history, message)
+ name = str(data.get("name", "")).strip() or "file.bin"
+ b64 = str(data.get("data", "")).strip()
+ if not b64 or not re.search(r"\.(png|jpe?g|gif|webp|mp4|webm|mov)$", name, re.I):
+ self._send(400, json.dumps({"error": "只支持图片/视频文件"}).encode("utf-8")); return
+ content = base64.b64decode(b64)
+ inbox = os.path.expanduser("~/cang-ying/inbox")
+ os.makedirs(inbox, exist_ok=True)
+ fname = time.strftime("%Y%m%d_%H%M%S") + "_" + os.path.basename(name)
+ with open(os.path.join(inbox, fname), "wb") as fh:
+ fh.write(content)
+ self._send(200, json.dumps({"url": "/media/inbox/" + fname}).encode("utf-8"))
+ except Exception as e:
+ self._send(400, json.dumps({"error": str(e)}).encode("utf-8"))
+ def do_POST_agent(self):
+ """🎬 一键短剧 Agent:贴剧本/分镜 → 全自动出成片(SSE 流式进度)"""
+ proc = None
+ try:
+ import urllib.request, sys
+ length = int(self.headers.get("Content-Length", 0))
+ raw = self.rfile.read(length) if length else b"{}"
+ data = json.loads(raw or b"{}")
+ input_text = str(data.get("input_text", "")).strip()
+ input_type = str(data.get("input_type", "storyboard"))
+ episode = int(data.get("episode", 1) or 1)
+ frames = int(data.get("frames", 49) or 49)
+ style = str(data.get("style", "")).strip()
+ if not input_text:
+ self._send(400, json.dumps({"reply": "(没贴内容)"}).encode("utf-8")); return
+ try:
+ urllib.request.urlopen("http://127.0.0.1:8188/system_stats", timeout=3)
+ except Exception:
+ self._send(503, json.dumps({"reply": "ComfyUI 没在跑,先启动 ComfyUI 再试"}).encode("utf-8")); return
+ import shutil, glob as _g
+ ws = os.path.expanduser("~/cang-ying/agent_workspace")
+ os.makedirs(ws, exist_ok=True)
+ proj = os.path.join(ws, "proj_" + uuid.uuid4().hex[:8])
+ os.makedirs(proj, exist_ok=True)
+ agent = os.path.expanduser("~/cang-ying/video-ai-system/agent_short_drama.py")
+ if input_type == "script":
+ if not data.get("doubao_auth"):
+ self._send(400, json.dumps({"reply": "剧本分镜需豆包(约¥0.01/集),请勾选授权后再试"}).encode("utf-8")); return
+ sp = os.path.join(proj, "script.txt")
+ with open(sp, "w", encoding="utf-8") as f:
+ f.write(input_text)
+ cmd = [sys.executable, agent, sp, "-e", str(episode), "--until", "compose"]
+ if style: cmd += ["--style", style]
+ else:
+ sb = os.path.join(proj, "storyboard.json")
+ with open(sb, "w", encoding="utf-8") as f:
+ f.write(input_text)
+ cmd = [sys.executable, agent, sb, "--from", "render", "--until", "compose"]
+ if style: cmd += ["--style", style]
+ cmd += ["--frames", str(frames)]
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Cache-Control", "no-cache")
- self.send_header("X-Accel-Buffering", "no")
self.send_header("Transfer-Encoding", "chunked")
self.send_header("Connection", "keep-alive")
self.end_headers()
+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
+ text=True, bufsize=1, start_new_session=True)
+ _register(proc)
+ final = ""
+ for line in proc.stdout:
+ line = line.rstrip()
+ if line:
+ self._event("agent_log", {"text": line})
+ final += line + "\n"
+ proc.wait()
+ vids = _g.glob(os.path.join(proj, "renders", "*.mp4"))
+ if vids:
+ out_p = os.path.expanduser("~/cang-ying/outputs/agent_EP.mp4")
+ shutil.copy(vids[0], out_p)
+ self._event("agent_done", {"url": "/media/outputs/agent_EP.mp4"})
+ else:
+ self._event("agent_done", {"reply": "未找到成片。\n" + final[-500:]})
+ except (BrokenPipeError, ConnectionResetError):
+ _kill_active()
+ except Exception as e:
+ print("DO_POST_AGENT_ERR:", repr(e), flush=True)
+ try: self._event("error", {"text": str(e)})
+ except Exception: pass
+ finally:
+ if proc is not None:
+ _unregister(proc)
+ if proc.poll() is None:
+ _kill_proc(proc)
+ try: self._chunk_end()
+ except Exception: pass
- acc = ""
- proc = subprocess.Popen(
- [CODEBUDDY, "--print", "--model", model, "--tools", "Read,WebSearch,Bash",
- "--no-session-persistence", "--output-format", "stream-json",
- "--system-prompt", EED_SYS, "-p", prompt],
- stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True, bufsize=1,
- )
+ def _stream_cmd(self, cmd):
+ """跑一次 codebuddy 子进程,边解析边把事件流式推给前端,返回累计文本。"""
+ proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
+ text=True, bufsize=1, start_new_session=True)
+ _register(proc)
+ acc = ""
+ try:
for line in proc.stdout:
line = line.strip()
if not line:
@@ -805,10 +1168,8 @@ class Handler(http.server.BaseHTTPRequestHandler):
continue
t = ev.get("type")
if t == "thinking":
- # 某些后端把思考作为顶层事件发出
self._event("thinking", {"text": ev.get("thinking") or ev.get("text", "")})
elif t == "tool_result":
- # 工具返回作为顶层事件
self._event("tool_result", {"id": ev.get("tool_use_id", ""),
"content": _text_of(ev.get("content", ""))})
elif t == "assistant":
@@ -827,7 +1188,6 @@ class Handler(http.server.BaseHTTPRequestHandler):
self._event("tool_result", {"id": c.get("tool_use_id", ""),
"content": _text_of(c.get("content", ""))})
elif t == "user":
- # 工具返回常以 user 消息(带 tool_result 内容块)回流
for c in ev.get("message", {}).get("content", []):
if c.get("type") == "tool_result":
self._event("tool_result", {"id": c.get("tool_use_id", ""),
@@ -839,18 +1199,139 @@ class Handler(http.server.BaseHTTPRequestHandler):
cost = ev.get("total_cost_usd", None)
if usage or cost is not None:
self._event("usage", {"usage": usage, "cost": cost})
- proc.wait()
+ finally:
+ try:
+ proc.wait()
+ except Exception:
+ pass
+ _unregister(proc)
+ if proc.poll() is None:
+ _kill_proc(proc)
+ return acc
+
+ def do_POST_skills(self):
+ """🧠 技能库:返回全部技能列表。"""
+ try:
+ sys.path.insert(0, os.path.expanduser("~/cang-ying"))
+ from skill.skill_center import list_skills
+ self._send(200, json.dumps({"skills": list_skills()}, ensure_ascii=False).encode("utf-8"))
+ except Exception as e:
+ self._send(500, json.dumps({"error": str(e)}, ensure_ascii=False).encode("utf-8"))
+
+ def do_POST_skill(self):
+ """🧠 技能执行:{action:load|run, skill, args}"""
+ try:
+ length = int(self.headers.get("Content-Length", 0))
+ raw = self.rfile.read(length) if length else b"{}"
+ data = json.loads(raw or b"{}")
+ action = str(data.get("action", "load"))
+ skill = str(data.get("skill", ""))
+ args = data.get("args") or []
+ sys.path.insert(0, os.path.expanduser("~/cang-ying"))
+ from skill.skill_center import load as sk_load, run as sk_run
+ if action == "run":
+ ok, out = sk_run(skill, args)
+ self._send(200, json.dumps({"ok": ok, "output": out}, ensure_ascii=False).encode("utf-8"))
+ else:
+ s = sk_load(skill)
+ if not s:
+ self._send(404, json.dumps({"error": "技能不存在"}).encode("utf-8"))
+ else:
+ self._send(200, json.dumps(s, ensure_ascii=False).encode("utf-8"))
+ except Exception as e:
+ self._send(500, json.dumps({"error": str(e)}, ensure_ascii=False).encode("utf-8"))
+
+ def do_POST(self):
+ path = self.path.split("?")[0]
+ if path == "/api/upload":
+ self.do_POST_upload(); return
+ if path == "/api/agent":
+ self.do_POST_agent(); return
+ if path == "/api/skills":
+ self.do_POST_skills(); return
+ if path == "/api/skill":
+ self.do_POST_skill(); return
+ if path == "/api/stop":
+ _kill_active()
+ self._send(200, json.dumps({"ok": True}).encode("utf-8"))
+ return
+ if path != "/api/chat":
+ self._send(404, b"not found"); return
+ try:
+ proc = None
+ length = int(self.headers.get("Content-Length", 0))
+ raw = self.rfile.read(length) if length else b"{}"
+ data = json.loads(raw or b"{}")
+ message = str(data.get("message", "")).strip()
+ model = str(data.get("model", DEFAULT_MODEL)).strip()
+ if model not in ALLOWED:
+ model = DEFAULT_MODEL
+ history = data.get("history", [])
+ sid = str(data.get("session_id", "")).strip()
+ started = bool(data.get("started"))
+ if not message:
+ self._send(400, json.dumps({"reply": "(没收到内容)"}).encode("utf-8")); return
+
+ self.send_response(200)
+ self.send_header("Content-Type", "text/event-stream")
+ self.send_header("Cache-Control", "no-cache")
+ self.send_header("X-Accel-Buffering", "no")
+ self.send_header("Transfer-Encoding", "chunked")
+ self.send_header("Connection", "keep-alive")
+ self.end_headers()
+
+ # ---- 会话模式:老会话 resume 续聊;新会话/重建 用 session-id 开新场 ----
+ # 关键容错:若 resume 的旧 session 已失效(被清理/不存在),--resume 后无输出,
+ # 此时自动退回「全新场次 + 历史」重新生成,保证爸爸一定有回复。
+ compacted = None
+ acc = ""
+ if sid and started:
+ compacted = maybe_compact(sid, model)
+ if compacted:
+ sid = compacted["new_sid"]
+ summary_ctx = ("\n\n[以下是本对话更早内容的自动压缩摘要,请作为背景记忆]\n"
+ + compacted["summary"])
+ cmd = [CODEBUDDY, "--print", "--model", model, "--tools", "Read,WebSearch,Bash",
+ "--output-format", "stream-json", "--system-prompt", EED_SYS,
+ "--append-system-prompt", summary_ctx,
+ "--session-id", sid, "-p", message]
+ else:
+ cmd = [CODEBUDDY, "--print", "--model", model, "--tools", "Read,WebSearch,Bash",
+ "--output-format", "stream-json", "--system-prompt", EED_SYS,
+ "--resume", sid, "-p", message]
+ acc = self._stream_cmd(cmd)
+ if not acc.strip():
+ # 退回全新场次(带历史),用新 session-id
+ sid = "eed_" + uuid.uuid4().hex[:12]
+ cmd = [CODEBUDDY, "--print", "--model", model, "--tools", "Read,WebSearch,Bash",
+ "--output-format", "stream-json", "--system-prompt", EED_SYS,
+ "--session-id", sid, "-p", build_prompt(history, message)]
+ acc2 = self._stream_cmd(cmd)
+ if acc2.strip():
+ acc = acc2
+ compacted = None
+ else:
+ if not sid:
+ sid = "eed_" + uuid.uuid4().hex[:12]
+ prompt = build_prompt(history, message)
+ cmd = [CODEBUDDY, "--print", "--model", model, "--tools", "Read,WebSearch,Bash",
+ "--output-format", "stream-json", "--system-prompt", EED_SYS,
+ "--session-id", sid, "-p", prompt]
+ acc = self._stream_cmd(cmd)
if not acc:
acc = "(蛋蛋没回话,换个说法试试~)"
- self._event("done", {"text": acc})
+ self._event("done", {"text": acc, "session_id": sid,
+ "compacted": bool(compacted),
+ "summary": (compacted or {}).get("summary", "")})
except (BrokenPipeError, ConnectionResetError):
- pass
+ _kill_active() # 浏览器断开(点停止/关页)→ 杀掉后台进程
except Exception as e:
try:
self._event("error", {"text": str(e)})
except Exception:
pass
finally:
+ # 子进程的生命周期由 _stream_cmd 负责清理;这里只需收尾 SSE 流
try:
self._chunk_end()
except Exception:
diff --git a/skill/character-sheet-generator/README.md b/skill/character-sheet-generator/README.md
new file mode 100644
index 0000000..ba0ae8a
--- /dev/null
+++ b/skill/character-sheet-generator/README.md
@@ -0,0 +1,159 @@
+# Character Sheet Generator - 人物角色设定板生成技能
+
+
+
+
+
+
+
+
+**专业AI人物角色设定板(三视图)生成提示词技能**
+
+[English](#english) | [中文](#中文)
+
+
+
+---
+
+## 中文
+
+### 简介
+
+这是一个用于AI图像生成的专业角色设定板(Character Design Sheet / Turnaround)生成技能。它可以帮助你生成标准化的人物三视图设定稿,包含:
+
+- **主视觉区**:正面 + 侧面 + 背面 半身像
+- **补充信息区**:面部特写 + 配色色板(附色值)
+- **局部细节区**:配饰、纹样、关键道具放大展示
+- **全身比例照**:带身高标尺的全身立绘
+
+### 支持的风格
+
+| 模板 | 适用场景 |
+|------|----------|
+| `base.md` | 通用基础模板,所有风格适用 |
+| `ancient_chinese.md` | 国风古风、汉服、仙侠、武侠 |
+| `realistic.md` | 真人写实、摄影级肖像 |
+| `anime.md` | 日系动漫、二次元、赛璐璐 |
+| `modern.md` | 现代都市、时装、职场 |
+| `sci-fi.md` | 赛博朋克、科幻、未来 |
+| `fantasy.md` | 西式奇幻、中世纪、DND |
+
+### 快速开始
+
+1. **选择风格模板**:根据你的角色类型选择对应的模板文件
+2. **填写参数**:替换模板中的 `{{变量名}}` 为你的角色设定
+3. **生成图片**:将完整提示词输入AI绘图工具(即梦、Midjourney、Stable Diffusion、Seedream等)
+4. **迭代优化**:根据生成结果调整参数
+
+### 模板变量说明
+
+| 变量 | 说明 | 示例 |
+|------|------|------|
+| `{{gender}}` | 性别 | 女性/男性 |
+| `{{age}}` | 外观年龄 | 20岁 |
+| `{{face_shape}}` | 脸型 | 瓜子脸 |
+| `{{expression}}` | 表情 | 微笑 |
+| `{{eyebrow_type}}` | 眉型 | 柳叶眉 |
+| `{{eye_type}}` | 眼型 | 杏仁眼 |
+| `{{eye_temperament}}` | 眼神 | 眼神傲慢 |
+| `{{hair_style}}` | 发型 | 黑长发束起 |
+| `{{hair_accessories}}` | 发饰 | 佩戴汉服头饰 |
+| `{{height}}` | 身高 | 170cm |
+| `{{temperament}}` | 气质 | 气质清冷 |
+| `{{outfit_description}}` | 服饰描述 | 粉底黑纹汉服 |
+| `{{outfit_colors}}` | 配色 | 粉+黑+金 |
+| `{{key_accessories}}` | 配饰 | 玉簪、珍珠耳坠 |
+
+### 使用示例
+
+参见 `examples/` 目录:
+- `example_ancient.md` - 国风汉服女子(参考案例)
+- `example_modern.md` - 现代职场男性
+- `example_anime.md` - 动漫二次元少女
+
+### 推荐生成参数
+
+- **尺寸**:竖版 3:4 或 2:3(如 864x1152、1024x1536)
+- **模型**:根据风格选择对应模型
+- **建议**:同时生成2-4张,挑选一致性最好的
+
+### 目录结构
+
+```
+character-sheet-skill/
+├── SKILL.md # 技能主文件(核心说明)
+├── README.md # 本说明文件
+├── templates/ # 提示词模板库
+│ ├── base.md # 通用基础模板
+│ ├── ancient_chinese.md # 国风古风模板
+│ ├── realistic.md # 真人写实模板
+│ ├── anime.md # 动漫二次元模板
+│ ├── modern.md # 现代都市模板
+│ ├── sci-fi.md # 科幻赛博模板
+│ └── fantasy.md # 西式奇幻模板
+├── examples/ # 完整示例
+│ ├── example_ancient.md
+│ ├── example_modern.md
+│ └── example_anime.md
+├── assets/ # 参考资源
+└── scripts/ # 辅助脚本
+```
+
+---
+
+## English
+
+### Introduction
+
+A professional AI character design sheet (turnaround) prompt engineering skill. Generate standardized character reference boards with:
+
+- **Main View**: Front + Side + Back bust portraits
+- **Reference Panel**: Facial close-up + Color palette with hex codes
+- **Detail Callouts**: Close-ups of accessories, patterns, key items
+- **Full Body**: Full body standing portrait with height scale
+
+### Supported Styles
+
+| Template | Use Case |
+|----------|----------|
+| `base.md` | Universal base template |
+| `ancient_chinese.md` | Chinese ancient / Hanfu / Xianxia / Wuxia |
+| `realistic.md` | Photorealistic / Photography portrait |
+| `anime.md` | Japanese anime / Cel-shading / 2D |
+| `modern.md` | Modern urban / Fashion / Contemporary |
+| `sci-fi.md` | Cyberpunk / Sci-fi / Futuristic |
+| `fantasy.md` | High fantasy / Medieval / D&D |
+
+### Quick Start
+
+1. **Choose a template** from `templates/` based on your character style
+2. **Fill in parameters** by replacing `{{variable_name}}` placeholders
+3. **Generate image** using the full prompt in AI image generators (Jimeng, Midjourney, Stable Diffusion, Seedream, etc.)
+4. **Iterate** based on results
+
+### Template Variables
+
+| Variable | Description | Example |
+|----------|-------------|---------|
+| `{{gender}}` | Character gender | Female/Male |
+| `{{age}}` | Apparent age | 20 years old |
+| `{{face_shape}}` | Face shape | Oval face |
+| `{{height}}` | Height | 170cm |
+| `{{outfit_description}}` | Outfit details | Pink and black hanfu |
+
+See `SKILL.md` for full documentation.
+
+### Recommended Settings
+
+- **Aspect Ratio**: Portrait 3:4 or 2:3 (e.g., 864x1152, 1024x1536)
+- **Tip**: Generate 2-4 variants and pick the most consistent one
+
+---
+
+## License
+
+MIT License - Feel free to use and modify.
+
+## Contributing
+
+Contributions are welcome! Feel free to submit pull requests with new style templates or improvements.
diff --git a/skill/character-sheet-generator/SKILL.md b/skill/character-sheet-generator/SKILL.md
new file mode 100644
index 0000000..d4f10f8
--- /dev/null
+++ b/skill/character-sheet-generator/SKILL.md
@@ -0,0 +1,218 @@
+---
+name: character-sheet-generator
+version: 1.0.0
+description: "专业人物角色设定板(Character Sheet / 三视图)生成技能。当用户需要生成角色三视图、人物设定图、角色参考板、原画设定稿,或提到'角色板'、'三视图'、'人物设定'、'character sheet'、'人设图'、'turnaround'时使用。支持国风古风、现代写实、动漫二次元、科幻赛博等多种风格,可生成包含正面/侧面/背面三视图、面部特写、配色色板、局部细节、身高比例对照的专业角色设定图。"
+metadata:
+ category: image-generation
+ tags: ["character-design", "ai-art", "character-sheet", "turnaround", "三视图", "人设"]
+ compatible_models: ["jimeng", "midjourney", "dall-e", "stable-diffusion", "seedream"]
+author: "AI Character Design Studio"
+---
+
+# 人物角色设定板生成器
+
+> 一个专业的AI图像生成提示词工程技能,用于生成标准化的人物角色设定板(Character Design Sheet / Turnaround)。
+
+## 技能概述
+
+本技能帮助用户生成专业级的人物角色设定板,包含:
+
+- **主视觉区**:正面 + 侧面 + 背面 三个核心视角(半身像)
+- **补充信息区**:面部特写 + 配色色板(附色值标注)
+- **局部细节区**:配饰、纹样、关键道具的放大展示
+- **全身比例照**:带身高标尺的全身立绘,含黄金比例参考
+
+## 快速使用
+
+### 基础调用格式
+
+当用户需要生成角色设定板时,按以下流程操作:
+
+1. **收集角色参数**(如用户未提供完整信息,主动询问缺失项):
+ - 性别 / 年龄外观
+ - 面部特征(脸型、眉眼、表情、气质)
+ - 发型发色
+ - 服饰风格与配色
+ - 身高体型
+ - 艺术风格(写实/动漫/古风/科幻等)
+ - 关键配饰与身份标识
+
+2. **选择对应风格模板**(从 `templates/` 目录选择):
+ - `templates/base.md` - 基础通用模板
+ - `templates/ancient_chinese.md` - 国风古风汉服(推荐)
+ - `templates/realistic.md` - 真人写实
+ - `templates/anime.md` - 动漫二次元
+ - `templates/modern.md` - 现代都市
+ - `templates/sci-fi.md` - 科幻赛博
+ - `templates/fantasy.md` - 西式奇幻
+
+3. **填充模板变量**,生成最终提示词
+
+4. **调用图像生成工具**(如 byted-seedream-image-generate 或其他AI绘图工具)生成图片
+
+### 最简调用示例
+
+```
+用户:帮我生成一个20岁古风女子的角色板,穿粉色汉服,黑长发,身高165cm
+→ 使用 ancient_chinese 模板,填充参数,生成提示词,输出图片
+```
+
+## 模板变量说明
+
+所有模板支持以下变量,使用 `{{变量名}}` 标记:
+
+| 变量名 | 说明 | 示例值 |
+|--------|------|--------|
+| `{{gender}}` | 性别 | 女性/男性 |
+| `{{age}}` | 外观年龄 | 20岁 |
+| `{{face_shape}}` | 脸型 | 瓜子脸/鹅蛋脸/方圆脸 |
+| `{{expression}}` | 表情神态 | 微笑/冷傲/温柔/轻蔑 |
+| `{{eyebrow_type}}` | 眉型 | 柳叶眉/剑眉/远山眉 |
+| `{{eye_type}}` | 眼型 | 杏仁眼/丹凤眼/桃花眼 |
+| `{{eye_temperament}}` | 眼神气质 | 眼神温柔/眼神傲慢/目光坚毅 |
+| `{{hair_style}}` | 发型 | 黑长发束起/高马尾/双马尾/发髻 |
+| `{{hair_accessories}}` | 发饰 | 佩戴汉服头饰/发簪/步摇 |
+| `{{height}}` | 身高 | 170cm/165cm/180cm |
+| `{{temperament}}` | 整体气质 | 气质霸道清冷/温婉贤淑/英气逼人 |
+| `{{outfit_description}}` | 服饰描述 | 粉底黑纹长襦裙配精美花饰刺绣 |
+| `{{outfit_colors}}` | 主要配色 | 粉色+黑色+金色 |
+| `{{key_accessories}}` | 关键配饰 | 玉坠发簪、珍珠耳坠、腰封流苏 |
+| `{{style_keywords}}` | 风格关键词 | 超写实国风,8K高清纹理 |
+| `{{background}}` | 背景 | 纯白色背景 |
+| `{{quality_tags}}` | 质量标签 | 最高品质,细节丰富,masterpiece |
+
+## 角色板标准布局规范
+
+生成的角色设定板必须严格遵循以下布局(参考行业标准):
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ [主视区] [侧视区] [背视区] │
+│ 正面半身 侧面半身 背面半身 ← 上方:三视图半身 │
+├────────────────┬────────────────────────────────────────────┤
+│ [面部特写] │ │
+│ 脸部大特写 │ [全身比例照] │
+├────────────────┤ 全身立绘 + 身高标尺 │
+│ [配色色板] │ (右侧标注cm刻度线) │
+│ 主要颜色+色值 │ │
+├────────────────┤ │
+│ [局部细节区] │ │
+│ 发饰/耳饰/纹样 │ │
+│ 等配饰放大展示 │ │
+└────────────────┴────────────────────────────────────────────┘
+```
+
+### 各区详细要求
+
+**1. 主视觉区(上方,横向三等分)**
+- 正面半身(胸部以上):清晰展示正脸、前襟、肩部装饰
+- 侧面半身(胸部以上):展示侧脸轮廓、发型侧面、侧面服饰线条
+- 背面半身(胸部以上):展示后脑勺发型、后背设计、背面装饰
+- 三个视角人物大小一致,水平对齐
+
+**2. 补充信息区(左侧)**
+- 面部特写:脸部近距离特写,清晰展示五官妆容细节
+- 配色色板:3-8个主要颜色色块,每个色块标注色值(如#F5D0C5浅粉、#1A1A1A玄黑)
+- 按服饰层次顺序排列:主色→辅色→点缀色→金属色
+
+**3. 局部细节区(左下角)**
+- 小模块网格排列,每格展示一个关键部件
+- 包含但不限于:发饰特写、耳饰/项链、腰封/腰带、鞋履、服饰纹样特写、手持道具
+- 每个细节图背景独立,与主体风格一致
+
+**4. 全身比例照(右侧,占据右侧大部分区域)**
+- 人物全身立绘,标准站姿(双手自然交叠于身前或两侧)
+- 人物右侧有垂直标尺,以10cm为刻度线,标注关键高度点
+- 可加入黄金比例参考物(如标准头高测量线)
+- 底部标注总身高(如"170cm")
+
+**5. 整体规范**
+- 背景为纯白色(#FFFFFF),无多余杂物
+- 各区之间有细分割线或适当留白
+- 各区可用小号字体标注中文/英文标签(如"主视区 Front View")
+- 所有视角保持人物形象一致性(同一人物、同一套服饰)
+
+## 风格预设库
+
+### 国风写实风格(默认推荐)
+关键词:超写实国风,真人写实风格,质感光照,自然光线,质感十足,8K高清纹理,布料褶皱自然,艺术写实风格,电影级光影
+
+### 动漫二次元风格
+关键词:anime style,cel-shading,动画赛璐璐风格,精致日系画风,清晰线稿,平涂上色,明亮色彩,二次元人设
+
+### 现代写实风格
+关键词:现代都市,摄影级写实质感,时尚大片风格,商业摄影灯光,高清皮肤纹理,真实布料质感,杂志风
+
+### 科幻赛博风格
+关键词:cyberpunk,赛博朋克,未来科技感,霓虹光效,金属质感,全息元素,机械义体,高科技面料
+
+### 西式奇幻风格
+关键词:fantasy art,魔幻风格,中世纪奇幻,精灵/骑士/法师风格,魔法光效,厚重油画质感,史诗感
+
+## 提示词组合公式
+
+标准提示词结构(按重要性排序):
+
+```
+[布局指令] + [人物基础设定] + [面部与发型] + [服饰细节] + [气质表情] + [配饰道具] + [风格与质量] + [技术参数]
+```
+
+### 布局指令(必须放在最前)
+
+```
+Character design sheet, turnaround model sheet, white background,
+top section: three-view bust portraits (front view + side view + back view),
+left section: facial close-up + color palette with hex codes + detail callouts of accessories,
+right section: full body standing portrait with height measurement scale in centimeters,
+professional character reference board, multiple views of the same character, consistent character design,
+```
+
+### 中文布局指令(用于中文模型)
+
+```
+人物角色设定板,三视图设定稿,纯白色背景,
+上方:正面半身+侧面半身+背面半身三个视角水平排列,
+左侧:面部大特写+配色色板附色值标注+发饰耳饰等局部细节放大图,
+右侧:全身立绘配身高厘米刻度标尺,
+专业原画设定稿,同一人物多角度展示,形象高度一致,
+```
+
+## 工作流
+
+当用户请求生成角色板时,执行以下步骤:
+
+1. **参数提取**:从用户描述中提取角色参数,填充模板变量
+2. **补全询问**:对关键缺失信息(性别、风格、服饰)进行简短询问,非关键项使用合理默认值
+3. **模板选择**:根据服饰/时代/风格自动选择最合适的模板,不确定时使用 `base.md`
+4. **提示词生成**:将参数填入模板,生成完整的中英文双语提示词
+5. **质量增强**:自动追加质量标签(masterpiece, best quality, 8K, ultra-detailed等)
+6. **输出结果**:输出最终提示词,并调用图像生成工具生成图片;如无图像生成工具,直接输出提示词供用户使用
+7. **迭代优化**:如用户对结果不满意,根据反馈调整参数重新生成
+
+## 提示词优化技巧
+
+1. **一致性控制**:在提示词中多次强调"same character"、"same outfit"、"consistent design"
+2. **避免背景干扰**:明确"pure white background"、"no shadows on background"
+3. **细节强化**:对关键特征重复描述(如"black long hair"在发型区和整体区各提一次)
+4. **负面提示词**:建议搭配负面提示词使用:`different characters, multiple people, mutated hands, ugly, deformed, blurry, watermark, text, signature`
+5. **尺寸建议**:推荐生成尺寸为竖版 3:4 或 2:3(如 1024x1536、864x1152),竖版更适合角色板布局
+
+## 负面提示词模板
+
+```
+(worst quality, low quality:1.4), deformed, mutated, ugly, disfigured, blurry,
+multiple characters, different outfits, inconsistent design, extra limbs,
+bad anatomy, watermark, signature, text, logo, cropped, out of frame,
+complex background, gradient background, shadows on background,
+```
+
+## 示例
+
+参见 `examples/` 目录下的完整示例:
+- `examples/example_ancient.md` - 国风汉服女子完整示例(即用户提供的参考案例)
+- `examples/example_modern.md` - 现代都市男子示例
+- `examples/example_anime.md` - 动漫少女示例
+
+## 版本历史
+
+- v1.0.0 - 初始版本,包含基础布局和5种风格预设
diff --git a/skill/character-sheet-generator/examples/example_ancient.md b/skill/character-sheet-generator/examples/example_ancient.md
new file mode 100644
index 0000000..4142d20
--- /dev/null
+++ b/skill/character-sheet-generator/examples/example_ancient.md
@@ -0,0 +1,63 @@
+# 示例:国风汉服女子(参考案例)
+
+基于用户提供的参考图片和提示词生成的完整示例。
+
+## 输入参数
+
+```yaml
+gender: 女性
+age: 20岁
+face_shape: 瓜子脸
+expression: 微笑型
+eyebrow_type: 柳叶眉
+eye_type: 杏仁眼
+eye_temperament: 眼神傲慢轻蔑
+hair_style: 黑长发束起
+hair_accessories: 佩戴汉服头饰(粉花金簪步摇)
+height: 170cm
+temperament: 气质霸道迂腐,威压感强烈
+outfit_description: 粉底黑纹长襦裙配精美花饰刺绣,宽袖流仙裙,丝绸面料,黑色腰封配粉玉流苏
+outfit_colors: 粉色+黑色+金色(#F5D0C5浅粉、#1A1A1A玄黑、#D4AF37金色)
+key_accessories: 玉簪花发饰、珍珠耳坠、粉玉腰佩流苏、刺绣牡丹纹样
+style_keywords: 【真人写实】超写实国风,写实风格,质感光照,自然光线,质感十足,8K高清纹理,布料褶皱自然,艺术写实风格,营造出震撼的视觉效果
+```
+
+## 生成的完整中文提示词
+
+```
+人物角色设定板,三视图设定稿,纯白色背景,
+上方:正面半身+侧面半身+背面半身三个视角水平排列,
+左侧:面部大特写+配色色板附色值标注+发饰耳饰等局部细节放大图,
+右侧:全身立绘配身高170cm厘米刻度标尺,
+专业原画设定稿,同一人物多角度展示,形象高度一致,
+
+【真人写实】超写实国风,现代风格,写实风格,质感光照,自然光线,质感十足,8K高清纹理,
+布料褶皱自然,艺术写实风格,营造出震撼的视觉效果,电影级光影,
+
+女性,外表20岁,黑长发束起,佩戴汉服头饰(粉花金簪步摇),
+瓜子脸带微笑型,柳叶眉杏仁眼,眼神傲慢轻蔑,
+身高170cm,气质霸道迂腐,威压感强烈,
+粉底黑纹长襦裙配精美花饰刺绣,宽袖流仙裙,丝绸面料,黑色腰封配粉玉流苏,
+主要配色:粉色+黑色+金色(#F5D0C5浅粉、#1A1A1A玄黑、#D4AF37金色),
+关键配饰:玉簪花发饰、珍珠耳坠、粉玉腰佩流苏、刺绣牡丹纹样,
+
+刺绣纹样精致,丝绸质感,古风妆造,传统中式美学,
+最高品质,细节丰富,masterpiece,best quality,8K,ultra-detailed,
+character turnaround sheet, multiple views, same character, consistent design,
+hanfu, chinese ancient clothing, oriental beauty
+```
+
+## 负面提示词
+
+```
+(worst quality, low quality:1.4), deformed, mutated, ugly, disfigured, blurry,
+multiple characters, different outfits, inconsistent design, extra limbs,
+bad anatomy, bad hands, watermark, signature, text, logo,
+modern clothing, western clothing, casual wear, complex background, shadows on background
+```
+
+## 推荐参数
+
+- 推荐尺寸:864x1152(竖版3:4)或 1024x1536(竖版2:3)
+- 推荐模型:即梦AI、Midjourney v6、Seedream
+- 生成次数:建议生成2-4张挑选最佳效果
diff --git a/skill/character-sheet-generator/examples/example_anime.md b/skill/character-sheet-generator/examples/example_anime.md
new file mode 100644
index 0000000..c607d43
--- /dev/null
+++ b/skill/character-sheet-generator/examples/example_anime.md
@@ -0,0 +1,61 @@
+# 示例:动漫二次元少女
+
+## 输入参数
+
+```yaml
+gender: 女性
+age: 16岁
+face_shape: 精致鹅蛋脸
+expression: 元气开朗的笑容
+eyebrow_type: 柔和细眉
+eye_type: 大大圆眼(星星眼高光)
+eye_temperament: 眼神明亮活泼
+hair_style: 双马尾,粉色渐变长发,蓬松刘海
+hair_accessories: 白色蝴蝶结发带,星星发夹
+height: 158cm
+temperament: 气质元气可爱,活力满满
+outfit_description: 日式水手服校服,水蓝色领子配白色三本线,白色上衣,百褶裙,白色过膝袜,黑色制服鞋
+outfit_colors: 水蓝+白色+藏青+粉色(#87CEEB水蓝、#FFFFFF纯白、#1A1A4D藏青、#FFB6C1粉)
+key_accessories: 学生书包、粉色兔子挂件、腕带装饰
+style_keywords: 日系动画风格,赛璐璐上色,精致线稿,平涂结合阴影,明亮通透的色彩,二次元美少女画风
+```
+
+## 生成的完整中文提示词
+
+```
+人物角色设定板,三视图设定稿,纯白色背景,
+上方:正面半身+侧面半身+背面半身三个视角水平排列,
+左侧:面部大特写+配色色板附色值标注+发饰服饰等局部细节放大图,
+右侧:全身立绘配身高158cm厘米刻度标尺,
+专业动画人设稿,同一角色多角度展示,形象高度一致,
+
+【动漫风格】日系动画风格,赛璐璐上色,精致线稿,平涂结合阴影,
+明亮通透的色彩,二次元美少女画风,动画原画级别,京都动画级别人设质量,
+
+女性,外表16岁,双马尾粉色渐变长发蓬松刘海,白色蝴蝶结发带星星发夹,
+精致鹅蛋脸,元气开朗的笑容,柔和细眉,大大圆眼(星星眼高光),眼神明亮活泼,
+身高158cm,气质元气可爱,活力满满,
+日式水手服校服,水蓝色领子配白色三本线,白色上衣,百褶裙,白色过膝袜,黑色制服鞋,
+主要配色:水蓝+白色+藏青+粉色(#87CEEB水蓝、#FFFFFF纯白、#1A1A4D藏青、#FFB6C1粉),
+关键配饰:学生书包、粉色兔子挂件、腕带装饰,
+
+大眼睛,高光水润瞳孔,精致五官,动漫头身比例,清晰线稿,干净上色,无多余线条,
+最高品质,masterpiece,best quality,anime style, cel shading,
+character turnaround sheet, multiple views, same character, consistent design,
+official art, anime coloring, lineart
+```
+
+## 负面提示词
+
+```
+(worst quality, low quality:1.4), deformed, mutated, ugly, disfigured, blurry,
+multiple characters, different outfits, inconsistent design, extra limbs,
+bad anatomy, bad hands, watermark, signature, text, logo,
+photorealistic, realistic, 3d render, live action, photo,
+complex background, sketch, rough lineart, messy coloring
+```
+
+## 推荐参数
+
+- 推荐尺寸:864x1152(竖版3:4)
+- 推荐模型:NovelAI、Anything V5、Seedream动漫模式
diff --git a/skill/character-sheet-generator/examples/example_modern.md b/skill/character-sheet-generator/examples/example_modern.md
new file mode 100644
index 0000000..a7ebec2
--- /dev/null
+++ b/skill/character-sheet-generator/examples/example_modern.md
@@ -0,0 +1,60 @@
+# 示例:现代都市职场男性
+
+## 输入参数
+
+```yaml
+gender: 男性
+age: 28岁
+face_shape: 棱角分明的国字脸
+expression: 沉稳自信的微笑
+eyebrow_type: 浓黑剑眉
+eye_type: 深邃双眼
+eye_temperament: 目光锐利有神
+hair_style: 利落短发,侧分,深棕色
+hair_accessories: 无(简洁商务发型)
+height: 183cm
+temperament: 气质精英干练,商务精英范
+outfit_description: 深藏青色定制三件套西装,白色法式衬衫,深蓝色暗纹领带,棕色牛津皮鞋,白色口袋巾
+outfit_colors: 深藏青+白色+棕色+金色点缀(#1A2F4B藏青、#FFFFFF纯白、#6B4423棕、#D4AF37金)
+key_accessories: 银色机械腕表、袖扣、金丝边眼镜、真皮公文包
+style_keywords: 现代都市,摄影级写实质感,时尚大片风格,商业摄影灯光,高清皮肤纹理,真实布料质感
+```
+
+## 生成的完整中文提示词
+
+```
+人物角色设定板,三视图设定稿,纯白色背景,
+上方:正面半身+侧面半身+背面半身三个视角水平排列,
+左侧:面部大特写+配色色板附色值标注+服饰配件局部细节放大图,
+右侧:全身立绘配身高183cm厘米刻度标尺,
+专业摄影级角色参考板,同一人物多角度展示,形象高度一致,
+
+【真人写实】超写实风格,摄影级质感,影棚灯光,自然光线,质感十足,
+8K高清纹理,皮肤纹理真实,布料褶皱自然,商业人像摄影风格,杂志大片质感,
+
+男性,外表28岁,利落短发侧分深棕色,无发饰,
+棱角分明的国字脸,沉稳自信的微笑,浓黑剑眉,深邃双眼,目光锐利有神,
+身高183cm,气质精英干练,商务精英范,
+深藏青色定制三件套西装,白色法式衬衫,深蓝色暗纹领带,棕色牛津皮鞋,白色口袋巾,
+主要配色:深藏青+白色+棕色+金色点缀(#1A2F4B藏青、#FFFFFF纯白、#6B4423棕、#D4AF37金),
+关键配饰:银色机械腕表、袖扣、金丝边眼镜、真皮公文包,
+
+专业妆造,真实材质感,高端时尚摄影,
+最高品质,细节丰富,masterpiece,best quality,8K,ultra-detailed,hyperrealistic,
+photorealistic, character turnaround sheet, multiple views, same character, consistent design
+```
+
+## 负面提示词
+
+```
+(worst quality, low quality:1.4), deformed, mutated, ugly, disfigured, blurry,
+multiple characters, different outfits, inconsistent design, extra limbs,
+bad anatomy, bad hands, watermark, signature, text, logo,
+anime, cartoon, 3d render, casual clothing, sportswear, t-shirt,
+complex background, shadows on background, overexposed
+```
+
+## 推荐参数
+
+- 推荐尺寸:864x1152(竖版3:4)
+- 推荐模型:Midjourney v6、DALL-E 3、Seedream写实模式
diff --git a/skill/character-sheet-generator/templates/ancient_chinese.md b/skill/character-sheet-generator/templates/ancient_chinese.md
new file mode 100644
index 0000000..93ba8f2
--- /dev/null
+++ b/skill/character-sheet-generator/templates/ancient_chinese.md
@@ -0,0 +1,116 @@
+# 国风古风汉服模板
+
+专门用于中国风古风、汉服、仙侠、武侠等中式古典风格角色设定。
+
+## 中文提示词模板
+
+```
+人物角色设定板,三视图设定稿,纯白色背景,
+上方:正面半身+侧面半身+背面半身三个视角水平排列,
+左侧:面部大特写+配色色板附色值标注+发饰耳饰等局部细节放大图,
+右侧:全身立绘配身高{{height}}厘米刻度标尺,
+专业原画设定稿,同一人物多角度展示,形象高度一致,
+
+【真人写实】超写实国风,现代风格,写实风格,质感光照,自然光线,质感十足,8K高清纹理,
+布料褶皱自然,艺术写实风格,营造出震撼的视觉效果,电影级光影,
+
+{{gender}},外表{{age}},{{hair_style}},{{hair_accessories}},
+{{face_shape}}带{{expression}},{{eyebrow_type}}{{eye_type}},{{eye_temperament}},
+身高{{height}},{{temperament}},威压感强烈,
+{{outfit_description}},
+主要配色:{{outfit_colors}},
+关键配饰:{{key_accessories}},
+
+刺绣纹样精致,丝绸质感,古风妆造,传统中式美学,
+最高品质,细节丰富,masterpiece,best quality,8K,ultra-detailed,
+character turnaround sheet, multiple views, same character, consistent design,
+hanfu, chinese ancient clothing, oriental beauty
+```
+
+## 英文提示词模板
+
+```
+Character design sheet, turnaround model sheet, pure white background,
+top row: three-view bust portraits (front view + side view + back view) arranged horizontally,
+left column: facial close-up + color palette swatches with hex codes + hair ornament and jewelry detail callouts,
+right column: full body standing portrait with height measurement scale marked in centimeters ({{height}}),
+professional Chinese ancient style character reference board, multiple views of the same character, highly consistent design,
+
+photorealistic Chinese ancient style, hanfu fashion, cinematic lighting, natural light,
+rich textures, 8K high-definition texture, natural fabric folds, artistic realistic style,
+visually stunning, oriental aesthetic,
+
+{{gender}}, appearance of {{age}} years old, {{hair_style}}, {{hair_accessories}},
+{{face_shape}} face with {{expression}}, {{eyebrow_type}}, {{eye_type}}, {{eye_temperament}},
+height {{height}}, {{temperament}}, strong presence,
+{{outfit_description}},
+main colors: {{outfit_colors}},
+key accessories: {{key_accessories}},
+
+exquisite embroidery patterns, silk texture, traditional Chinese makeup,
+masterpiece, best quality, 8K, ultra-detailed, same character, consistent design
+```
+
+## 负面提示词
+
+```
+(worst quality, low quality:1.4), deformed, mutated, ugly, disfigured, blurry,
+multiple characters, different outfits, inconsistent design, extra limbs,
+bad anatomy, bad hands, watermark, signature, text, logo,
+modern clothing, western clothing, casual wear, complex background, shadows on background
+```
+
+## 风格参数选项
+
+### 发型选项
+- 黑长发束起,佩戴汉服头饰
+- 高盘发髻配金步摇
+- 双环髻配丝带飘带
+- 单刀半翻髻配玉簪
+- 高马尾配红发带(武侠风)
+- 披散长发配花环(仙侠风)
+
+### 脸型与五官选项
+- 瓜子脸带微笑,柳叶眉杏仁眼,眼神温柔
+- 鹅蛋脸面无表情,远山眉丹凤眼,眼神清冷
+- 方圆脸带英气,剑眉星目,目光坚毅
+- 瓜子脸带轻蔑,柳叶眉桃花眼,眼神傲慢
+
+### 服饰风格选项
+- **汉服贵族版**:粉底黑纹长襦裙配精美花饰刺绣,丝绸面料,宽袖流仙裙
+- **汉服宫廷版**:明制立领长袄配马面裙,织金面料,凤冠霞帔
+- **仙侠修仙版**:白衣广袖流仙裙配轻纱披帛,仙气飘飘,玉佩流苏
+- **武侠江湖版**:劲装短打配护腕腰带,利落剪裁,皮革装饰
+- **唐风富贵版**:齐胸襦裙配大袖衫,披帛绕臂,牡丹花饰
+- **宋制清雅版**:褙子配百迭裙,素雅色调,珍珠妆面
+
+### 气质选项
+- 气质温婉贤淑,大家闺秀风范
+- 气质霸道清冷,威压感强烈
+- 气质仙气飘逸,不食人间烟火
+- 气质英气逼人,侠女风范
+- 气质雍容华贵,贵妃仪态
+- 气质迂腐刻板,老气横秋
+
+## 配色方案参考
+
+### 粉黑经典(参考案例)
+- 主色:#F5D0C5 浅粉 / #F2B4B4 樱花粉
+- 辅色:#1A1A1A 玄黑
+- 点缀:#D4AF37 金色 / #E8C4C4 肉粉
+- 过渡:#8B7355 棕褐
+
+### 青花瓷
+- 主色:#FFFFFF 月白
+- 辅色:#1A4D8F 青花蓝
+- 点缀:#D4AF37 描金
+
+### 朱砂墨
+- 主色:#1A1A1A 墨黑
+- 辅色:#C23A30 朱砂红
+- 点缀:#FFD700 金色
+
+### 翠竹青
+- 主色:#F0F5E8 月白
+- 辅色:#5B8C5A 竹青
+- 点缀:#C9A961 芽黄
diff --git a/skill/character-sheet-generator/templates/anime.md b/skill/character-sheet-generator/templates/anime.md
new file mode 100644
index 0000000..e407697
--- /dev/null
+++ b/skill/character-sheet-generator/templates/anime.md
@@ -0,0 +1,74 @@
+# 动漫二次元模板
+
+适用于日系动漫、赛璐璐风格、二次元角色设定。
+
+## 中文提示词模板
+
+```
+人物角色设定板,三视图设定稿,纯白色背景,
+上方:正面半身+侧面半身+背面半身三个视角水平排列,
+左侧:面部大特写+配色色板附色值标注+发饰服饰等局部细节放大图,
+右侧:全身立绘配身高{{height}}厘米刻度标尺,
+专业动画人设稿,同一角色多角度展示,形象高度一致,
+
+【动漫风格】日系动画风格,赛璐璐上色,精致线稿,平涂结合阴影,
+明亮通透的色彩,二次元美少女/美少年画风,动画原画级别,
+京都动画/Aniplex级别人设质量,
+
+{{gender}},外表{{age}}岁,{{hair_style}},{{hair_accessories}},
+{{face_shape}},{{expression}},{{eyebrow_type}}{{eye_type}},{{eye_temperament}},
+身高{{height}},{{temperament}},
+{{outfit_description}},
+主要配色:{{outfit_colors}},
+关键配饰:{{key_accessories}},
+
+大眼睛,高光水润瞳孔,精致五官,动漫头身比例,
+清晰线稿,干净上色,无多余线条,
+最高品质,masterpiece,best quality,anime style, cel shading,
+character turnaround sheet, multiple views, same character, consistent design,
+official art, anime coloring, lineart
+```
+
+## 英文提示词模板
+
+```
+Character design sheet, turnaround model sheet, pure white background,
+top row: three-view bust portraits (front view + side view + back view) arranged horizontally,
+left column: facial close-up + color palette swatches with hex codes + accessory detail callouts,
+right column: full body standing portrait with height measurement scale marked in centimeters ({{height}}),
+professional anime character reference sheet, multiple views of the same character, highly consistent design,
+
+Japanese anime style, cel-shading, clean lineart, flat coloring with soft shadows,
+bright vibrant colors, 2D animation style, official art quality,
+
+{{gender}}, appearance of {{age}} years old, {{hair_style}}, {{hair_accessories}},
+{{face_shape}} face, {{expression}}, {{eyebrow_type}}, {{eye_type}}, {{eye_temperament}},
+height {{height}}, {{temperament}},
+{{outfit_description}},
+main colors: {{outfit_colors}},
+key accessories: {{key_accessories}},
+
+large expressive eyes, glossy highlight pupils, delicate anime facial features, idealized proportions,
+clean linework, anime coloring,
+masterpiece, best quality, anime style, cel shading, official art, same character
+```
+
+## 负面提示词
+
+```
+(worst quality, low quality:1.4), deformed, mutated, ugly, disfigured, blurry,
+multiple characters, different outfits, inconsistent design, extra limbs,
+bad anatomy, bad hands, watermark, signature, text, logo,
+photorealistic, realistic, 3d render, live action, photo,
+complex background, sketch, rough lineart, messy coloring
+```
+
+## 风格参数选项
+
+### 动漫风格子分类
+- **萌系Q版**:二头身/三头身Q版,圆润可爱,萌系画风
+- **现代日系**:标准七头身,精致美型,京都动画风格
+- **复古赛璐璐**:90年代动画风格,明显阴影边界,复古色调
+- **赛璐璐厚涂混合**:现代动画电影风格,线稿淡,色彩层次丰富
+- **少女漫画风**:大眼睛,花眼,星光点缀,花瓣背景元素
+- **少年漫画风**:锐利线条,热血风格,棱角分明
diff --git a/skill/character-sheet-generator/templates/base.md b/skill/character-sheet-generator/templates/base.md
new file mode 100644
index 0000000..7e187a2
--- /dev/null
+++ b/skill/character-sheet-generator/templates/base.md
@@ -0,0 +1,63 @@
+# 基础通用模板
+
+适用于所有风格的通用角色设定板模板。
+
+## 中文提示词模板
+
+```
+人物角色设定板,三视图设定稿,纯白色背景,
+上方:正面半身+侧面半身+背面半身三个视角水平排列,
+左侧:面部大特写+配色色板附色值标注+发饰耳饰等局部细节放大图,
+右侧:全身立绘配身高{{height}}厘米刻度标尺,
+专业原画设定稿,同一人物多角度展示,形象高度一致,
+
+{{gender}},外表{{age}},{{hair_style}},{{hair_accessories}},
+{{face_shape}},{{expression}},{{eyebrow_type}}{{eye_type}},{{eye_temperament}},
+身高{{height}},{{temperament}},
+{{outfit_description}},
+主要配色:{{outfit_colors}},
+关键配饰:{{key_accessories}},
+
+{{style_keywords}},{{background}},{{quality_tags}},
+布料褶皱自然,细节丰富,最高品质,masterpiece,best quality,8K,ultra-detailed,
+character turnaround sheet, multiple views, same character, consistent design
+```
+
+## 英文提示词模板
+
+```
+Character design sheet, turnaround model sheet, pure white background,
+top row: three-view bust portraits (front view + side view + back view) arranged horizontally,
+left column: facial close-up + color palette swatches with hex codes + accessory detail callouts,
+right column: full body standing portrait with height measurement scale marked in centimeters ({{height}}),
+professional character reference board, multiple views of the same character, highly consistent design,
+
+{{gender}}, appearance of {{age}} years old, {{hair_style}}, {{hair_accessories}},
+{{face_shape}}, {{expression}}, {{eyebrow_type}}, {{eye_type}}, {{eye_temperament}},
+height {{height}}, {{temperament}},
+{{outfit_description}},
+main colors: {{outfit_colors}},
+key accessories: {{key_accessories}},
+
+{{style_keywords}}, {{background}}, {{quality_tags}},
+natural fabric folds, rich details, masterpiece, best quality, 8K, ultra-detailed
+```
+
+## 负面提示词
+
+```
+(worst quality, low quality:1.4), deformed, mutated, ugly, disfigured, blurry,
+multiple characters, different outfits, inconsistent design, extra limbs, missing limbs,
+bad anatomy, bad hands, missing fingers, extra fingers, watermark, signature, text, logo,
+cropped, out of frame, complex background, gradient background, shadows on background
+```
+
+## 默认参数值
+
+当用户未提供以下参数时使用默认值:
+
+| 参数 | 默认值 |
+|------|--------|
+| `{{style_keywords}}` | 超写实风格,质感光照,自然光线 |
+| `{{background}}` | 纯白色背景 |
+| `{{quality_tags}}` | 最高品质,细节丰富 |
diff --git a/skill/character-sheet-generator/templates/fantasy.md b/skill/character-sheet-generator/templates/fantasy.md
new file mode 100644
index 0000000..2502df1
--- /dev/null
+++ b/skill/character-sheet-generator/templates/fantasy.md
@@ -0,0 +1,73 @@
+# 西式奇幻模板
+
+适用于中世纪奇幻、精灵/骑士/法师、D&D/魔兽世界风格角色设定。
+
+## 中文提示词模板
+
+```
+人物角色设定板,三视图设定稿,纯白色背景,
+上方:正面半身+侧面半身+背面半身三个视角水平排列,
+左侧:面部大特写+配色色板附色值标注+武器/装备/饰品局部细节放大图,
+右侧:全身立绘配身高{{height}}厘米刻度标尺,
+专业奇幻角色设定稿,同一人物多角度展示,形象高度一致,
+
+【西式奇幻】魔幻风格,中世纪奇幻设定,厚重油画质感,史诗感,
+魔法光效点缀,皮革金属磨损细节,魔兽世界/指环王视觉风格,
+DND角色设定标准,
+
+{{gender}},外表{{age}}岁,{{hair_style}},{{hair_accessories}},
+{{face_shape}},{{expression}},{{eyebrow_type}}{{eye_type}},{{eye_temperament}},
+身高{{height}},{{temperament}},
+{{outfit_description}},
+主要配色:{{outfit_colors}},
+关键配饰:{{key_accessories}},
+
+皮革纹理真实,金属做旧磨损,布料厚重质感,
+魔法微光,符文雕刻,宝石镶嵌细节,
+最高品质,细节丰富,masterpiece,best quality,8K,ultra-detailed,
+fantasy concept art, character turnaround sheet, same character, medieval fantasy
+```
+
+## 英文提示词模板
+
+```
+Character design sheet, turnaround model sheet, pure white background,
+top row: three-view bust portraits (front view + side view + back view) arranged horizontally,
+left column: facial close-up + color palette swatches with hex codes + weapon/armor/accessory detail callouts,
+right column: full body standing portrait with height measurement scale marked in centimeters ({{height}}),
+professional fantasy character reference board, multiple views of the same character, highly consistent design,
+
+high fantasy style, medieval fantasy setting, rich oil painting texture, epic atmosphere,
+subtle magical glow effects, weathered leather and metal details,
+World of Warcraft / Lord of the Rings visual style, D&D character design standard,
+
+{{gender}}, appearance of {{age}} years old, {{hair_style}}, {{hair_accessories}},
+{{face_shape}} face, {{expression}}, {{eyebrow_type}}, {{eye_type}}, {{eye_temperament}},
+height {{height}}, {{temperament}},
+{{outfit_description}},
+main colors: {{outfit_colors}},
+key accessories: {{key_accessories}},
+
+realistic leather texture, worn and weathered metal, heavy fabric texture,
+magical shimmer, engraved runes, gemstone setting details,
+masterpiece, best quality, 8K, ultra-detailed, fantasy concept art, medieval fantasy
+```
+
+## 负面提示词
+
+```
+(worst quality, low quality:1.4), deformed, mutated, ugly, disfigured, blurry,
+multiple characters, different outfits, inconsistent design, extra limbs,
+bad anatomy, bad hands, watermark, signature, text, logo,
+modern clothing, sci-fi, cyberpunk, futuristic, gun, modern weapon,
+complex background, shadows on background
+```
+
+## 奇幻职业选项
+- **精灵游侠**:尖耳朵,皮革轻甲,长弓,森林绿棕配色,树叶披风
+- **人类骑士**:全身板甲,盾牌长剑,家徽纹章,披风,银蓝配色
+- **法师/巫师**:长袍法袍,法杖,魔法书,奥术光效,紫金/深蓝配色
+- **矮人战士**:矮壮身材,战斧,锻造板甲,胡须编织,红棕铁灰配色
+- **盗贼/刺客**:紧身皮甲,匕首兜帽,暗影色调,黑色深灰配色
+- **牧师/圣骑**:白金配色,神圣光效,战锤/圣典,教会纹章
+- **野蛮人**:皮毛皮革,巨大武器,战纹涂装,肌肉发达,大地色系
diff --git a/skill/character-sheet-generator/templates/modern.md b/skill/character-sheet-generator/templates/modern.md
new file mode 100644
index 0000000..1cd87e3
--- /dev/null
+++ b/skill/character-sheet-generator/templates/modern.md
@@ -0,0 +1,68 @@
+# 现代都市模板
+
+适用于现代时装、都市职场、休闲潮流等当代风格角色设定。
+
+## 中文提示词模板
+
+```
+人物角色设定板,三视图设定稿,纯白色背景,
+上方:正面半身+侧面半身+背面半身三个视角水平排列,
+左侧:面部大特写+配色色板附色值标注+服饰配件局部细节放大图,
+右侧:全身立绘配身高{{height}}厘米刻度标尺,
+专业时尚角色参考板,同一人物多角度展示,形象高度一致,
+
+【现代风格】现代都市风格,时尚街拍质感,高级成衣风格,
+写实与时尚插画结合,杂志大片视觉感,潮流穿搭参考,
+
+{{gender}},外表{{age}}岁,{{hair_style}},{{hair_accessories}},
+{{face_shape}},{{expression}},{{eyebrow_type}}{{eye_type}},{{eye_temperament}},
+身高{{height}},{{temperament}},
+{{outfit_description}},
+主要配色:{{outfit_colors}},
+关键配饰:{{key_accessories}},
+
+面料质感真实,版型剪裁立体,穿搭层次分明,
+最高品质,细节丰富,masterpiece,best quality,8K,ultra-detailed,
+fashion illustration, modern style, character turnaround sheet, same character
+```
+
+## 英文提示词模板
+
+```
+Character design sheet, turnaround model sheet, pure white background,
+top row: three-view bust portraits (front view + side view + back view) arranged horizontally,
+left column: facial close-up + color palette swatches with hex codes + fabric and accessory detail callouts,
+right column: full body standing portrait with height measurement scale marked in centimeters ({{height}}),
+professional modern fashion character reference board, multiple views of the same character,
+
+modern urban style, street fashion aesthetic, high-end ready-to-wear,
+fashion editorial look, trendy outfit reference,
+
+{{gender}}, appearance of {{age}} years old, {{hair_style}}, {{hair_accessories}},
+{{face_shape}} face, {{expression}}, {{eyebrow_type}}, {{eye_type}}, {{eye_temperament}},
+height {{height}}, {{temperament}},
+{{outfit_description}},
+main colors: {{outfit_colors}},
+key accessories: {{key_accessories}},
+
+realistic fabric textures, structured tailoring, layered styling,
+masterpiece, best quality, 8K, ultra-detailed, fashion illustration, same character
+```
+
+## 负面提示词
+
+```
+(worst quality, low quality:1.4), deformed, mutated, ugly, disfigured, blurry,
+multiple characters, different outfits, inconsistent design, extra limbs,
+bad anatomy, bad hands, watermark, signature, text, logo,
+historical clothing, armor, hanfu, kimono, medieval clothing,
+complex background, shadows on background
+```
+
+## 穿搭风格选项
+- **职场精英**:剪裁合体西装,衬衫领带/丝巾,干练皮鞋
+- **休闲街头**:oversize卫衣,牛仔裤,运动鞋,棒球帽
+- **高街潮流**:设计师品牌,潮牌单品,层次感叠穿
+- **学院风**:针织衫,格纹裙/百褶裙,乐福鞋,学院徽章
+- **轻熟优雅**:针织连衣裙,小香风外套,低跟鞋,珍珠配饰
+- **运动风**:运动套装,跑鞋,发带,运动手表
diff --git a/skill/character-sheet-generator/templates/realistic.md b/skill/character-sheet-generator/templates/realistic.md
new file mode 100644
index 0000000..dd8c0c5
--- /dev/null
+++ b/skill/character-sheet-generator/templates/realistic.md
@@ -0,0 +1,76 @@
+# 真人写实模板
+
+适用于摄影级写实风格、现代真人、肖像级角色设定。
+
+## 中文提示词模板
+
+```
+人物角色设定板,三视图设定稿,纯白色背景,
+上方:正面半身+侧面半身+背面半身三个视角水平排列,
+左侧:面部大特写+配色色板附色值标注+服饰配件局部细节放大图,
+右侧:全身立绘配身高{{height}}厘米刻度标尺,
+专业摄影级角色参考板,同一人物多角度展示,形象高度一致,
+
+【真人写实】超写实风格,摄影级质感,影棚灯光,自然光线,质感十足,
+8K高清纹理,皮肤纹理真实,布料褶皱自然,毛孔细节可见,
+商业人像摄影风格,杂志大片质感,
+
+{{gender}},外表{{age}}岁,{{hair_style}},{{hair_accessories}},
+{{face_shape}},{{expression}},{{eyebrow_type}}{{eye_type}},{{eye_temperament}},
+身高{{height}},{{temperament}},
+{{outfit_description}},
+主要配色:{{outfit_colors}},
+关键配饰:{{key_accessories}},
+
+专业妆造,真实材质感,高端时尚摄影,
+最高品质,细节丰富,masterpiece,best quality,8K,ultra-detailed,hyperrealistic,
+photorealistic, character turnaround sheet, multiple views, same character, consistent design
+```
+
+## 英文提示词模板
+
+```
+Character design sheet, turnaround model sheet, pure white studio background,
+top row: three-view bust portraits (front view + side view + back view) arranged horizontally,
+left column: facial close-up + color palette swatches with hex codes + fabric and accessory detail callouts,
+right column: full body standing portrait with height measurement scale marked in centimeters ({{height}}),
+professional photorealistic character reference board, multiple views of the same character, highly consistent design,
+
+hyperrealistic photography, studio lighting, natural light, rich textures,
+8K high-definition, realistic skin texture with visible pores, natural fabric wrinkles,
+commercial portrait photography, fashion editorial quality,
+
+{{gender}}, appearance of {{age}} years old, {{hair_style}}, {{hair_accessories}},
+{{face_shape}} face, {{expression}}, {{eyebrow_type}}, {{eye_type}}, {{eye_temperament}},
+height {{height}}, {{temperament}},
+{{outfit_description}},
+main colors: {{outfit_colors}},
+key accessories: {{key_accessories}},
+
+professional makeup, realistic material textures, high-end fashion photography,
+masterpiece, best quality, 8K, ultra-detailed, hyperrealistic, photorealistic, same character
+```
+
+## 负面提示词
+
+```
+(worst quality, low quality:1.4), deformed, mutated, ugly, disfigured, blurry,
+multiple characters, different outfits, inconsistent design, extra limbs,
+bad anatomy, bad hands, watermark, signature, text, logo,
+anime, cartoon, 3d render, cgi, painting, illustration, drawing, anime style,
+complex background, shadows on background, overexposed
+```
+
+## 风格参数选项
+
+### 摄影灯光选项
+- 影棚柔光箱灯光,均匀布光,商业人像标准
+- 自然窗光,柔和侧光,文艺肖像风格
+- 硬光摄影,高对比度,时尚杂志风格
+- 逆光轮廓光,发丝光,电影感肖像
+
+### 妆造风格选项
+- 自然裸妆,清透底妆,日常感
+- 精致浓妆,烟熏眼妆,晚宴风格
+- 复古港风妆,红唇卷发,90年代风格
+- 韩系清透妆,水光肌,偶像风格
diff --git a/skill/character_sheet_learn.md b/skill/character_sheet_learn.md
new file mode 100644
index 0000000..1ded6bc
--- /dev/null
+++ b/skill/character_sheet_learn.md
@@ -0,0 +1,64 @@
+# 人物三视图/四视图/角色版 · 系统学习笔记(2026-08-01)
+
+> 来源:GitHub character-sheet-generator(开源技能)、GPT-Image-2 五模块法(CSDN piaoxue166)、
+> NanoBanana/即梦/高定 art 教程、AI漫剧角色一致性系列文章
+
+## 一、人物三视图(front/side/back)
+**定义**:同一角色正面、90°侧面、背面三视角,角色设计标准配置。
+**标准 prompt 结构**:
+```
+character design sheet / turnaround model sheet, pure white background,
+三视图平行排开(front / side profile / back),full body 或 半身 bust,
+同一角色,形象高度一致,无崩
+```
+**关键**:三面要一致(脸型/发型/服饰/比例),量化描述(8头身/172cm)+服饰分层(领+纹+饰)。
+
+## 二、人物四视图(front/back/side/face close-up)
+**定义**:正面 + 背面 + 侧面 + 脸部特写(爸爸定的标准;社区头条文章:"肩部以上的正面特写 + 全身三视图")。
+**标准 prompt**(社区主流"主视觉+三视图"):
+```
+character sheet, 16:9, 纯白背景, 平光无阴影,
+左侧 1/3:面部大特写(发丝级细节),
+右侧 2/3:并排正面/侧面/背面全身站姿,
+同一角色,8头身/172cm,服饰褶皱/配饰位置跨视角完全一致
+```
+**替代布局**:3D 展示台(上三视图 + 下细节特写组——面部/面料/配饰)。
+
+## 三、人物角色版 Character Sheet(开源技能标准四区布局)
+```
+┌───────────────────────────────────────────┐
+│ [主视区] [侧视区] [背视区] ← 上方三视图半身 │
+├──────────────┬────────────────────────────┤
+│ [面部特写] │ │
+│ [配色色板] │ [全身比例照] │
+│ 附色值 │ 身高标尺(cm) │
+│ [局部细节区] │ │
+│ 配饰/纹样放大│ │
+└──────────────┴────────────────────────────┘
+```
+
+**完整变量体系**(开源技能):gender/age/face_shape/expression/eyebrow_type/eye_type/eye_temperament/hair_style/hair_accessories/height/temperament/outfit_description/outfit_colors/key_accessories/style_keywords/background/quality_tags
+
+**7 风格模板**:base(通用) / ancient_chinese(国风汉服) / realistic(写实) / anime(日系动漫) / modern(都市) / sci-fi(科幻) / fantasy(西式奇幻)
+
+**推荐尺寸**:竖版 3:4 或 2:3(864x1152 / 1024x1536)
+
+**负面提示词(社区标准)**:
+```
+(worst quality, low quality:1.4), deformed, mutated, ugly, disfigured, blurry,
+multiple characters, different outfits, inconsistent design, extra limbs,
+bad anatomy, bad hands, missing fingers, watermark, signature, text, logo
+```
+
+## 四、用法(怎么用)
+1. **选模板**:按角色风格选(动漫→anime.md,古风→ancient_chinese.md)
+2. **填变量**:{{变量}} 替换为角色设定(性别/年龄/脸型/发型/服饰分层/配饰)
+3. **生成**:输入绘图工具(咱=Z-Image 本地)
+4. **迭代**:生成 2-4 张挑一致性最好的;做动画/漫剧时用设定板锁角色身份
+
+## 五、一致性铁律(学到的最核心)
+1. 量化身材(8头身/172cm)+ 服饰分层描述(领+纹+饰)
+2. 重复"同一角色/完全一致"关键词
+3. 平光无阴影(减少干扰)
+4. 相似 seed 系列锁风格
+5. 设定板 = 角色资产库,后续分镜/动画统一引用(LibTV 式"资产库+多视图锁定")
diff --git a/skill/community_techniques.md b/skill/community_techniques.md
new file mode 100644
index 0000000..08b4c44
--- /dev/null
+++ b/skill/community_techniques.md
@@ -0,0 +1,89 @@
+# 社区技巧库 · AI漫剧/角色一致性(2026-08-01 网络搜集)
+
+> 来源:知乎《100天AI漫剧出海》系列、CSDN 漫剧攻略、搜狐角色一致性解析、
+> NanoBanana 三视图教程、头条分镜教程、B站漫剧教程等 10+ 篇
+
+## 一、角色一致性(行业第一大痛点:76% 创作者)
+**核心观点**:一致性不是靠单一提示词锁死的,是"资产库 + 多视图 + 参数固定"的组合拳。
+1. **资产库锁定**:先定妆(三视图/多视图)→ 存为资产 → 后续所有分镜统一引用
+2. **参数固定**:seed、模型、负面词、风格描述全局一致(我们 Agent 已做 seed=1000+i*17)
+3. **提示词锁定**:角色特征写成固定模板(发型/发色/发饰/服装/配饰/鞋/体型)插入每个镜头
+4. **三视图的真正价值**:能被后续分镜环节**有效复用**(不是画得好看,是"标准统一可复用")
+
+## 二、三视图提示词 · 社区标准模板(GPT-Image-2 / NanoBanana / 高定 / 即梦)
+把提示词拆成 5 个模块,缺一不可:
+1. **画面类型**:character design sheet / character sheet / 设定图 / 角色设计
+2. **排版结构**:**核心:左 1/3 脸部特写 + 右 2/3 三全身(社区严格比例)**
+3. **人物本体**:年龄/性别/身材量化(**8头身 / 160cm**)+ 姿态(A-pose)
+4. **造型细节**:发型/发色/发饰/服装/配饰/鞋 + **服饰分层描述**(如"交领长袍+暗纹云绣+玉带吊坠")
+5. **光影背景**:白底 + **平光无阴影** + **masterpiece, best quality, 8K超清**
+
+**标准模板(社区主流,可直接复制)**:
+```
+8K超清, masterpiece, best quality, 16:9横版, 纯白背景, 平光无阴影, character sheet, masterpiece, best quality,
+画面布局:左侧1/3为面部特写(发丝级细节),右侧2/3并排正面、侧面、背面全身站姿视图,
+角色描述:【性别/年龄/身材量化】,【五官/发型/发色+量化五官细节】,【服饰分层+配饰位置】,
+一致性要求:同一角色,**8头身 / 172cm**,身高/肩宽/体态统一,服饰褶皱/配饰位置跨视角完全一致,无崩
+
+负面提示词:nsfw, lowres, bad anatomy, text, error, missing fingers, blurry, distorted
+```
+
+**视图指令关键**:英文关键词 `character sheet, front/side/back view, full body` 强制布局
+
+**一致性 4 大技巧**:
+1. 重复"同一角色""完全一致"关键词
+2. **量化身材**(8头身/172cm)
+3. **服饰分层描述**(领+纹+饰 三个层次描述)
+4. 统一 seed 系列锁风格
+
+**避坑要点**:
+- 单图"四等分 panels"在 Z-Image 下不稳定(画 5-7 个),**严格 left 1/3 + right 2/3 比例**控制力最强
+- 避免"可爱"等模糊词,用"圆杏眼+齐刘海"客观细节
+- 平光照明减少阴影干扰(特写必备)
+- 3D 建模需加 "orthographic view"(正交视图)
+- cfg=1 下否定词部分失效,但元素词/比例词有效
+
+## 三、场景四视图(工业级方法论 · 2026-08-01 系统学习)
+**核心认知**:一张场景图只是"固定角度/光线/景别的截面",让模型脑补其他角度只能猜。
+→ 场景四视图需要**空间逻辑先行 + 多角度资产**,不能靠"单图+文字"。
+
+**四视角标准**(动画场景设计语义):①正面外观 ②背面外观 ③室内视角 ④鸟瞰俯视
+
+**工业级 5 招(社区共识)**:
+1. **俯视图锁定空间逻辑**(核心):先生成俯视/平面图定义布局→所有视角共享空间基准
+2. **九宫格多角度**:一张图 9 个角度
+3. **720° 全景图自由取景**:宽幅全景→切段(空间逻辑天然一致)
+4. **360° 环绕截图**
+5. **参考图控图**(最强):Seedance/即梦/ComfyUI IPAdapter+ControlNet 基于俯视基准图生成各视角
+
+**prompt 要点**:
+- 俯视图:`top-down orthographic floor plan, bird eye view, building layout, rooms, entrances, clean line`
+- 立面:`front/back exterior elevation view, architectural facade, straight-on`
+- 室内:`interior view, room perspective, walls floor ceiling furniture`
+- 一致性词:`consistent with the floor plan layout, same spatial logic`
+- 负面词:`characters, people, furniture clutter, inconsistent layout`
+
+**咱的落地**:Z-Image 无参考图机制→单图 4 视角会自由发挥;
+务实用"宽幅全景→切段"(空间逻辑一致)或进阶 ComfyUI SDXL+ControlNet-Union
+
+## 四、AI 漫剧五步工业化流水线(CSDN 权威版)
+```
+剧本生成 → 视觉素材制作(角色/场景资产) → 动态化生成(图生视频) → 音频合成(配音/BGM) → 后期精修
+```
+对照咱苍耳管线:✅ 全对齐(剧本→分镜→资产→出图→LTX/Wan→拼接→声画装配)
+
+## 五、ComfyUI 一致性实战
+- IPAdapter = "图像提示词":告诉 AI 照着参考图画(身份/风格保持)
+- 组合拳:ControlNet(构图) + IPAdapter(身份) + 固定 seed
+- 注意:IPAdapter 分 SDXL/SD1.5 版本,**必须匹配底模架构**(咱 Z-Image 是 Kolors 系,SDXL 版 IPAdapter 不兼容)
+
+## 六、分镜技巧(留存率骨架)
+- 分镜是"骨架",决定观众留存率
+- 七大要素:景别 / 运镜 / 视角 / 光影 / 构图 / 人物动作 / 环境动态
+- 提示词公式:镜头语言 + 主体 + 场景 + 光影 + 情绪
+
+## 七、咱的落地清单(已做/待做)
+- ✅ Z-Image 三视图直出(五模块结构化 prompt 已内置)
+- ✅ 场景四视图(2×2 网格:前/左/右/俯视)
+- ⏳ 面部特写+三视图布局(16:9 游戏立绘风)— 可加
+- ⏳ 资产库规范化(三视图→分镜统一引用)— 结合 skill 系统
diff --git a/skill/index.json b/skill/index.json
new file mode 100644
index 0000000..426064a
--- /dev/null
+++ b/skill/index.json
@@ -0,0 +1,126 @@
+{
+ "version": "1.0",
+ "skills": [
+ {
+ "id": "screenplay",
+ "name": "🎭 编剧大师",
+ "type": "prompt",
+ "desc": "剧本创作方法论:4格式+8步工作流+5概念组合+5结构",
+ "prompt_file": "../memory/eed/screenplay_skill.md",
+ "usage": "注入编剧大师方法论,用「输入创意→大师流程→成稿」方式创作剧本"
+ },
+ {
+ "id": "storyboard",
+ "name": "🎬 分镜生成",
+ "type": "tool",
+ "desc": "剧本→分镜故事板(含5色标注法)",
+ "prompt_file": "../memory/eed/storyboard_自动化.md",
+ "tool": "python3 ../video-ai-system/tools/run_storyboard.py",
+ "args_schema": {
+ "script": "剧本文本或文件路径"
+ }
+ },
+ {
+ "id": "style_transfer",
+ "name": "🎨 风格转绘",
+ "type": "prompt",
+ "desc": "2D⇄真人双向转绘提示词骨架+10种风格库",
+ "prompt_file": "../memory/eed/style_transfer_skill.md",
+ "usage": "注入风格转绘骨架,描述原图+目标风格→生成转绘提示词"
+ },
+ {
+ "id": "ltx_video",
+ "name": "🎥 LTX 出片",
+ "type": "tool",
+ "desc": "文本/图片→LTX视频(GGUF Q4本地推理)",
+ "prompt_file": "../memory/eed/ltx_prompt_skill.md",
+ "tool": "python3 ../video-ai-system/tools/run_ltx_i2v.py",
+ "args_schema": {
+ "prompt": "视频提示词",
+ "image": "起始图路径(可选)"
+ }
+ },
+ {
+ "id": "zimage",
+ "name": "🖼️ Z-Image 生图",
+ "type": "tool",
+ "desc": "文本→Z-Image高清图(本地ComfyUI)",
+ "prompt_file": null,
+ "tool": "python3 ../video-ai-system/tools/generate_assets_local.py",
+ "args_schema": {
+ "prompt": "生图提示词"
+ }
+ },
+ {
+ "id": "compose",
+ "name": "✂️ 拼接成片",
+ "type": "tool",
+ "desc": "多段视频拼接+成片(FFmpeg)",
+ "prompt_file": null,
+ "tool": "python3 ../video-ai-system/tools/video_composer.py",
+ "args_schema": {
+ "input": "视频目录",
+ "output": "输出路径"
+ }
+ },
+ {
+ "id": "character_turnaround",
+ "name": "👤 人物三视图/场景四视图",
+ "type": "tool",
+ "desc": "Z-Image:人物三视图(front/side/back)+特写 / 场景四视图(正面/背面/室内/鸟瞰)",
+ "prompt_file": null,
+ "tool": "python3 ../video-ai-system/tools/character_turnaround.py",
+ "args_schema": {
+ "type": "char(人物三视图)|scene(场景四视图)",
+ "desc": "角色/场景描述",
+ "views": "3或4",
+ "output": "输出路径(可选)"
+ }
+ },
+ {
+ "id": "audio_voice",
+ "name": "🎙 Edge-TTS 配音",
+ "type": "tool",
+ "desc": "分镜 dialogue → Edge-TTS 多角色配音(微软免费)",
+ "prompt_file": null,
+ "tool": "python3 ../video-ai-system/tools/audio_pipeline.py",
+ "args_schema": {
+ "action": "voice",
+ "sb": "分镜JSON",
+ "out": "输出目录"
+ }
+ },
+ {
+ "id": "audio_mix",
+ "name": "🎛 声画装配",
+ "type": "tool",
+ "desc": "配音+字幕+BGM+混音成片(Agent stage ⑥)",
+ "prompt_file": null,
+ "tool": "python3 ../video-ai-system/tools/audio_pipeline.py",
+ "args_schema": {
+ "action": "mix/srt/bgm",
+ "video": "成片",
+ "voice_dir": "配音目录",
+ "bgm": "BGM文件",
+ "out": "输出"
+ }
+ },
+ {
+ "id": "community_techniques",
+ "name": "📚 社区技巧库",
+ "type": "prompt",
+ "desc": "AI漫剧/角色一致性/三视图/场景四视图/分镜 社区实战技巧+角色版学习笔记",
+ "prompt_file": "community_techniques.md",
+ "prompt_file_alt": "community_techniques.md",
+ "usage": "注入社区技巧库,按需套用(角色锁定/三视图布局/场景四视图/分镜公式)"
+ },
+ {
+ "id": "character_sheet",
+ "name": "📋 人物角色设定板(开源技能)",
+ "type": "prompt",
+ "desc": "社区开源 character-sheet-generator:角色三视图设定板生成技能(7风格模板+变量体系+四区布局规范)",
+ "prompt_file": "character-sheet-generator/SKILL.md",
+ "usage": "注入开源角色设定板技能,选风格模板(base/ancient/realistic/anime/modern/sci-fi/fantasy),填变量生成标准角色设定板"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/skill/scene_4view_learn.md b/skill/scene_4view_learn.md
new file mode 100644
index 0000000..f30bc55
--- /dev/null
+++ b/skill/scene_4view_learn.md
@@ -0,0 +1,37 @@
+# 场景四视图 · 系统学习笔记(2026-08-01)
+
+> 来源:头条《一个提示词生成四视图》、B站《AI视频场景一致性完整工作流》、
+> 抖音《场景穿帮 5 招工业级解决方案》、gpt88 场景一致性专题、知乎 Day40
+
+## 一、核心认知(为什么我之前失败)
+**根本限制**(头条那篇点破):一张场景图只是"固定角度、固定光线、固定景别的截面"。
+让模型脑补"从侧面看、从空中看、从门口往里看",它没有那个信息,**只能猜**。
+→ 场景四视图不是"单图 + 文字描述"能解决的,需要**空间逻辑先行 + 多角度资产**。
+
+## 二、工业级 5 招(社区共识)
+1. **俯视图锁定空间逻辑**(核心):先生成俯视图/平面图,定义建筑布局、房间关系、出入口——所有视角共享的"空间基准"
+2. **九宫格多角度**:一张图内排 9 个角度的场景视图(前/后/左/右/45°×4 + 俯视)
+3. **720° 全景图自由取景**:生成宽幅全景 → 任意切段作为不同机位的背景
+4. **360° 环绕**:环绕视频/多帧截图 → 取不同帧作为视角
+5. **参考图控图**:用支持参考图的模型(Seedance/即梦/ComfyUI IPAdapter+ControlNet),基于"俯视基准图"生成各视角——**最强的做法**
+
+## 三、场景四视图 = 俯视图锁空间 + 各视角生成(标准流程)
+```
+第1步:俯视图/平面图(锁定空间逻辑、布局、出入口)→ 基准图
+第2步:基于基准图,逐视角生成:正面外观 / 背面外观 / 室内 / 鸟瞰
+第3步:参考图控图(基准图作参考)保证每个视角与空间逻辑一致
+```
+(对应爸爸之前定的"正面/背面/室内/鸟瞰"四视角 ✅)
+
+## 四、prompt 要点
+- **俯视图 prompt**:`top-down orthographic floor plan, bird eye view, showing building layout, rooms, entrances, furniture arrangement, clean line, blue print style`
+- **正面/背面**:`front/back exterior elevation view, architectural facade, straight-on view`
+- **室内**:`interior view, room perspective, showing walls floor ceiling and furniture`
+- **关键**:强调 "consistent with the floor plan layout, same spatial logic"
+- **负面词**:`characters, people, furniture clutter, inconsistent layout`
+
+## 五、咱的落地方向
+- **文生图(Z-Image)极限**:单图 4 视角(前/背/室内/鸟瞰)Z-Image 会自由发挥(无参考图机制)
+- **务实方案 A**:俯视图基准 → 逐视角生成(统一描述+相似seed,效果一般)
+- **务实方案 B(推荐)**:宽幅全景图 → PIL 切段(空间逻辑天然一致,工业级"全景自由取景")
+- **进阶方案 C**:ComfyUI SDXL + ControlNet-Union(用俯视基准图作控制条件)——彻底锁定
diff --git a/skill/skill_center.py b/skill/skill_center.py
new file mode 100644
index 0000000..6ca32a0
--- /dev/null
+++ b/skill/skill_center.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python3
+"""苍耳技能中心:技能注册表 + 提示词加载 + 工具执行
+用法:python3 skill_center.py [list|load
|run [args...]]"""
+import json, os, subprocess, sys
+
+SKILL_DIR = os.path.dirname(os.path.abspath(__file__))
+INDEX = os.path.join(SKILL_DIR, "index.json")
+
+
+def _index():
+ with open(INDEX, encoding="utf-8") as f:
+ return json.load(f)
+
+
+def list_skills():
+ return _index().get("skills", [])
+
+
+def load(skill_id):
+ """返回技能定义 + 提示词全文(prompt_file 存在时)。"""
+ for s in list_skills():
+ if s["id"] == skill_id:
+ data = dict(s)
+ pf = data.get("prompt_file")
+ if pf:
+ p = os.path.normpath(os.path.join(SKILL_DIR, pf))
+ try:
+ with open(p, encoding="utf-8") as f:
+ data["prompt"] = f.read()
+ except Exception as e:
+ data["prompt"] = ""
+ data["prompt_error"] = str(e)
+ return data
+ return None
+
+
+def run(skill_id, args=None):
+ """执行工具型技能,返回 (ok, output)。"""
+ s = load(skill_id)
+ if not s:
+ return False, "技能不存在: " + skill_id
+ if s.get("type") != "tool":
+ return False, f"技能「{s.get('name','')}」是提示词型,请注入提示词使用"
+ tool = s.get("tool")
+ if not tool:
+ return False, "技能无执行命令"
+ cmd = tool.split() + (args or [])
+ try:
+ r = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
+ return r.returncode == 0, (r.stdout or r.stderr)[-3000:]
+ except subprocess.TimeoutExpired:
+ return False, "执行超时(10分钟)"
+ except Exception as e:
+ return False, str(e)
+
+
+if __name__ == "__main__":
+ if len(sys.argv) < 2:
+ print(json.dumps(list_skills(), ensure_ascii=False, indent=2)); sys.exit(0)
+ cmd = sys.argv[1]
+ if cmd == "list":
+ print(json.dumps({"skills": list_skills()}, ensure_ascii=False, indent=2)); sys.exit(0)
+ elif cmd == "load":
+ s = load(sys.argv[2]) if len(sys.argv) > 2 else None
+ if s:
+ print(json.dumps({"name": s.get("name"), "type": s.get("type"),
+ "prompt_len": len(s.get("prompt", "")),
+ "tool": s.get("tool")}, ensure_ascii=False))
+ else:
+ print("技能不存在"); sys.exit(1)
+ elif cmd == "run":
+ ok, out = run(sys.argv[2], sys.argv[3:])
+ print(("OK\n" if ok else "FAIL\n") + out)
+ sys.exit(0 if ok else 1)
+ else:
+ print("未知命令: " + cmd); sys.exit(1)
diff --git a/video-ai-system/agent_short_drama.py b/video-ai-system/agent_short_drama.py
new file mode 100644
index 0000000..eaa5687
--- /dev/null
+++ b/video-ai-system/agent_short_drama.py
@@ -0,0 +1,329 @@
+#!/usr/bin/env python3
+"""
+agent_short_drama.py — 苍耳 · 一键短剧 Agent
+剧本 → 分镜(豆包) → 路由(豆包) → 逐镜出图(本地Z-Image)
+ → 逐镜视频(LTX I2V, 本地) → 拼接(FFmpeg) → 成片
+
+用法:
+ python agent_short_drama.py <剧本> -e 1 --until render # 只到出图
+ python agent_short_drama.py <剧本> -e 1 --until video # 到视频
+ python agent_short_drama.py <剧本> -e 1 --until compose # 到成片
+ python agent_short_drama.py <分镜.json> --from render # 从出图续跑
+ python agent_short_drama.py --from video # 从视频续跑
+"""
+import os, sys, json, time, subprocess, urllib.request, argparse, glob, shutil
+
+COMFY = "http://127.0.0.1:8188"
+VIDEO_AI = os.path.dirname(os.path.abspath(__file__))
+COMFY_OUT = os.path.expanduser("~/comfy/ComfyUI/output")
+COMFY_IN = os.path.expanduser("~/comfy/ComfyUI/input")
+
+LTX_NEG = ("static, frozen, no movement, flickering, jittery, choppy motion, morphing, "
+ "deformed, twisted, contorted, distorted, blurry, low resolution, extra limbs, "
+ "unnatural body, ugly, bad anatomy, disfigured, malformed, warped spine")
+
+# ---------- 工具 ----------
+def run_script(script, args):
+ cmd = [sys.executable, os.path.join(VIDEO_AI, script)] + args
+ print(" $", " ".join(cmd))
+ r = subprocess.run(cmd)
+ if r.returncode != 0:
+ raise RuntimeError(f"{script} 退出码 {r.returncode}")
+
+def comfy_submit(prompt):
+ req = urllib.request.Request(f"{COMFY}/prompt",
+ data=json.dumps({"prompt": prompt}).encode(),
+ headers={"Content-Type": "application/json"})
+ return json.load(urllib.request.urlopen(req, timeout=15))["prompt_id"]
+
+def comfy_wait(pid, timeout=900):
+ t0 = time.time()
+ while time.time() - t0 < timeout:
+ time.sleep(3)
+ try:
+ h = json.load(urllib.request.urlopen(f"{COMFY}/history/{pid}", timeout=8))
+ except Exception:
+ continue
+ if pid in h and h[pid].get("outputs"):
+ return h[pid]["outputs"]
+ raise TimeoutError(f"ComfyUI 任务 {pid} 超时")
+
+def wait_file(path, timeout=60):
+ t0 = time.time()
+ while time.time() - t0 < timeout:
+ if os.path.isfile(path) and os.path.getsize(path) > 0:
+ return True
+ time.sleep(2)
+ return False
+
+# ---------- Z-Image 出图 ----------
+def zimage_workflow(prompt, seed, prefix, width=1024, height=1024):
+ return {
+ "1": {"class_type": "UNETLoader", "inputs": {"unet_name": "z_image_turbo_bf16.safetensors", "weight_dtype": "default"}},
+ "2": {"class_type": "CLIPLoader", "inputs": {"clip_name": "qwen_3_4b.safetensors", "type": "lumina2", "device": "default"}},
+ "3": {"class_type": "VAELoader", "inputs": {"vae_name": "ae.safetensors"}},
+ "4": {"class_type": "CLIPTextEncode", "inputs": {"clip": ["2", 0], "text": prompt}},
+ "5": {"class_type": "ConditioningZeroOut", "inputs": {"conditioning": ["4", 0]}},
+ "6": {"class_type": "EmptySD3LatentImage", "inputs": {"width": width, "height": height, "batch_size": 1}},
+ "7": {"class_type": "ModelSamplingAuraFlow", "inputs": {"model": ["1", 0], "shift": 3.0}},
+ "8": {"class_type": "KSampler", "inputs": {"model": ["7", 0], "seed": seed, "steps": 8, "cfg": 1.0,
+ "sampler_name": "res_multistep", "scheduler": "simple",
+ "positive": ["4", 0], "negative": ["5", 0], "latent_image": ["6", 0], "denoise": 1.0}},
+ "9": {"class_type": "VAEDecode", "inputs": {"samples": ["8", 0], "vae": ["3", 0]}},
+ "10": {"class_type": "SaveImage", "inputs": {"images": ["9", 0], "filename_prefix": prefix}},
+ }
+
+# ---------- LTX I2V 出视频 ----------
+def ltx_i2v_workflow(image_name, prompt, seed, prefix, frames=49, width=768, height=512):
+ return {
+ "1": {"class_type": "UnetLoaderGGUF", "inputs": {"unet_name": "ltx-2.3-22b-distilled-1.1-Q4_K_M.gguf"}},
+ "2": {"class_type": "LTXAVTextEncoderLoader", "inputs": {"text_encoder": "gemma_3_12B_it.safetensors", "ckpt_name": "ltx-2.3-22b-embconn.safetensors", "device": "cpu"}},
+ "3": {"class_type": "CLIPTextEncode", "inputs": {"clip": ["2", 0], "text": prompt}},
+ "4": {"class_type": "CLIPTextEncode", "inputs": {"clip": ["2", 0], "text": LTX_NEG}},
+ "5": {"class_type": "LTXVConditioning", "inputs": {"positive": ["3", 0], "negative": ["4", 0], "frame_rate": 24.0}},
+ "6": {"class_type": "LoadImage", "inputs": {"image": image_name}},
+ "7": {"class_type": "VAELoader", "inputs": {"vae_name": "LTX23_video_vae_bf16.safetensors"}},
+ "8": {"class_type": "LTXVImgToVideo", "inputs": {"positive": ["5", 0], "negative": ["5", 1], "vae": ["7", 0], "image": ["6", 0], "width": width, "height": height, "length": frames, "batch_size": 1, "strength": 1.0}},
+ "9": {"class_type": "RandomNoise", "inputs": {"noise_seed": seed}},
+ "10": {"class_type": "KSamplerSelect", "inputs": {"sampler_name": "euler_ancestral_cfg_pp"}},
+ "11": {"class_type": "CFGGuider", "inputs": {"model": ["1", 0], "positive": ["8", 0], "negative": ["8", 1], "cfg": 1.0}},
+ "12": {"class_type": "ManualSigmas", "inputs": {"sigmas": "1.0, 0.99375, 0.9875, 0.98125, 0.975, 0.909375, 0.725, 0.421875, 0.0"}},
+ "13": {"class_type": "SamplerCustomAdvanced", "inputs": {"noise": ["9", 0], "guider": ["11", 0], "sampler": ["10", 0], "sigmas": ["12", 0], "latent_image": ["8", 2]}},
+ "14": {"class_type": "LTXVTiledVAEDecode", "inputs": {"vae": ["7", 0], "latents": ["13", 0], "horizontal_tiles": 2, "vertical_tiles": 2, "overlap": 6, "last_frame_fix": False, "working_device": "cpu", "working_dtype": "float16"}},
+ "15": {"class_type": "CreateVideo", "inputs": {"images": ["14", 0], "fps": 24.0}},
+ "16": {"class_type": "SaveVideo", "inputs": {"video": ["15", 0], "filename_prefix": prefix, "format": "mp4", "codec": "h264"}},
+ }
+
+def shot_to_prompt(shot, style=""):
+ parts = []
+ cam = shot.get('camera', '中景')
+ scenes = shot.get('scenes', [])
+ chars = shot.get('characters', [])
+ desc = shot.get('description', '')
+ if cam: parts.append(cam)
+ if scenes: parts.append("in " + scenes[0])
+ if chars: parts.append("with " + ", ".join(chars[:3]))
+ if desc: parts.append(desc[:120])
+ if style: parts.append(style)
+ return ", ".join(parts)
+
+def shot_to_ltx_prompt(desc, camera=""):
+ """分镜 → LTX I2V 运动提示词(四段式简化:起势→动作→环境→收尾)"""
+ base = (desc or "").strip()
+ action = base if base else "the subject moves naturally"
+ parts = [
+ f"Opening on the first frame scene, {camera or 'medium shot'}. The subject {action}, "
+ "with gentle flowing motion, grass and light drifting softly around. "
+ "Camera holds steady, shallow depth of field, warm natural light. "
+ "Slow, fluid, seamless motion, high detail. "
+ "Ends with the subject settling into a calm final pose."
+ ]
+ return " ".join(parts)
+
+# ---------- 各阶段 ----------
+def stage_storyboard(script, episode, pro=False):
+ args = [script, "-e", str(episode)]
+ if pro: args.append("--pro")
+ run_script("tools/run_storyboard.py", args)
+ d = os.path.dirname(os.path.abspath(script))
+ m = glob.glob(os.path.join(d, "STORYBOARD*.json"))
+ if not m:
+ raise RuntimeError("未找到分镜 JSON")
+ return m[0]
+
+def stage_route(sb_json):
+ run_script("tools/route_shots.py", [sb_json])
+ return sb_json
+
+def stage_render(sb_json, out_dir=None, style="", max_shots=0):
+ sb = json.load(open(sb_json, encoding="utf-8"))
+ shots = sb.get("shots", [])
+ if max_shots: shots = shots[:max_shots]
+ if not shots: raise RuntimeError("分镜无 shots")
+ if not out_dir:
+ out_dir = os.path.join(os.path.dirname(os.path.abspath(sb_json)), "renders")
+ os.makedirs(out_dir, exist_ok=True)
+ manifest = os.path.join(out_dir, "shot_manifest.json")
+ entries = []
+ for i, sh in enumerate(shots):
+ p = shot_to_prompt(sh, style)
+ seed = 1000 + i * 17
+ prefix = f"eed_shot_{i+1:04d}"
+ pid = comfy_submit(zimage_workflow(p, seed, prefix))
+ print(f" 🎬 [{i+1}/{len(shots)}] {sh.get('shot_number', f'S{i+1:02d}')}: {p[:50]}...")
+ comfy_wait(pid)
+ # 等文件落盘
+ img = os.path.join(COMFY_OUT, f"{prefix}_00001_.png")
+ wait_file(img)
+ entries.append({"shot": sh.get("shot_number", f"S{i+1:02d}"), "prompt": p, "ltx_desc": sh.get("description",""),
+ "camera": sh.get("camera",""), "seed": seed, "prefix": prefix})
+ with open(manifest, "w", encoding="utf-8") as f:
+ json.dump({"out_dir": out_dir, "shots": entries}, f, ensure_ascii=False, indent=2)
+ print(f" 📦 出图清单: {manifest}")
+ return manifest
+
+def stage_video(manifest, frames=49, max_shots=0):
+ data = json.load(open(manifest, encoding="utf-8"))
+ entries = data["shots"]
+ if max_shots: entries = entries[:max_shots]
+ vdir = os.path.join(data["out_dir"], "videos")
+ os.makedirs(vdir, exist_ok=True)
+ vlist = []
+ for i, e in enumerate(entries):
+ src = os.path.join(COMFY_OUT, f"{e['prefix']}_00001_.png")
+ if not os.path.isfile(src):
+ print(f" ⚠️ 缺起始图 {src},跳过 {e['shot']}"); continue
+ iname = f"agent_shot_{i+1:04d}.png"
+ shutil.copy(src, os.path.join(COMFY_IN, iname))
+ prompt = shot_to_ltx_prompt(e.get("ltx_desc", ""), e.get("camera", ""))
+ seed = 5000 + i * 29
+ prefix = f"eed_vid_{i+1:04d}"
+ pid = comfy_submit(ltx_i2v_workflow(iname, prompt, seed, prefix, frames=frames))
+ print(f" 🎥 [{i+1}/{len(entries)}] {e['shot']} LTX I2V {frames}帧 提交,预计 {frames*4}s...")
+ comfy_wait(pid, timeout=900)
+ vout = os.path.join(COMFY_OUT, f"{prefix}_00001_.mp4")
+ if not wait_file(vout, timeout=60):
+ print(f" ⚠️ 视频未落盘 {vout}")
+ continue
+ dest = os.path.join(vdir, f"shot_{i+1:04d}.mp4")
+ shutil.copy(vout, dest)
+ vlist.append({"shot": e["shot"], "file": dest, "prompt": prompt})
+ print(f" ✅ {e['shot']} → {dest}")
+ vman = os.path.join(vdir, "video_manifest.json")
+ with open(vman, "w", encoding="utf-8") as f:
+ json.dump({"video_dir": vdir, "videos": vlist}, f, ensure_ascii=False, indent=2)
+ print(f" 📦 视频清单: {vman}")
+ return vman
+
+def stage_compose(vman_or_dir, output="", title="", credits=""):
+ if os.path.isfile(vman_or_dir) and vman_or_dir.endswith("video_manifest.json"):
+ d = json.load(open(vman_or_dir, encoding="utf-8"))
+ folder = d["video_dir"]
+ else:
+ folder = vman_or_dir
+ vids = sorted(glob.glob(os.path.join(folder, "shot_*.mp4")))
+ if not vids:
+ raise RuntimeError(f"没有可拼接的视频片段: {folder}")
+ if not output:
+ output = os.path.join(os.path.dirname(folder), "EP01.mp4")
+ if len(vids) == 1 and not title and not credits:
+ # 单段且无片头片尾:直接拷贝(LTX 视频无声,video_composer 方案B 会因缺音频流失败)
+ shutil.copy(vids[0], output)
+ print(f" 🎬 单段成片(直接拷贝): {output}")
+ return output
+ args = [folder, "-o", output]
+ if title: args += ["--title", title]
+ if credits: args += ["--credits", credits]
+ run_script("tools/video_composer.py", args)
+ print(f" 🎬 成片: {output}")
+ return output
+
+def _video_duration(path):
+ try:
+ r = subprocess.run(["ffprobe", "-v", "quiet", "-show_entries", "format=duration",
+ "-of", "csv=p=0", path], capture_output=True, text=True)
+ return float(r.stdout.strip() or 0)
+ except Exception:
+ return 0
+
+def stage_audio_pipeline(sb_json, composed, ep_dir):
+ """⑥ 声画装配:配音 → 字幕 → BGM → 混音 → 有声终成片"""
+ from tools import audio_pipeline as ap
+ print(" 🎙 配音 (Edge-TTS)...")
+ voice = ap.generate_voice(sb_json, ep_dir)
+ print(" 📝 字幕...")
+ ap.generate_srt(sb_json, ep_dir, voice)
+ dur = _video_duration(composed) or 20
+ print(f" 🎵 BGM ({dur:.0f}s)...")
+ ap.generate_bgm(ep_dir, int(dur) + 2)
+ final = os.path.join(ep_dir, "FINAL.mp4")
+ print(" 🎛 混音...")
+ ap.mix_final(composed, os.path.join(ep_dir, "voice"),
+ os.path.join(ep_dir, "bgm.mp3"), final)
+ print(f" 🎉 有声终成片: {final}")
+ return final
+
+def main():
+ ap = argparse.ArgumentParser(description="苍耳 · 一键短剧 Agent")
+ ap.add_argument("input", help="剧本 / 分镜JSON(--from render,route) / 清单(--from video,compose)")
+ ap.add_argument("-e", "--episode", type=int, default=1)
+ ap.add_argument("--pro", action="store_true", help="分镜用Pro模型")
+ ap.add_argument("--style", default="", help="全局风格")
+ ap.add_argument("--until", choices=["storyboard","route","render","video","compose","audio"], default="render")
+ ap.add_argument("--from", dest="from_stage", choices=["route","render","video","compose","audio"])
+ ap.add_argument("--max-shots", type=int, default=0, help="只跑前 N 镜")
+ ap.add_argument("--frames", type=int, default=49, help="LTX 视频帧数(默认49)")
+ ap.add_argument("-o", "--output", default="", help="成片路径")
+ args = ap.parse_args()
+
+ sb_json = None
+ if args.from_stage in ("video", "compose"):
+ manifest = args.input
+ print(f"📖 从清单续跑: {manifest}")
+ if args.from_stage == "video":
+ vman = stage_video(manifest, frames=args.frames, max_shots=args.max_shots)
+ if args.from_stage == "compose":
+ out = stage_compose(manifest, args.output)
+ print("🎯 成片完成:", out); return
+ if args.from_stage == "audio":
+ out = stage_compose(manifest, args.output)
+ sb = glob.glob(os.path.join(os.path.dirname(os.path.dirname(out)), "STORYBOARD*.json"))
+ if sb:
+ ep_dir = os.path.dirname(os.path.abspath(out))
+ final = stage_audio_pipeline(sb[0], out, ep_dir)
+ print("🎯 有声终成片:", final)
+ return
+ print("🎯 阶段完成。续跑: --from compose")
+ return
+
+ if args.from_stage:
+ sb_json = args.input
+ if not os.path.isfile(sb_json):
+ print(f"❌ 找不到分镜: {sb_json}"); sys.exit(1)
+ print(f"📖 从分镜续跑: {sb_json}")
+ else:
+ if not os.path.isfile(args.input):
+ print(f"❌ 找不到剧本: {args.input}"); sys.exit(1)
+ print(f"🚀 苍耳 · 一键短剧 Agent | 剧本: {args.input} | EP{args.episode:02d} | 到: {args.until}")
+ print("═" * 44); print("① 剧本→分镜 (豆包, ¥0.01/集 级)"); print("═" * 44)
+ sb_json = stage_storyboard(args.input, args.episode, args.pro)
+ print(f"✅ 分镜: {sb_json}")
+
+ if not sb_json: sys.exit(1)
+
+ if args.from_stage in (None, "storyboard") and args.until not in ("storyboard",):
+ print("═" * 44); print("② 逐镜路由分配 (豆包)"); print("═" * 44)
+ sb_json = stage_route(sb_json)
+ print("✅ 路由完成")
+
+ if args.until in ("render", "video", "compose"):
+ print("═" * 44); print("③ 逐镜出图 (本地 Z-Image, 零成本)"); print("═" * 44)
+ manifest = stage_render(sb_json, style=args.style, max_shots=args.max_shots)
+ else:
+ print("🎯 阶段完成。"); return
+
+ if args.until in ("video", "compose"):
+ print("═" * 44); print("④ 逐镜视频 (本地 LTX I2V, 零成本)"); print("═" * 44)
+ vman = stage_video(manifest, frames=args.frames, max_shots=args.max_shots)
+ else:
+ print("🎯 出图完成。续跑: --from video <清单> 或 --until video")
+ return
+
+ if args.until in ("compose", "audio"):
+ print("═" * 44); print("⑤ 剪辑拼接 (FFmpeg)"); print("═" * 44)
+ out = stage_compose(vman, args.output)
+ print(f"\n🎬 成片: {out}")
+ if args.until == "audio":
+ print("═" * 44); print("⑥ 声画装配 (配音+字幕+BGM+混音)"); print("═" * 44)
+ sb = sb_json or (glob.glob(os.path.join(os.path.dirname(os.path.dirname(out)), "STORYBOARD*.json")) or [None])[0]
+ if sb:
+ ep_dir = os.path.dirname(os.path.abspath(out))
+ final = stage_audio_pipeline(sb, out, ep_dir)
+ print(f"\n🎉🎉 有声终成片: {final}")
+ else:
+ print("⚠️ 未找到分镜JSON,跳过声画装配")
+ elif args.until != "compose":
+ print("🎯 视频完成。续跑: --from compose <清单> 或 --until compose/audio")
+
+if __name__ == "__main__":
+ main()
diff --git a/video-ai-system/tools/audio_pipeline.py b/video-ai-system/tools/audio_pipeline.py
new file mode 100644
index 0000000..5ff2877
--- /dev/null
+++ b/video-ai-system/tools/audio_pipeline.py
@@ -0,0 +1,243 @@
+#!/usr/bin/env python3
+"""
+audio_pipeline.py — 声画流水线:配音 / 字幕 / BGM / 混音
+剧本有声成片的最后一段:把分镜 dialogue 变成配音+字幕+BGM,混入成片。
+
+用法:
+ python audio_pipeline.py voice <分镜.json> <输出目录> # Edge-TTS 配音
+ python audio_pipeline.py srt <分镜.json> <输出目录> # 生成 SRT 字幕
+ python audio_pipeline.py bgm <输出目录> <秒数> # 生成 BGM(stable_audio 失败则 ffmpeg 氛围音)
+ python audio_pipeline.py mix <成片.mp4> <配音目录> <输出.mp4> # 混音
+"""
+import os, sys, json, asyncio, subprocess, math
+
+VOICES = {
+ "男": "zh-CN-YunxiNeural",
+ "女": "zh-CN-XiaoxiaoNeural",
+ "旁白": "zh-CN-XiaoyiNeural",
+ "少年": "zh-CN-YunyangNeural",
+ "老人": "zh-CN-YunfengNeural",
+}
+
+
+def load_shots(sb_json):
+ d = json.load(open(sb_json, encoding="utf-8"))
+ shots = d.get("shots", [])
+ return shots
+
+
+def _dialogue_of(shot):
+ """从分镜镜头提取台词(支持字符串或 dict)。"""
+ dl = shot.get("dialogue", "")
+ if not dl:
+ return []
+ if isinstance(dl, str):
+ return [{"text": dl, "char": shot.get("characters", [""])[0] if shot.get("characters") else "旁白"}]
+ if isinstance(dl, list):
+ out = []
+ for it in dl:
+ if isinstance(it, str):
+ out.append({"text": it, "char": shot.get("characters", [""])[0] if shot.get("characters") else "旁白"})
+ elif isinstance(it, dict):
+ out.append({"text": it.get("text", ""), "char": it.get("character", it.get("char", "旁白"))})
+ return [x for x in out if x["text"].strip()]
+ return []
+
+
+def generate_voice(sb_json, out_dir):
+ """Edge-TTS 逐条配音。返回 [(shot_no, 文件路径, 台词, 时长)]"""
+ shots = load_shots(sb_json)
+ vdir = os.path.join(out_dir, "voice")
+ os.makedirs(vdir, exist_ok=True)
+ results = []
+ for i, sh in enumerate(shots):
+ lines = _dialogue_of(sh)
+ if not lines:
+ continue
+ shot_no = sh.get("shot_number", f"S{i+1:02d}")
+ for j, ln in enumerate(lines):
+ voice = VOICES.get(ln["char"], VOICES.get(ln["char"][:1], "zh-CN-XiaoxiaoNeural"))
+ fname = f"shot_{i+1:04d}_{j:02d}.mp3"
+ fpath = os.path.join(vdir, fname)
+ if not os.path.isfile(fpath):
+ text = ln["text"][:500]
+ try:
+ asyncio.run(_tts(text, voice, fpath))
+ except Exception as e:
+ print(f" ⚠️ 配音失败 {shot_no}:{ln['char']}: {e}")
+ continue
+ dur = _mp3_duration(fpath)
+ results.append({"shot": shot_no, "char": ln["char"], "text": ln["text"],
+ "file": fpath, "duration": dur})
+ print(f" 🎙 [{shot_no}] {ln['char']}: {ln['text'][:40]} ({dur:.1f}s)")
+ return results
+
+
+async def _tts(text, voice, fpath):
+ import edge_tts
+ c = edge_tts.Communicate(text, voice)
+ await c.save(fpath)
+
+
+def _mp3_duration(path):
+ try:
+ r = subprocess.run(["ffprobe", "-v", "quiet", "-show_entries", "format=duration",
+ "-of", "csv=p=0", path], capture_output=True, text=True)
+ return float(r.stdout.strip() or 0)
+ except Exception:
+ return 0
+
+
+def generate_srt(sb_json, out_dir, voice_results=None):
+ """生成 SRT 字幕(基于配音时长分配时间轴)。"""
+ shots = load_shots(sb_json)
+ sdir = os.path.join(out_dir, "subs")
+ os.makedirs(sdir, exist_ok=True)
+ srt_path = os.path.join(sdir, "subtitles.srt")
+ idx = 1
+ t = 0.0
+ lines = []
+ for i, sh in enumerate(shots):
+ lines_d = _dialogue_of(sh)
+ if not lines_d:
+ continue
+ shot_no = sh.get("shot_number", f"S{i+1:02d}")
+ for ln in lines_d:
+ dur = 2.5
+ if voice_results:
+ for vr in voice_results:
+ if vr["shot"] == shot_no and vr["char"] == ln["char"]:
+ dur = max(1.0, vr["duration"] + 0.3)
+ break
+ st = t
+ t += dur
+ lines.append(f"{idx}\n{_fmt_srt(st)} --> {_fmt_srt(t)}\n{ln['char']}: {ln['text']}\n")
+ idx += 1
+ with open(srt_path, "w", encoding="utf-8") as f:
+ f.write("\n".join(lines))
+ print(f" 📝 字幕: {srt_path} ({idx-1} 条)")
+ return srt_path
+
+
+def _fmt_srt(sec):
+ h = int(sec // 3600); m = int((sec % 3600) // 60); s = int(sec % 60); ms = int((sec - int(sec)) * 1000)
+ return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}"
+
+
+def generate_bgm(out_dir, seconds=20, mood="calm ambient background music, gentle piano", use_stable=False):
+ """生成 BGM:默认 ffmpeg 氛围音(稳定);use_stable=True 时先试 stable_audio_3。"""
+ bgm_path = os.path.join(out_dir, "bgm.mp3")
+ ok = False
+ if use_stable:
+ ok = _try_stable_audio(bgm_path, seconds, mood)
+ if ok:
+ print(f" 🎵 BGM(stable_audio_3): {bgm_path} ({seconds}s)")
+ if not ok:
+ _ffmpeg_pad(bgm_path, seconds)
+ print(f" 🎵 BGM(ffmpeg氛围音): {bgm_path} ({seconds}s)")
+ return bgm_path
+
+
+def _try_stable_audio(path, seconds, mood):
+ """调本地 ComfyUI stable_audio_3 生成。返回 bool。"""
+ try:
+ import json, urllib.request, time
+ COMFY = "http://127.0.0.1:8188"
+ sigmas = ", ".join(str(round(1.0 - i / 24, 4)) for i in range(25)) + ", 0.0"
+ wf = {
+ "1": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": "stable_audio_3_medium_base.safetensors"}},
+ "2": {"class_type": "CLIPTextEncode", "inputs": {"text": mood, "clip": ["1", 1]}},
+ "3": {"class_type": "CLIPTextEncode", "inputs": {"text": "vocals, singing, noisy, harsh, distorted, speech", "clip": ["1", 1]}},
+ "4": {"class_type": "ConditioningStableAudio", "inputs": {"positive": ["2", 0], "negative": ["3", 0], "seconds_start": 0, "seconds_total": seconds}},
+ "5": {"class_type": "EmptyLatentAudio", "inputs": {"seconds": seconds, "batch_size": 1}},
+ "6": {"class_type": "KSamplerSelect", "inputs": {"sampler_name": "euler"}},
+ "9": {"class_type": "ManualSigmas", "inputs": {"sigmas": sigmas}},
+ "10": {"class_type": "SamplerCustom", "inputs": {"model": ["1", 0], "positive": ["4", 0], "negative": ["3", 0], "cfg": 4.0, "noise_seed": 42, "add_noise": True, "sampler": ["6", 0], "sigmas": ["9", 0], "latent_image": ["5", 0]}},
+ "11": {"class_type": "VAEDecodeAudio", "inputs": {"samples": ["10", 0], "vae": ["1", 2]}},
+ "12": {"class_type": "SaveAudio", "inputs": {"audio": ["11", 0], "filename_prefix": "eed_bgm_gen"}},
+ }
+ data = json.dumps({"prompt": wf}).encode()
+ req = urllib.request.Request(f"{COMFY}/prompt", data=data, headers={"Content-Type": "application/json"})
+ pid = json.loads(urllib.request.urlopen(req, timeout=15).read())["prompt_id"]
+ for _ in range(15): # 30秒内不成功即放弃,避免卡住
+ time.sleep(2)
+ h = json.loads(urllib.request.urlopen(f"{COMFY}/history/{pid}", timeout=8).read())
+ if pid in h and h[pid].get("outputs"):
+ aud = h[pid]["outputs"].get("12", {}).get("audio", [])
+ if aud:
+ import shutil
+ shutil.copy(os.path.join(os.path.expanduser("~/comfy/ComfyUI/output"), aud[0]["filename"]), path)
+ return True
+ if pid in h and h[pid].get("status", {}).get("status_str") == "error":
+ return False
+ except Exception:
+ return False
+ return False
+
+
+def _ffmpeg_pad(path, seconds):
+ """ffmpeg 生成柔和氛围音垫底(aevalsrc 单输入多频和声,避免多 lavfi 输入偶发失败)。"""
+ expr = (f"0.04*sin(2*PI*220*t)+0.04*sin(2*PI*277*t)+0.04*sin(2*PI*330*t)"
+ f"+0.03*sin(2*PI*165*t)")
+ cmd = ["ffmpeg", "-y", "-f", "lavfi",
+ "-i", f"aevalsrc={expr}:d={seconds}:s=44100",
+ "-af", "lowpass=f=1500,volume=0.35", path]
+ try:
+ r = subprocess.run(cmd, capture_output=True, timeout=60)
+ if r.returncode != 0:
+ print(f" ⚠️ BGM ffmpeg 退出码 {r.returncode}: {(r.stderr or '')[-200:]}")
+ except Exception as e:
+ print(f" ⚠️ BGM ffmpeg 异常: {e}")
+
+
+def mix_final(video, voice_dir, bgm, out_path, subtitles=None):
+ """ffmpeg 混音:成片(无声) + 配音 + BGM(压低)。"""
+ v = ["-i", video]
+ inputs = [video]
+ filters = []
+ n = 0
+ vf_in = {}
+ if voice_dir and os.path.isdir(voice_dir):
+ mps = sorted([os.path.join(voice_dir, f) for f in os.listdir(voice_dir) if f.endswith(".mp3")])
+ for mp in mps:
+ inputs.append(mp); vf_in[f"v{n+1}"] = mp
+ n += 1
+ if bgm and os.path.isfile(bgm):
+ inputs.append(bgm); vf_in[f"v{n+1}"] = bgm
+ n += 1
+ if not inputs[1:]:
+ # 无任何音轨:直接复制视频
+ subprocess.run(["ffmpeg", "-y", "-i", video, "-c", "copy", out_path], capture_output=True)
+ return out_path
+ # 简单混音:所有输入 amix,BGM 音量压低
+ labels = [f"[{i}:a]" for i in range(1, len(inputs))]
+ vol = []
+ for i in range(1, len(inputs)):
+ if inputs[i] == bgm:
+ vol.append(f"[{i}:a]volume=0.15[v{i}]")
+ else:
+ vol.append(f"[{i}:a][v{i}]" if False else f"[{i}:a]volume=1.0[v{i}]")
+ fc = ";".join(vol) + ";" + "".join(f"[v{i}]" for i in range(1, len(inputs))) + f"amix=inputs={n}:normalize=0[aout]"
+ cmd = ["ffmpeg", "-y"] + sum([["-i", i] for i in inputs], []) + \
+ ["-filter_complex", fc, "-map", "0:v", "-map", "[aout]", "-c:v", "copy", "-c:a", "aac", out_path]
+ try:
+ subprocess.run(cmd, capture_output=True, timeout=120)
+ except Exception as e:
+ print(" 混音失败:", e)
+ return out_path
+
+
+if __name__ == "__main__":
+ if len(sys.argv) < 2:
+ print(__doc__); sys.exit(0)
+ cmd = sys.argv[1]
+ if cmd == "voice" and len(sys.argv) >= 4:
+ generate_voice(sys.argv[2], sys.argv[3])
+ elif cmd == "srt" and len(sys.argv) >= 4:
+ generate_srt(sys.argv[2], sys.argv[3])
+ elif cmd == "bgm" and len(sys.argv) >= 4:
+ generate_bgm(sys.argv[2], int(float(sys.argv[3])), sys.argv[4] if len(sys.argv) > 4 else "calm ambient background music, gentle piano")
+ elif cmd == "mix" and len(sys.argv) >= 5:
+ mix_final(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5])
+ else:
+ print(__doc__)
diff --git a/video-ai-system/tools/character_turnaround.py b/video-ai-system/tools/character_turnaround.py
new file mode 100644
index 0000000..d8be4a6
--- /dev/null
+++ b/video-ai-system/tools/character_turnaround.py
@@ -0,0 +1,258 @@
+#!/usr/bin/env python3
+"""
+character_turnaround.py — Z-Image 角色三视图/四视图生成
+漫剧角色资产:同角色 正面/侧面/背面(或 3/4 视角)展板,单图一张出。
+
+用法:
+ python character_turnaround.py --desc "长黑发红发箍白裙少女" --output char.png
+ python character_turnaround.py --desc "...(详细角色描述)" --views 4 --seed 888
+ # --desc 支持中英文;建议给足细节:发型/发色/发饰/服装/配饰/鞋/体型
+"""
+import os, sys, json, urllib.request, time, argparse, uuid
+
+COMFY = "http://127.0.0.1:8188"
+COMFY_OUT = os.path.expanduser("~/comfy/ComfyUI/output")
+
+VIEWS_3 = ("front view on the left, 90 degree side profile view in the middle, back view on the right")
+# 人物四视图标准:正面 / 背面 / 侧面 / 脸部特写(动画·游戏角色资产规范)
+VIEWS_4 = ("character sheet, 16:9 horizontal composition, pure white background, flat lighting no shadow, masterpiece, best quality, "
+ "left 1/3 of the image is a face extreme close-up portrait with hair-level facial detail looking at viewer, "
+ "right 2/3 of the image shows three full-body views arranged in a horizontal row: front view, 90 degree side profile view, back view, "
+ "the same character in every panel, identical face hairstyle and outfit across all views, "
+ "full body standing pose for the three right-side views")
+# 社区布局:面部特写+三视图(16:9 游戏立绘风)
+PORTRAIT = ("character design sheet, horizontal layout divided into four panels: "
+ "front view, back view, 90 degree side profile view, and a large close-up portrait of the face, "
+ "the SAME character in all four panels, identical face hairstyle and outfit, "
+ "plain white background, anime style, clean lineart, high quality, detailed")
+# 3D展示台布局(爸爸发的开源格式):上方三视图 + 下方细节特写镜头组
+SHOWCASE = ("3D model display, three views of the character (front view, side view, back view), "
+ "clean neutral background, below the three main views are close-up detail shots showing "
+ "fabric, clothing details, face and accessories, "
+ "detail shots of face, collar, fabric texture, accessories, "
+ "modern style, 3D render, high quality, masterpiece")
+# 场景四视图(动画场景设计标准):同一空间的 4 种视角,**纯场景无人物**
+# ①正面外观 ②背面外观 ③室内视角 ④鸟瞰俯视 —— 强制 empty scene no people
+SCENE_VIEWS = [
+ ("front", "front exterior view from street level showing the main facade and entrance, empty scene, no characters, no people"),
+ ("back", "back exterior view from behind showing the rear side of the building, empty scene, no characters, no people"),
+ ("interior", "interior view inside the empty room showing indoor layout, tables chairs lanterns and architectural details, no characters, no people"),
+ ("aerial", "top-down orthographic bird eye view of the whole building layout with rooms courtyard trees stone path, floor plan style, no characters, no people"),
+]
+
+
+def submit_wide(desc, seed, prefix, width=2048, height=1024):
+ """Z-Image 宽幅全景图(用于场景切段法)。"""
+ prompt = (f"wide panoramic establishing shot, continuous sweeping vista of: {desc}, "
+ f"single unbroken scene from left to right, consistent architecture and scenery throughout, "
+ f"clean background, anime style, game environment design, high quality, detailed")
+ wf = {
+ "1": {"class_type": "UNETLoader", "inputs": {"unet_name": "z_image_turbo_bf16.safetensors", "weight_dtype": "default"}},
+ "2": {"class_type": "CLIPLoader", "inputs": {"clip_name": "qwen_3_4b.safetensors", "type": "lumina2", "device": "default"}},
+ "3": {"class_type": "VAELoader", "inputs": {"vae_name": "ae.safetensors"}},
+ "4": {"class_type": "CLIPTextEncode", "inputs": {"clip": ["2", 0], "text": prompt}},
+ "5": {"class_type": "CLIPTextEncode", "inputs": {"clip": ["2", 0], "text": NEG_PROMPT}},
+ "6": {"class_type": "EmptySD3LatentImage", "inputs": {"width": width, "height": height, "batch_size": 1}},
+ "7": {"class_type": "ModelSamplingAuraFlow", "inputs": {"model": ["1", 0], "shift": 3.0}},
+ "8": {"class_type": "KSampler", "inputs": {"model": ["7", 0], "seed": seed, "steps": 8, "cfg": 1.0,
+ "sampler_name": "res_multistep", "scheduler": "simple",
+ "positive": ["4", 0], "negative": ["5", 0], "latent_image": ["6", 0], "denoise": 1.0}},
+ "9": {"class_type": "VAEDecode", "inputs": {"samples": ["8", 0], "vae": ["3", 0]}},
+ "10": {"class_type": "SaveImage", "inputs": {"images": ["9", 0], "filename_prefix": prefix}},
+ }
+ data = json.dumps({"prompt": wf}).encode()
+ req = urllib.request.Request(f"{COMFY}/prompt", data=data, headers={"Content-Type": "application/json"})
+ pid = json.loads(urllib.request.urlopen(req, timeout=15).read())["prompt_id"]
+ for _ in range(120):
+ time.sleep(2)
+ h = json.loads(urllib.request.urlopen(f"{COMFY}/history/{pid}", timeout=8).read())
+ if pid in h and h[pid].get("outputs"):
+ return h[pid]["outputs"]["10"]["images"][0]["filename"]
+ if pid in h and h[pid].get("status", {}).get("status_str") == "error":
+ for m in h[pid]["status"].get("messages", []):
+ if isinstance(m, list) and len(m) > 1 and m[0] == "execution_error":
+ raise RuntimeError(m[1].get("exception_message", "").strip()[:300])
+ raise TimeoutError("全景生成超时")
+
+
+def scene_panorama_views(desc, seed, output_dir, out_name="scene_4view"):
+ """全景图切四段法:宽幅全景 → 切 4 段 → 拼成四视图。
+ 四段天然来自同一张图,一致性 100% 锁定。"""
+ from PIL import Image
+ import shutil
+ prefix = "eed_scene_pan_" + time.strftime("%H%M%S")
+ f = submit_wide(desc, seed, prefix, width=2048, height=1024)
+ src = os.path.join(COMFY_OUT, f)
+ os.makedirs(output_dir, exist_ok=True)
+ im = Image.open(src)
+ w, h = im.size
+ seg_w = w // 4
+ views = []
+ for i in range(4):
+ seg = im.crop((i * seg_w, 0, (i + 1) * seg_w, h))
+ seg_path = os.path.join(output_dir, f"{out_name}_view{i+1}.png")
+ seg.save(seg_path)
+ views.append(seg_path)
+ # 拼四视图展示版
+ gap = 16
+ canvas = Image.new("RGB", (seg_w * 4 + gap * 3, h), "white")
+ for i, v in enumerate(views):
+ canvas.paste(Image.open(v), (i * (seg_w + gap), 0))
+ final = os.path.join(output_dir, f"{out_name}_combo.png")
+ canvas.save(final)
+ return final, views
+
+
+def char_four_views_compose(desc, seed, output_dir, out_name="char_4view"):
+ """人物四视图(逐张生成 + PIL 拼图,**精确 4 张**):
+ ①正面 ②背面 ③侧面 ④脸部特写。
+ 逐张提交 Z-Image 标准出图,角色一致性靠相似 seed 锁定。"""
+ import shutil
+ from PIL import Image
+ os.makedirs(output_dir, exist_ok=True)
+ # 每张强制 ONLY ONE single character(避免 Z-Image 自由发挥画多个),简化元素避免歧义
+ cmds = [
+ ("front", f"character design, ONLY ONE single character, {desc}, front view looking at viewer, full body, A-pose, isolated on plain white background, no other figures, anime style, high quality"),
+ ("back", f"character design, ONLY ONE single character, {desc}, back view facing away, full body, A-pose, isolated on plain white background, no other figures, anime style, high quality"),
+ ("side", f"character design, ONLY ONE single character, {desc}, 90 degree side profile view facing left, full body, A-pose, isolated on plain white background, no other figures, anime style, high quality"),
+ ("face", f"character design, ONLY ONE single character, {desc}, face close-up portrait, head and shoulders only, looking at viewer, isolated on plain white background, no other figures, anime style, high quality"),
+ ]
+ paths = []
+ for i, (key, prompt) in enumerate(cmds):
+ f = submit(prompt, 1, seed + i * 13, f"eed_char_{key}_{time.strftime('%H%M%S')}")
+ p = os.path.join(output_dir, f"{out_name}_{key}.png")
+ shutil.copy(os.path.join(COMFY_OUT, f), p)
+ paths.append(p)
+ print(f" ✅ {key}: {p}")
+ # 2x2 拼图
+ ims = [Image.open(p) for p in paths]
+ w, h = ims[0].size
+ gap = 12
+ canvas = Image.new("RGB", (w * 2 + gap, h * 2 + gap), "white")
+ canvas.paste(ims[0], (0, 0))
+ canvas.paste(ims[1], (w + gap, 0))
+ canvas.paste(ims[2], (0, h + gap))
+ canvas.paste(ims[3], (w + gap, h + gap))
+ combo = os.path.join(output_dir, f"{out_name}_combo.png")
+ canvas.save(combo)
+ return combo, paths
+
+
+def scene_four_views(desc, seed, output_dir, out_name="scene_4view"):
+ """场景四视图(专业语义):同一场景的 ①正面外观 ②背面外观 ③室内 ④鸟瞰。
+ 统一场景描述串 + 相近 seed 逐张生成,保证是"同一个空间"。"""
+ import shutil
+ from PIL import Image
+ os.makedirs(output_dir, exist_ok=True)
+ views = []
+ base_seed = seed
+ for i, (key, view_word) in enumerate(SCENE_VIEWS):
+ prompt = f"{desc}, {view_word}, empty scene no characters no people, the same building and location as the other views, consistent architecture details, anime style, game environment concept art, clean style, high quality, detailed"
+ print(f" 🎨 视角{i+1}/4 [{key}]: {view_word[:40]}...")
+ f = submit(prompt, 1, base_seed + i * 7, f"eed_scene_{key}_{time.strftime('%H%M%S')}")
+ v = os.path.join(output_dir, f"{out_name}_{key}.png")
+ shutil.copy(os.path.join(COMFY_OUT, f), v)
+ views.append(v)
+ print(f" ✅ {v}")
+ # 拼 2x2 四视图展示版
+ ims = [Image.open(v) for v in views]
+ w, h = ims[0].size
+ gap = 12
+ canvas = Image.new("RGB", (w * 2 + gap, h * 2 + gap), "white")
+ pos = [(0, 0), (w + gap, 0), (0, h + gap), (w + gap, h + gap)]
+ for im, (x, y) in zip(ims, pos):
+ canvas.paste(im, (x, y))
+ combo = os.path.join(output_dir, f"{out_name}_combo.png")
+ canvas.save(combo)
+ return combo, views
+
+
+NEG_PROMPT = "nsfw, lowres, bad anatomy, text, error, missing fingers, blurry, distorted, watermark, multiple characters, characters, people, persons, human figures, crowd"
+
+def submit(desc, views, seed, prefix, ctype="char", layout="standard"):
+ if ctype == "scene":
+ prompt = f"environment concept art, {desc}, anime style, high quality"
+ elif layout == "showcase":
+ prompt = f"{SHOWCASE}. Character and outfit: {desc}"
+ elif layout == "portrait":
+ prompt = f"{PORTRAIT}. Character: {desc}"
+ else:
+ view_part = VIEWS_4 if views >= 4 else VIEWS_3
+ prompt = (f"character reference sheet, model sheet, {views} views of the SAME character: "
+ f"{view_part}, identical character design across all views, same face hairstyle outfit, "
+ f"plain white background, anime style, clean lineart, "
+ f"character design sheet, high quality, detailed. Character: {desc}")
+ wf = {
+ "1": {"class_type": "UNETLoader", "inputs": {"unet_name": "z_image_turbo_bf16.safetensors", "weight_dtype": "default"}},
+ "2": {"class_type": "CLIPLoader", "inputs": {"clip_name": "qwen_3_4b.safetensors", "type": "lumina2", "device": "default"}},
+ "3": {"class_type": "VAELoader", "inputs": {"vae_name": "ae.safetensors"}},
+ "4": {"class_type": "CLIPTextEncode", "inputs": {"clip": ["2", 0], "text": prompt}},
+ "5": {"class_type": "CLIPTextEncode", "inputs": {"clip": ["2", 0], "text": NEG_PROMPT}}, # 真正的负面词
+ "6": {"class_type": "EmptySD3LatentImage", "inputs": {"width": 1024, "height": 1024, "batch_size": 1}},
+ "7": {"class_type": "ModelSamplingAuraFlow", "inputs": {"model": ["1", 0], "shift": 3.0}},
+ "8": {"class_type": "KSampler", "inputs": {"model": ["7", 0], "seed": seed, "steps": 8, "cfg": 1.0,
+ "sampler_name": "res_multistep", "scheduler": "simple",
+ "positive": ["4", 0], "negative": ["5", 0], "latent_image": ["6", 0], "denoise": 1.0}},
+ "9": {"class_type": "VAEDecode", "inputs": {"samples": ["8", 0], "vae": ["3", 0]}},
+ "10": {"class_type": "SaveImage", "inputs": {"images": ["9", 0], "filename_prefix": prefix}},
+ }
+ data = json.dumps({"prompt": wf}).encode()
+ req = urllib.request.Request(f"{COMFY}/prompt", data=data, headers={"Content-Type": "application/json"})
+ pid = json.loads(urllib.request.urlopen(req, timeout=15).read())["prompt_id"]
+ for _ in range(90):
+ time.sleep(2)
+ h = json.loads(urllib.request.urlopen(f"{COMFY}/history/{pid}", timeout=8).read())
+ if pid in h and h[pid].get("outputs"):
+ return h[pid]["outputs"]["10"]["images"][0]["filename"]
+ if pid in h and h[pid].get("status", {}).get("status_str") == "error":
+ for m in h[pid]["status"].get("messages", []):
+ if isinstance(m, list) and len(m) > 1 and m[0] == "execution_error":
+ raise RuntimeError(m[1].get("exception_message", "").strip()[:300])
+ raise TimeoutError("Z-Image 生成超时")
+
+
+def main():
+ ap = argparse.ArgumentParser(description="Z-Image 角色三视图/四视图生成")
+ ap.add_argument("--desc", required=True, help="角色描述(细节越足越好)")
+ ap.add_argument("--views", type=int, default=3, choices=[3, 4], help="视图数:3或4(仅 char 用)")
+ ap.add_argument("--type", dest="ctype", default="char", choices=["char", "scene"], help="char=人物三视图 / scene=场景四视图")
+ ap.add_argument("--layout", default="standard", choices=["standard", "portrait", "showcase"], help="char布局:standard=标准三栏 / portrait=面部特写+三视图 / showcase=3D展示台(上三视图+下细节特写)")
+ ap.add_argument("--method", default="prompt", choices=["prompt", "panorama", "compose"], help="场景: prompt/panorama; 人物四视图: compose=逐张生成+PIL拼图(精确4张)")
+ ap.add_argument("--seed", type=int, default=777)
+ ap.add_argument("--output", default="", help="输出路径(默认 cang-ying/outputs/character_