diff --git a/scripts/eed_web.py b/scripts/eed_web.py index 4bfb179..24418c2 100644 --- a/scripts/eed_web.py +++ b/scripts/eed_web.py @@ -41,6 +41,54 @@ EED_SYS = ( "涉及花钱/调API等现实操作,按 EED-PROTO-005 走 申请→爸爸验证码→固定动作→回执,自己不执行。" ) +# ---- 停止机制:杀掉正在运行的 codebuddy 子进程(治"停止后还在后台等") ---- +ACTIVE = {"proc": None, "lock": threading.Lock()} + + +def _kill_proc(proc): + if proc is None or proc.poll() is not None: + return + try: + os.killpg(os.getpgid(proc.pid), signal.SIGTERM) + except Exception: + try: + proc.terminate() + except Exception: + pass + try: + proc.wait(timeout=3) + except subprocess.TimeoutExpired: + try: + os.killpg(os.getpgid(proc.pid), signal.SIGKILL) + except Exception: + try: + proc.kill() + except Exception: + pass + try: + proc.wait(timeout=5) + except Exception: + pass + + +def _register(proc): + with ACTIVE["lock"]: + ACTIVE["proc"] = proc + + +def _unregister(proc): + with ACTIVE["lock"]: + if ACTIVE["proc"] is proc: + ACTIVE["proc"] = None + + +def _kill_active(): + with ACTIVE["lock"]: + p = ACTIVE["proc"] + ACTIVE["proc"] = None + _kill_proc(p) + + PAGE = r""" @@ -132,7 +180,8 @@ PAGE = r""" background:var(--bg);color:var(--txt);padding:13px;font-size:15px;font-family:inherit;outline:none;} #msg:focus{border-color:var(--me);} #send,#stop{border:none;border-radius:12px;background:var(--me);color:#fff;padding:0 20px;font-size:15px;cursor:pointer;height:52px;} - #stop{background:#e0594f;display:none;} + #stop{background:#e0594f;opacity:.45;transition:opacity .15s;color:#fff;} + #stop.live{opacity:1;box-shadow:0 0 0 2px rgba(224,89,79,.45);} #tobottom{position:absolute;right:24px;bottom:84px;background:var(--panel);border:1px solid var(--line); color:var(--me);border-radius:20px;padding:6px 14px;font-size:12px;cursor:pointer;display:none;z-index:20;box-shadow:0 4px 14px rgba(0,0,0,.25);} /* markdown */ @@ -234,6 +283,8 @@ PAGE = r""" + + @@ -266,6 +317,35 @@ PAGE = r"""

对话仅存本机浏览器,关掉再开还在。

+ + + +

💰 积分余额

打开 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,'$1'); + 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 - 人物角色设定板生成技能 + +
    + +![Character Sheet](https://img.shields.io/badge/Character%20Sheet-Generator-blueviolet) +![Version](https://img.shields.io/badge/version-1.0.0-green) +![Style](https://img.shields.io/badge/styles-6%2B-orange) +![Models](https://img.shields.io/badge/models-compatible-red) + +**专业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_