82 lines
8.4 KiB
Python
82 lines
8.4 KiB
Python
"""Reproduce the existing storyboard request in a private DB copy; never change live projects or submit media."""
|
|
import contextlib,json,os,pathlib,re,shutil,sqlite3,subprocess,time,urllib.request,urllib.error
|
|
ROOT=pathlib.Path(__file__).resolve().parent.parent
|
|
OUT=ROOT/'integration-work'/('storyboard-coverage-verification-'+time.strftime('%Y%m%d-%H%M%S')+'-'+os.getenv('STORYBOARD_DIAG_PROVIDER','deepseek'));OUT.mkdir()
|
|
(ROOT/'integration-work/current-storyboard-diagnostic.txt').write_text(str(OUT),'utf-8')
|
|
shutil.copytree(ROOT/'integration-work/stage-4.0.11/app/resources/workspace-template',OUT/'workspace')
|
|
dbfile=OUT/'huobao.sqlite3'
|
|
with contextlib.closing(sqlite3.connect((ROOT/'huobao-data/huobao.sqlite3').as_uri()+'?mode=ro',uri=True)) as live,contextlib.closing(sqlite3.connect(dbfile)) as db:live.backup(db)
|
|
with sqlite3.connect(dbfile) as db:
|
|
db.row_factory=sqlite3.Row
|
|
db.execute('delete from sys_task');db.execute('delete from video_merges');db.execute("delete from app_settings where key like 'storyboard_job:%'")
|
|
assert dbfile.resolve().is_relative_to(OUT.resolve()) and dbfile.resolve()!= (ROOT/'huobao-data/huobao.sqlite3').resolve()
|
|
for (table,) in db.execute("select name from sqlite_master where type='table' and name not in ('sqlite_sequence','ai_service_configs','style_presets')").fetchall():
|
|
db.execute('delete from "'+table.replace('"','""')+'"')
|
|
ts='2026-11-13T00:00:00Z'
|
|
script=(ROOT/'integration-work/storyboard-coverage-fixture.txt').read_text('utf-8')
|
|
db.execute('insert into dramas(id,title,created_at,updated_at) values(7,?,?,?)',('Museum repair fixture',ts,ts))
|
|
db.execute('insert into episodes(id,drama_id,episode_number,title,content,script_content,created_at,updated_at) values(8,7,1,?,?,?,?,?)',('Fixture',script,script,ts,ts))
|
|
for ident,name in [(52,'林师傅'),(53,'小周')]:db.execute('insert into characters(id,drama_id,name,appearance,created_at,updated_at) values(?,7,?,?,?,?)',(ident,name,'成年博物馆工作人员,穿工作服',ts,ts))
|
|
for ident,name in [(33,'修复工作室'),(34,'博物馆屋顶'),(35,'验收大厅')]:db.execute('insert into scenes(id,drama_id,location,time,prompt,created_at,updated_at) values(?,7,?,?,?,?,?)',(ident,name,'白天','安静整洁的工作空间,自然光',ts,ts))
|
|
ep=dict(db.execute('select * from episodes where id=8').fetchone())
|
|
cfg=dict(db.execute("select * from ai_service_configs where service_type='text' and is_active=1 order by priority desc limit 1").fetchone())
|
|
if os.getenv('STORYBOARD_DIAG_PROVIDER'):
|
|
selector=os.environ['STORYBOARD_DIAG_PROVIDER']
|
|
cfg=dict(db.execute("select * from ai_service_configs where service_type='text' and is_active=1 and (provider=? or model like ?) order by priority desc limit 1",(selector,'%'+selector+'%')).fetchone())
|
|
model=json.loads(cfg['model'])[0]
|
|
chars=[dict(r) for r in db.execute('select id,name from characters where drama_id=? and deleted_at is null',(ep['drama_id'],))]
|
|
scenes=[dict(r) for r in db.execute('select id,location from scenes where drama_id=? and deleted_at is null',(ep['drama_id'],))]
|
|
body={'drama_id':ep['drama_id'],'episode_id':8,'config_id':cfg['id'],'model':model,'message':'请基于当前集剧本拆分完整分镜,并为每个分镜段落同时生成 video_prompt(中文视频提示词)和 video_prompt_en(英文制作提示词,中文对白及 @/# 素材引用原样保留)。本次视频模型:MiniMax-H3。当前集已有角色:'+json.dumps(chars,ensure_ascii=False)+'。当前集已有场景:'+json.dumps(scenes,ensure_ascii=False)+'。必须从已有角色和场景中选择正确 ID 绑定,按顺序分批保存全部分镜。'}
|
|
source=(ROOT/'huobao-source/desktop/build/backend.mjs').read_text('utf-8')
|
|
stage=ROOT/'integration-work/stage-4.0.11/app'
|
|
source=source.replace('const require = __hbCreateRequire(import.meta.url)','const require = __hbCreateRequire('+json.dumps(str(stage/'resources/app.asar/build/backend.mjs'))+')',1)
|
|
source=re.sub(r'^import (\w+) from "(better-sqlite3|sharp)";',r'const \1 = require("\2");',source,flags=re.M)
|
|
instrument=r'''
|
|
const __diagFetch=globalThis.fetch; let __diagN=0;
|
|
globalThis.fetch=async(input,init)=>{
|
|
const url=new URL(typeof input==='string'?input:input.url||String(input));
|
|
if(url.hostname==='127.0.0.1')return __diagFetch(input,init);
|
|
const n=++__diagN,t=Date.now();let b={};try{b=JSON.parse(init?.body||'{}')}catch{}
|
|
for (const m of b.messages||[]) if(m.tool_calls) for(const t of m.tool_calls) if(t.function?.name==='saveStoryboards') console.log('DRAFT '+JSON.stringify({n,id:t.id,args:t.function.arguments}));
|
|
console.log('DIAGNOSTIC '+JSON.stringify({event:'request',n,model:b.model,stream:b.stream,max_tokens:b.max_tokens,thinking:b.thinking,messages:b.messages?.length,input_chars:JSON.stringify(b.messages||[]).length,tools:b.tools?.map(x=>x.function.name),recent_tools:b.messages?.filter(x=>x.role==='tool').slice(-2).map(x=>({id:x.tool_call_id,content:String(x.content).slice(0,1200)})),recent_calls:b.messages?.filter(x=>x.tool_calls).slice(-1).map(x=>x.tool_calls.map(t=>({name:t.function.name,args:t.function.arguments.slice(0,150)})))}));
|
|
try{const r=await __diagFetch(input,{...init,signal:AbortSignal.any([...(init?.signal?[init.signal]:[]),AbortSignal.timeout(240000)])});
|
|
console.log('DIAGNOSTIC '+JSON.stringify({event:'headers',n,status:r.status,seconds:(Date.now()-t)/1000}));
|
|
r.clone().text().then(s=>console.log('DIAGNOSTIC '+JSON.stringify({event:'body',n,chars:s.length,seconds:(Date.now()-t)/1000,finish:s.match(/"finish_reason"\s*:\s*"[^" ]+"/g)?.slice(-2)}))).catch(()=>{});return r;
|
|
}catch(e){console.log('DIAGNOSTIC '+JSON.stringify({event:'error',n,name:e.name,seconds:(Date.now()-t)/1000}));throw e}
|
|
};
|
|
'''
|
|
(OUT/'backend.mjs').write_text(instrument+source,'utf-8')
|
|
env={**os.environ,'ELECTRON_RUN_AS_NODE':'1','PORT':os.getenv('STORYBOARD_DIAG_PORT','18988'),'HUOBAO_DATA_DIR':str(OUT),'SQLITE_PATH':str(dbfile),'FRONTEND_DIST':str(ROOT/'huobao-source/frontend/.output/public'),'WORKSPACE_PATH':str(OUT/'workspace')}
|
|
process=None
|
|
try:
|
|
with (OUT/'backend.log').open('w',encoding='utf-8') as log:
|
|
process=subprocess.Popen([str(stage/'HuobaoDrama.exe'),str(OUT/'backend.mjs')],cwd=OUT,env=env,stdout=log,stderr=log,creationflags=subprocess.CREATE_NO_WINDOW)
|
|
for _ in range(100):
|
|
if process.poll() is not None:raise RuntimeError('Isolated backend failed to start')
|
|
try:
|
|
urllib.request.urlopen('http://127.0.0.1:'+os.getenv('STORYBOARD_DIAG_PORT','18988')+'/api/v1/health',timeout=1).close();break
|
|
except OSError:time.sleep(.2)
|
|
start=time.monotonic();print(json.dumps({'started':str(OUT),'episode':8,'liveProjectUnmodified':True,'neutralTestScript':True,'model':model}),flush=True)
|
|
req=urllib.request.Request('http://127.0.0.1:'+os.getenv('STORYBOARD_DIAG_PORT','18988')+'/api/v1/agent/storyboard_breaker/jobs',data=json.dumps(body).encode(),headers={'Content-Type':'application/json'})
|
|
try:
|
|
with urllib.request.urlopen(req,timeout=600) as response:result=json.load(response)
|
|
except urllib.error.HTTPError as e:result=json.load(e)
|
|
except Exception as e:result={'error':type(e).__name__}
|
|
|
|
if result.get('data',{}).get('status')=='running':
|
|
while time.monotonic()-start<960:
|
|
time.sleep(3)
|
|
with urllib.request.urlopen('http://127.0.0.1:'+os.getenv('STORYBOARD_DIAG_PORT','18988')+'/api/v1/agent/storyboard_breaker/jobs/8',timeout=10) as response: result=json.load(response)
|
|
if result.get('data',{}).get('status')!='running':break
|
|
(OUT/'result.json').write_text(json.dumps(result,ensure_ascii=False,indent=2),'utf-8')
|
|
with sqlite3.connect(dbfile) as db:
|
|
db.row_factory=sqlite3.Row
|
|
saved=[dict(r) for r in db.execute('select storyboard_number,duration,video_prompt,video_prompt_en from storyboards where episode_id=8 order by storyboard_number')]
|
|
count=len(saved)
|
|
job=json.loads(db.execute("select value from app_settings where key='storyboard_job:8'").fetchone()[0])
|
|
(OUT/'coverage-artifact.json').write_text(json.dumps({'job':job,'saved':saved},ensure_ascii=False,indent=2),'utf-8')
|
|
print(json.dumps({'seconds':round(time.monotonic()-start,1),'shots':count,'status':result.get('data',{}).get('status'),'completed':result.get('data',{}).get('completed'),'jobError':result.get('data',{}).get('error'),'code':result.get('code'),'error':result.get('message') or result.get('error')}),flush=True)
|
|
assert job['status']=='done' and job['sectionIndex']==job['sectionCount']==3 and count==len(job['drafts']), 'Real model must finish every fixture scene and commit all drafts'
|
|
finally:
|
|
if process and process.poll() is None:process.terminate();process.wait(15)
|