asset: 归档第一集已生成镜头与测试素材,排除可再生缓存

This commit is contained in:
2026-10-01 00:35:36 +08:00
parent 0da2659df2
commit f900237d96
1719 changed files with 264761 additions and 0 deletions
@@ -0,0 +1,114 @@
"""Tracked screen graphics on H3 video; originals preserved."""
import sys,os,json,subprocess,math
from pathlib import Path
sys.path.insert(0,r'C:/Users/zhang/.codex/tools/screen-post')
import cv2,numpy as np
from PIL import Image,ImageDraw,ImageFont
ROOT=Path(__file__).resolve().parent.parent
SKILL=Path(r'C:/Users/zhang/.codex/skills/ffmpeg-video-editor/scripts')
os.environ['PATH']=r'C:/Users/zhang/.codex/tools/media-bin;'+os.environ.get('PATH','')
FONT=r'C:/Windows/Fonts/msyh.ttc'
def dump(p,v): p.write_text(json.dumps(v,ensure_ascii=False,indent=2),encoding='utf-8')
def font(n): return ImageFont.truetype(FONT,n)
def wrap(s,n):return '\n'.join(s[i:i+n] for i in range(0,len(s),n))
def graphic(meta,t):
cat=meta['shot']=='E01-S01-C02'
W,H=(480,480) if cat else (420,760)
im=Image.new('RGBA',(W,H),(0,0,0,0));d=ImageDraw.Draw(im)
if cat:
d.text((55,90),'分类结果',font=font(35),fill=(65,72,78,255))
d.text((190,190),'锅',font=font(82),fill=(35,44,53,255))
d.line([(170,350),(192,370),(235,320)],fill=(62,118,82,255),width=7)
if t>=2.6:d.line([(165,325),(239,373)],fill=(164,65,61,255),width=6)
return np.array(im)
event=next((e for e in meta['events'] if e['start']<=t<e['end']),meta['events'][-1])
d.rounded_rectangle((0,0,W-1,H-1),radius=24,fill=(237,238,234,255))
d.rectangle((0,0,W,82),fill=(224,228,225,255))
if event['mode']=='rota':
d.text((150,22),'排班表',font=font(31),fill=(36,44,48,255))
for k,txt in enumerate(event['rows']):
y=155+k*98;d.rectangle((24,y-18,396,y+62),outline=(145,155,157),width=2)
d.text((45,y),txt,font=font(29),fill=(44,50,53,255))
return np.array(im)
d.text((177,22),event.get('contact','妈妈'),font=font(32),fill=(36,44,48,255))
d.rounded_rectangle((22,124,352,220),radius=15,fill=(255,255,252,255))
d.text((42,151),event.get('incoming',''),font=font(29),fill=(39,45,47,255))
s=event.get('draft','')
if 'typing' in event:
a,b=event['typing']['start'],event['typing']['end'];s=s[:int(len(s)*max(0,min(1,(t-a)/(b-a))))]
if 'deleting' in event:
a,b=event['deleting']['start'],event['deleting']['end'];s=s[:math.ceil(len(s)*(1-max(0,min(1,(t-a)/(b-a)))))]
sent=event.get('sent',False)
d.rounded_rectangle((18,302,401,470),radius=13,fill=(180,212,170,255) if sent else (252,253,249,255),outline=(164,174,173,255),width=2)
if s:d.multiline_text((34,321),wrap(s,12),font=font(28),spacing=12,fill=(32,41,42,255))
d.text((34,492),'已发送' if sent else '未发送',font=font(20),fill=(109,121,120,255))
# Muted simple keyboard lies behind thumb occlusion.
for y in range(560,726,51):
for x in range(20,386,39):d.rounded_rectangle((x,y,x+30,y+40),radius=4,fill=(251,251,247,255),outline=(189,197,196,255))
return np.array(im)
def runtool(name,args,log):
p=subprocess.run([sys.executable,str(SKILL/name),*args],capture_output=True,text=True,encoding='utf-8',errors='replace')
log.write_text(p.stdout+'\n'+p.stderr,encoding='utf-8')
if p.returncode:raise RuntimeError(str(log))
return p.stdout
def main(sid):
folder=ROOT/'shots'/sid;meta=json.loads((folder/'screen_overlay.json').read_text(encoding='utf-8'))
src=Path(json.loads((folder/'outputs.json').read_text(encoding='utf-8'))[0])
cap=cv2.VideoCapture(str(src));fps=cap.get(cv2.CAP_PROP_FPS);w=int(cap.get(cv2.CAP_PROP_FRAME_WIDTH));h=int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
ok,first=cap.read();assert ok
cs=meta['coordinate_space'];scale=np.array([w/cs['width'],h/cs['height']],dtype=np.float32)
quad=np.float32(meta.get('panel_quad_clockwise',meta['screen_quad_clockwise']))*scale
screen=np.float32(meta['screen_quad_clockwise'])*scale
g0=cv2.cvtColor(first,cv2.COLOR_BGR2GRAY)
mask=np.zeros((h,w),np.uint8);cv2.fillConvexPoly(mask,np.int32(screen),255)
mask=cv2.dilate(mask,np.ones((27,27),np.uint8))
inner=np.zeros_like(mask);cv2.fillConvexPoly(inner,np.int32(screen),255);inner=cv2.erode(inner,np.ones((11,11),np.uint8))
mask=cv2.subtract(mask,inner)
pts=cv2.goodFeaturesToTrack(g0,maxCorners=160,qualityLevel=.005,minDistance=5,mask=mask)
if pts is None or len(pts)<8:raise RuntimeError('Insufficient screen-border features')
frames=folder/'post_frames';frames.mkdir(exist_ok=True)
cap.set(cv2.CAP_PROP_POS_FRAMES,0);idx=0;lastH=np.eye(3);metrics=[]
while True:
ok,fr=cap.read()
if not ok:break
gray=cv2.cvtColor(fr,cv2.COLOR_BGR2GRAY)
dst,status,err=cv2.calcOpticalFlowPyrLK(g0,gray,pts,None,winSize=(25,25),maxLevel=4)
good=(status.ravel()==1)&(err.ravel()<30)
H,ins=(None,None)
if good.sum()>=8:H,ins=cv2.findHomography(pts[good],dst[good],cv2.RANSAC,3)
valid=H is not None and int(ins.sum())>=7
if valid:
test=cv2.perspectiveTransform(screen[None],H)[0]
area=abs(cv2.contourArea(test));base=abs(cv2.contourArea(screen))
valid=.7<area/base<1.35 and np.max(np.linalg.norm(test-screen,axis=1))<100
if valid:lastH=H
target=cv2.perspectiveTransform(quad[None],lastH)[0].astype(np.float32)
art=graphic(meta,idx/fps);ah,aw=art.shape[:2]
M=cv2.getPerspectiveTransform(np.float32([[0,0],[aw-1,0],[aw-1,ah-1],[0,ah-1]]),target)
layer=cv2.warpPerspective(art,M,(w,h),flags=cv2.INTER_CUBIC)
alpha=layer[:,:,3].astype(np.float32)/255
if sid!='E01-S01-C02':
hsv=cv2.cvtColor(fr,cv2.COLOR_BGR2HSV)
skin=((hsv[:,:,0]<30)&(hsv[:,:,1]>52)&(hsv[:,:,2]>90)).astype(np.uint8)
skin=cv2.dilate(skin,np.ones((3,3),np.uint8))
alpha*=1-skin
rgb=cv2.cvtColor(fr,cv2.COLOR_BGR2RGB)
out=(rgb*(1-alpha[:,:,None])+layer[:,:,:3]*alpha[:,:,None]).clip(0,255).astype(np.uint8)
Image.fromarray(out).save(frames/('frame_%04d.png'%idx))
metrics.append({'frame':idx,'valid_homography':bool(valid),'inliers':int(ins.sum()) if ins is not None else 0,'quad':target.tolist()})
idx+=1
cap.release()
dump(folder/'screen_tracking.json',{'method':'First-frame screen-border Shi-Tomasi features, pyramidal LK flow, RANSAC homography, bounded area/displacement, skin occlusion mask for phones','frames':metrics,'valid_fraction':sum(x['valid_homography'] for x in metrics)/len(metrics)})
video=folder/'post_screen_silent.mp4';out=folder/'post_screen.mp4'
args=['--dir',str(frames),'--pattern','frame_%04d.png','--fps',str(fps),'-o',str(video),'--quality','18','--overwrite']
runtool('sequence.py',args+['--dry-run','--json'],folder/'post_plan.json')
runtool('sequence.py',args+['--json'],folder/'post_sequence.json')
args=[str(video),'--replace',str(src),'-o',str(out),'--overwrite']
runtool('audio.py',args+['--dry-run','--json'],folder/'post_audio_plan.json')
runtool('audio.py',args+['--json'],folder/'post_audio.json')
runtool('probe.py',[str(out),'--json'],folder/'post_probe.json')
runtool('check.py',[str(out),'--platform','custom','--json'],folder/'post_check.json')
runtool('look.py',[str(out),'--tiles','3x2','--overwrite'],folder/'post_look.log')
dump(folder/'post_outputs.json',[str(out)])
print(json.dumps({'shot':sid,'output':str(out),'frames':idx,'valid_fraction':sum(x['valid_homography'] for x in metrics)/len(metrics)},ensure_ascii=False))
if __name__=='__main__':main(sys.argv[1])