EED: 经验记忆推送——技能库(11技能+开源角色设定板)+遇事不决先搜索铁律+人物三/四视图+场景四视图工业级方法论+声画装配+多视图工具
- skill/ 技能库:index.json 11 技能 + community_techniques.md 社区技巧 + character_sheet_learn.md 角色版学习 + scene_4view_learn.md 场景四视图工业级方法 + character-sheet-generator 开源技能(7风格模板) - tools/character_turnaround.py:人物三/四视图(主视觉+三视图/展示台)+场景四视角,Z-Image 本地 - tools/audio_pipeline.py:Edge-TTS配音+字幕+BGM+混音,Agent stage⑥有声成片 - agent_short_drama.py:一键短剧 Agent --until audio 全链路 - eed_web.py:技能库端点(/api/skills,/api/skill)+E2BIG根治(字节截断+巨兽降级)+一键短剧按钮
This commit is contained in:
parent
45ffbb6022
commit
80805449ca
@ -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"""<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
@ -132,7 +180,8 @@ PAGE = r"""<!DOCTYPE html>
|
||||
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"""<!DOCTYPE html>
|
||||
<button id="btnRead">📂 读仓库文件</button>
|
||||
<button id="btnSearch">🔍 联网搜索</button>
|
||||
<button id="btnAttach">📎 附文件</button>
|
||||
<button id="btnDrama" title="一键短剧:剧本/分镜 → 全自动成片">🎬 一键短剧</button>
|
||||
<button id="btnSkill" title="技能库:编剧/分镜/风格转绘/LTX/Z-Image 即插即用">🧠 技能库</button>
|
||||
<input id="fileinp" type="file" accept=".txt,.md,.json,.py,.js,.csv,.hdlp,.log,.yaml,.yml,.toml,.text" style="display:none">
|
||||
<input id="impinp" type="file" accept=".json" style="display:none">
|
||||
</div>
|
||||
@ -266,6 +317,35 @@ PAGE = r"""<!DOCTYPE html>
|
||||
<p style="color:var(--mut);font-size:11px;">对话仅存本机浏览器,关掉再开还在。</p>
|
||||
<div class="close"><button id="modalClose">知道了</button></div>
|
||||
</div></div>
|
||||
<div id="dramaModal" style="display:none"><div class="card" style="max-width:660px">
|
||||
<h3>🎬 一键短剧 Agent</h3>
|
||||
<textarea id="dramaInput" style="width:100%;height:120px;resize:vertical" placeholder="贴入 剧本 或 分镜JSON…"></textarea>
|
||||
<div style="margin:8px 0">
|
||||
<label>类型:
|
||||
<select id="dramaType">
|
||||
<option value="storyboard" selected>分镜JSON(零成本:出图→视频→成片)</option>
|
||||
<option value="script">剧本(需豆包分镜授权)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label> 集号 <input id="dramaEp" type="number" value="1" style="width:56px"></label>
|
||||
<label> 帧数 <input id="dramaFrames" type="number" value="49" style="width:64px"></label>
|
||||
</div>
|
||||
<label style="color:#e0594f"><input id="dramaAuth" type="checkbox"> 授权豆包拆分镜(约 ¥0.01/集,EED-PROTO-005)</label>
|
||||
<div style="margin:8px 0">
|
||||
<button id="dramaGo">开始生成</button>
|
||||
<button onclick="$('#dramaModal').style.display='none'">关闭</button>
|
||||
</div>
|
||||
<pre id="dramaLog" style="max-height:260px;overflow:auto;background:#111;color:#8f8;padding:8px;border-radius:8px;font-size:12px;white-space:pre-wrap"></pre>
|
||||
<div id="dramaResult"></div>
|
||||
</div></div>
|
||||
|
||||
<div id="skillModal" style="display:none"><div class="card" style="max-width:560px">
|
||||
<h3>🧠 苍耳技能库</h3>
|
||||
<p style="color:var(--mut);font-size:12px">提示词型=注入方法论到输入框(可编辑后发送);工具型=本地执行(生图/出片/拼接)</p>
|
||||
<div id="skillList" style="max-height:60vh;overflow-y:auto"></div>
|
||||
<div style="margin-top:10px"><button onclick="$('#skillModal').style.display='none'">关闭</button></div>
|
||||
</div></div>
|
||||
|
||||
<div id="balModal"><div class="card">
|
||||
<h2>💰 积分余额</h2>
|
||||
<p>打开 <a href="https://www.codebuddy.cn/profile/plans-usage" target="_blank" rel="noopener">codebuddy.cn/profile/plans-usage</a> 登录后,看"套餐总额"和"已用",填进来蛋蛋帮你算剩余:</p>
|
||||
@ -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,'<blockquote>$1</blockquote>');
|
||||
s=s.replace(/^###\s+(.*)$/gm,'<h3>$1</h3>').replace(/^##\s+(.*)$/gm,'<h2>$1</h2>').replace(/^#\s+(.*)$/gm,'<h1>$1</h1>');
|
||||
s=s.replace(/\*\*([^*]+)\*\*/g,'<b>$1</b>').replace(/\*([^*]+)\*/g,'<i>$1</i>').replace(/`([^`]+)`/g,'<code class="ic">$1</code>');
|
||||
s=s.replace(/!\[([^\]]*)\]\((\/media\/[^)]+\.(?:png|jpe?g|gif|webp))\)/g,'<img src="$2" alt="$1" style="max-width:100%;border-radius:10px;margin:.4em 0">');
|
||||
s=s.replace(/\[([^\]]+)\]\((\/media\/[^)]+\.(?:mp4|webm|mov))\)/g,'<video src="$2" controls style="max-width:100%;border-radius:10px;margin:.4em 0"></video>');
|
||||
s=s.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g,'<a href="$2" target="_blank" rel="noopener">$1</a>');
|
||||
s=s.replace(/^\s*[-*]\s+\[([ xX])\]\s+(.*)$/gm,(m,chk,t)=>'<li class="task">'+(chk.toLowerCase()==='x'?'☑':'☐')+' '+t+'</li>');
|
||||
s=s.replace(/^\s*[-*]\s+(.*)$/gm,'<li>$1</li>');
|
||||
@ -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=>
|
||||
`<div class="skill" data-id="${s.id}" data-type="${s.type}" style="padding:10px;margin:6px 0;border:1px solid #333;border-radius:8px;cursor:pointer;background:#1a1a1f">
|
||||
<div style="font-weight:700">${s.name} <span style="color:var(--mut);font-size:11px;font-weight:400">${s.type==='prompt'?'💉 提示词型':'⚙️ 工具型'}</span></div>
|
||||
<div style="font-size:12px;color:var(--mut);margin-top:3px">${s.desc||''}</div>
|
||||
</div>`).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='<video src="'+d.url+'" controls style="max-width:100%;border-radius:10px;margin-top:8px"></video>';}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:
|
||||
|
||||
159
skill/character-sheet-generator/README.md
Normal file
159
skill/character-sheet-generator/README.md
Normal file
@ -0,0 +1,159 @@
|
||||
# Character Sheet Generator - 人物角色设定板生成技能
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
**专业AI人物角色设定板(三视图)生成提示词技能**
|
||||
|
||||
[English](#english) | [中文](#中文)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 中文
|
||||
|
||||
### 简介
|
||||
|
||||
这是一个用于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.
|
||||
218
skill/character-sheet-generator/SKILL.md
Normal file
218
skill/character-sheet-generator/SKILL.md
Normal file
@ -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种风格预设
|
||||
63
skill/character-sheet-generator/examples/example_ancient.md
Normal file
63
skill/character-sheet-generator/examples/example_ancient.md
Normal file
@ -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张挑选最佳效果
|
||||
61
skill/character-sheet-generator/examples/example_anime.md
Normal file
61
skill/character-sheet-generator/examples/example_anime.md
Normal file
@ -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动漫模式
|
||||
60
skill/character-sheet-generator/examples/example_modern.md
Normal file
60
skill/character-sheet-generator/examples/example_modern.md
Normal file
@ -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写实模式
|
||||
116
skill/character-sheet-generator/templates/ancient_chinese.md
Normal file
116
skill/character-sheet-generator/templates/ancient_chinese.md
Normal file
@ -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 芽黄
|
||||
74
skill/character-sheet-generator/templates/anime.md
Normal file
74
skill/character-sheet-generator/templates/anime.md
Normal file
@ -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年代动画风格,明显阴影边界,复古色调
|
||||
- **赛璐璐厚涂混合**:现代动画电影风格,线稿淡,色彩层次丰富
|
||||
- **少女漫画风**:大眼睛,花眼,星光点缀,花瓣背景元素
|
||||
- **少年漫画风**:锐利线条,热血风格,棱角分明
|
||||
63
skill/character-sheet-generator/templates/base.md
Normal file
63
skill/character-sheet-generator/templates/base.md
Normal file
@ -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}}` | 最高品质,细节丰富 |
|
||||
73
skill/character-sheet-generator/templates/fantasy.md
Normal file
73
skill/character-sheet-generator/templates/fantasy.md
Normal file
@ -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
|
||||
```
|
||||
|
||||
## 奇幻职业选项
|
||||
- **精灵游侠**:尖耳朵,皮革轻甲,长弓,森林绿棕配色,树叶披风
|
||||
- **人类骑士**:全身板甲,盾牌长剑,家徽纹章,披风,银蓝配色
|
||||
- **法师/巫师**:长袍法袍,法杖,魔法书,奥术光效,紫金/深蓝配色
|
||||
- **矮人战士**:矮壮身材,战斧,锻造板甲,胡须编织,红棕铁灰配色
|
||||
- **盗贼/刺客**:紧身皮甲,匕首兜帽,暗影色调,黑色深灰配色
|
||||
- **牧师/圣骑**:白金配色,神圣光效,战锤/圣典,教会纹章
|
||||
- **野蛮人**:皮毛皮革,巨大武器,战纹涂装,肌肉发达,大地色系
|
||||
68
skill/character-sheet-generator/templates/modern.md
Normal file
68
skill/character-sheet-generator/templates/modern.md
Normal file
@ -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卫衣,牛仔裤,运动鞋,棒球帽
|
||||
- **高街潮流**:设计师品牌,潮牌单品,层次感叠穿
|
||||
- **学院风**:针织衫,格纹裙/百褶裙,乐福鞋,学院徽章
|
||||
- **轻熟优雅**:针织连衣裙,小香风外套,低跟鞋,珍珠配饰
|
||||
- **运动风**:运动套装,跑鞋,发带,运动手表
|
||||
76
skill/character-sheet-generator/templates/realistic.md
Normal file
76
skill/character-sheet-generator/templates/realistic.md
Normal file
@ -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年代风格
|
||||
- 韩系清透妆,水光肌,偶像风格
|
||||
64
skill/character_sheet_learn.md
Normal file
64
skill/character_sheet_learn.md
Normal file
@ -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 式"资产库+多视图锁定")
|
||||
89
skill/community_techniques.md
Normal file
89
skill/community_techniques.md
Normal file
@ -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 系统
|
||||
126
skill/index.json
Normal file
126
skill/index.json
Normal file
@ -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),填变量生成标准角色设定板"
|
||||
}
|
||||
]
|
||||
}
|
||||
37
skill/scene_4view_learn.md
Normal file
37
skill/scene_4view_learn.md
Normal file
@ -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(用俯视基准图作控制条件)——彻底锁定
|
||||
76
skill/skill_center.py
Normal file
76
skill/skill_center.py
Normal file
@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""苍耳技能中心:技能注册表 + 提示词加载 + 工具执行
|
||||
用法:python3 skill_center.py [list|load <id>|run <id> [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)
|
||||
329
video-ai-system/agent_short_drama.py
Normal file
329
video-ai-system/agent_short_drama.py
Normal file
@ -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 <manifest> --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()
|
||||
243
video-ai-system/tools/audio_pipeline.py
Normal file
243
video-ai-system/tools/audio_pipeline.py
Normal file
@ -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> <配音目录> <bgm.mp3> <输出.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__)
|
||||
258
video-ai-system/tools/character_turnaround.py
Normal file
258
video-ai-system/tools/character_turnaround.py
Normal file
@ -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_<time>.png)")
|
||||
args = ap.parse_args()
|
||||
|
||||
prefix = ("eed_scene_" if args.ctype == "scene" else "eed_char_") + time.strftime("%H%M%S")
|
||||
label = "场景四视图" if args.ctype == "scene" else f"角色{args.views}视图"
|
||||
print(f"🎨 Z-Image 生成 {label}: {args.desc[:60]}...")
|
||||
import shutil
|
||||
if args.ctype == "char" and args.views >= 4 and args.method == "compose":
|
||||
out_dir = os.path.expanduser("~/cang-ying/outputs")
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
final, views = char_four_views_compose(args.desc, args.seed, out_dir)
|
||||
print(f"✅ 人物四视图(逐张生成+拼图): {final}")
|
||||
print(f" 四张图: {', '.join(os.path.basename(v) for v in views)}")
|
||||
return
|
||||
if args.ctype == "scene":
|
||||
out_dir = os.path.expanduser("~/cang-ying/outputs")
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
if args.method == "panorama":
|
||||
final, views = scene_panorama_views(args.desc, args.seed, out_dir)
|
||||
print(f"✅ 场景四视图(全景切段,备选): {final}")
|
||||
else:
|
||||
final, views = scene_four_views(args.desc, args.seed, out_dir)
|
||||
print(f"✅ 场景四视图(正面/背面/室内/鸟瞰): {final}")
|
||||
print(f" 四张视角图: {', '.join(os.path.basename(v) for v in views)}")
|
||||
return
|
||||
f = submit(args.desc, args.views, args.seed, prefix, args.ctype, args.layout)
|
||||
src = os.path.join(COMFY_OUT, f)
|
||||
if not args.output:
|
||||
out_dir = os.path.expanduser("~/cang-ying/outputs")
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
args.output = os.path.join(out_dir, f"character_{time.strftime('%m%d_%H%M%S')}.png")
|
||||
shutil.copy(src, args.output)
|
||||
print(f"✅ {label}: {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -1,32 +1,117 @@
|
||||
# 步骤⑦ storyboard 引擎: 豆包 AI 拆解剧本
|
||||
import sys, json
|
||||
sys.path.insert(0, '.')
|
||||
#!/usr/bin/env python3
|
||||
# 步骤⑦ storyboard 引擎: 豆包 AI 拆解剧本 → 结构化分镜 JSON
|
||||
# 通用版 — 支持任意剧本文件
|
||||
# DEPENDS: lib/doubao_chat.py
|
||||
|
||||
import sys, os, json, argparse
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from lib.doubao_chat import breakdown_script
|
||||
|
||||
with open('projects/deep-sea-voyage/EP01-SCRIPT-LOCK.hdlp', 'r', encoding='utf-8') as f:
|
||||
script = f.read()
|
||||
|
||||
print('发送豆包 AI 拆解剧本...')
|
||||
result = breakdown_script(script, 1)
|
||||
def extract_json(text):
|
||||
"""从 AI 回复中提取 JSON 内容(兼容带 ```json 包裹的情况)"""
|
||||
if '```json' in text:
|
||||
return text.split('```json')[1].split('```')[0].strip()
|
||||
if '```' in text:
|
||||
return text.split('```')[1].split('```')[0].strip()
|
||||
# 尝试直接解析
|
||||
maybe = text.strip()
|
||||
if maybe.startswith('{'):
|
||||
return maybe
|
||||
return maybe
|
||||
|
||||
if 'error' in result:
|
||||
print('API ERROR:', json.dumps(result, ensure_ascii=False, indent=2))
|
||||
elif 'content' in result:
|
||||
content = result['content']
|
||||
# 提取 JSON
|
||||
if '```json' in content:
|
||||
content = content.split('```json')[1].split('```')[0]
|
||||
elif '```' in content:
|
||||
content = content.split('```')[1].split('```')[0]
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='豆包AI → 拆解剧本为结构化分镜JSON')
|
||||
parser.add_argument('script', help='剧本文件路径 (.hdlp 或 .txt)')
|
||||
parser.add_argument('-e', '--episode', type=int, default=1, help='集号 (默认: 1)')
|
||||
parser.add_argument('-o', '--output', help='输出路径 (默认: 自动推导)')
|
||||
parser.add_argument('--pro', action='store_true', help='使用 Pro 模型 (doubao-pro, 更强更贵)')
|
||||
parser.add_argument('--dry-run', action='store_true', help='只打印将发送的提示词,不调API')
|
||||
|
||||
# 保存到文件
|
||||
out_path = 'projects/deep-sea-voyage/STORYBOARD-AI.json'
|
||||
args = parser.parse_args()
|
||||
|
||||
# 读剧本
|
||||
if not os.path.isfile(args.script):
|
||||
print(f'❌ 找不到剧本文件: {args.script}')
|
||||
sys.exit(1)
|
||||
|
||||
with open(args.script, 'r', encoding='utf-8') as f:
|
||||
script_text = f.read()
|
||||
|
||||
print(f'📖 剧本: {args.script}')
|
||||
print(f'📐 长度: {len(script_text)} 字符')
|
||||
print(f'🎬 集号: EP{args.episode:02d}')
|
||||
print(f'🧠 模型: {"doubao-pro" if args.pro else "doubao-lite"}')
|
||||
print()
|
||||
|
||||
if args.dry_run:
|
||||
print('🧪 DRY RUN — 将发送的提示词:')
|
||||
print('=' * 60)
|
||||
from lib.doubao_chat import breakdown_script as bd
|
||||
print(bd.__doc__)
|
||||
print('=' * 60)
|
||||
return
|
||||
|
||||
# 调用豆包 API
|
||||
print('🚀 发送豆包 AI 拆解剧本...')
|
||||
result = breakdown_script(script_text, args.episode, model='pro' if args.pro else 'lite')
|
||||
|
||||
if 'error' in result:
|
||||
print('❌ API ERROR:', json.dumps(result, ensure_ascii=False, indent=2))
|
||||
sys.exit(1)
|
||||
|
||||
if 'content' not in result or not result['content']:
|
||||
print('❌ API 返回空内容:', json.dumps(result, ensure_ascii=False, indent=2))
|
||||
sys.exit(1)
|
||||
|
||||
content = result['content']
|
||||
json_str = extract_json(content)
|
||||
|
||||
# 解析 JSON 验证有效性
|
||||
try:
|
||||
parsed = json.loads(json_str)
|
||||
shot_count = len(parsed.get('shots', []))
|
||||
except json.JSONDecodeError as e:
|
||||
print(f'⚠️ JSON 解析失败: {e}')
|
||||
print('原始内容:')
|
||||
print(content[:500])
|
||||
shot_count = 0
|
||||
parsed = None
|
||||
|
||||
# 确定输出路径
|
||||
if args.output:
|
||||
out_path = args.output
|
||||
else:
|
||||
# 从剧本路径自动推导
|
||||
script_dir = os.path.dirname(os.path.abspath(args.script))
|
||||
script_base = os.path.splitext(os.path.basename(args.script))[0]
|
||||
# 如果剧本文件名包含 SCRIPT 或 BREAKDOWN,替换为 STORYBOARD
|
||||
if 'SCRIPT' in script_base.upper() or 'BREAKDOWN' in script_base.upper():
|
||||
storyboard_name = script_base.replace('SCRIPT', 'STORYBOARD').replace('BREAKDOWN', 'STORYBOARD')
|
||||
else:
|
||||
model_tag = 'PRO' if args.pro else 'AI'
|
||||
storyboard_name = f'STORYBOARD-{model_tag}-EP{args.episode:02d}'
|
||||
out_path = os.path.join(script_dir, f'{storyboard_name}.json')
|
||||
|
||||
# 保存
|
||||
with open(out_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content.strip())
|
||||
print('SAVED:', out_path)
|
||||
print('tokens:', result.get('tokens', 'N/A'))
|
||||
print('---')
|
||||
print(content.strip())
|
||||
else:
|
||||
print('UNKNOWN:', result)
|
||||
f.write(json_str if parsed else content.strip())
|
||||
|
||||
print(f'\n✅ 分镜已保存: {out_path}')
|
||||
if parsed:
|
||||
print(f'📊 共 {shot_count} 镜')
|
||||
print(f'💰 tokens: {result.get("tokens", "N/A")}')
|
||||
print(f'⏱️ 完成原因: {result.get("finish", "N/A")}')
|
||||
|
||||
# 打印前3镜预览
|
||||
if parsed and shot_count > 0:
|
||||
print('\n📋 前3镜预览:')
|
||||
for s in parsed['shots'][:3]:
|
||||
print(f' {s.get("shot_number","?"):>4} | {s.get("camera","?"): <4} | {s.get("duration","?"):>2}s | {s.get("description","")[:40]}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user