#!/usr/bin/env python3
import json, os, subprocess, time
from pathlib import Path

PROJECT_ROOT = Path('/Users/bot1/Volumes/root_for_ai/AI工作区/通用_产品宣传视频_古钱币杜邦纸钱袋包_20260530_1702')
LIBTV = '/Users/bot1/.hermes/profiles/video/tools/libtv-cli/libtv'
OUT_DIR = PROJECT_ROOT / 'outputs/libtv-cli-seedance-submit2-20260530_1845'
DOWNLOAD_DIR = OUT_DIR / 'videos'
STATUS = OUT_DIR / 'status.json'
LOG = OUT_DIR / 'run.log'
RUN_ID = '20260530_1845'

SCENES = [
    dict(scene_no=1, group='开场', title='古代文物造型转现代、轻颤苏醒、站起卡通化', node='Seedance视频01_submit2_20260530_1845', refs=['i-6SjoCxfKTJ','i-Kpy6XaZQlk','i-gbrjBBS1yb','i-j1aoO9eusM'], y='0', prompt='用上游4张分镜图作为参考，生成连续单画面视频，不要九宫格/拼贴/画中画。15秒，3:4，720p。故事：古代文物造型的钱币从暖暗文博桌面过渡到干净现代浅米色展示；中间硬币先轻微摇晃、细微颤动；随后硬币们活过来，长出短圆柔软手脚并站起，保持原有钱币形状、颜色、材质和刻纹差异。不要文字、logo、水印、UI；不要把所有钱币统一成方孔钱；只要环境声和动作音效，不要BGM。'),
    dict(scene_no=2, group='古币成精', title='刚成人的钱币小角色行走、一枚受惊轻跃', node='Seedance视频02_submit2_20260530_1845', refs=['i-q0F6haxerK','i-1BQiwj8Z6R','i-rcZQgXXJ5m','i-zeWHX8x4dT'], y='420', prompt='用上游4张分镜图作为参考，生成连续单画面视频，不要九宫格/拼贴/画中画。15秒，3:4，720p。故事：刚刚活过来的钱币小角色在浅米色桌面/微缩世界中松散同向前行，短圆柔软手脚，动作笨拙可爱；其中一枚钱币受到惊吓轻轻跃起，其它钱币保持行走状态。保持每枚钱币原有形状、颜色、材质和刻纹差异。不要文字、logo、水印、UI；不要机械关节；只要环境声和动作音效，不要BGM。'),
    dict(scene_no=3, group='产品进入', title='三枚硬币逃跑、被钱袋包吸成金色光点并成为纹样', node='Seedance视频03_submit2_20260530_1845', refs=['i-16I71wUAM5','i-EVUUh2fB4c','i-5UJyqPpokk','i-Qi3tk9R4WJ'], y='840', prompt='用上游4张分镜图作为参考，生成连续单画面视频，不要九宫格/拼贴/画中画。15秒，3:4，720p。故事：三枚钱币小角色慌张逃跑，黄色杜邦纸钱袋包出现并产生温和吸引力；钱币被吸起后化成金色光点，光点飞入钱袋包表面，逐渐成为包身上的金色钱币纹样。保持钱袋包的黄色材质、口袋结构和分镜里的纹样位置。不要文字、logo、水印、UI；不要新增复杂机械装置；只要环境声和动作音效，不要BGM。'),
    dict(scene_no=4, group='产品展示', title='钱袋包纹样亮起、手提与背包产品展示', node='Seedance视频04_submit2_20260530_1845', refs=['i-8Fd3CjhaZ5','i-qIyPx61jxx','i-ZHrw5BOqN4','i-JkZeSA6Trp'], y='1260', prompt='用上游4张分镜图作为参考，生成连续单画面视频，不要九宫格/拼贴/画中画。15秒，3:4，720p。故事：黄色杜邦纸钱袋包完成纹样转化后进入产品展示，包身金色钱币纹样轻微亮起；镜头稳定展示手提、背包/佩戴和干净棚拍产品状态，突出材质、结构、开口和包身图案。保持产品外观、颜色、纹样和比例，不重新设计成其它包。不要文字、logo、水印、UI；只要环境声和动作音效，不要BGM。'),
]

def log(msg):
    OUT_DIR.mkdir(parents=True, exist_ok=True)
    line = f"[{time.strftime('%Y-%m-%dT%H:%M:%S%z')}] {msg}"
    print(line, flush=True)
    with LOG.open('a', encoding='utf-8') as f: f.write(line+'\n')

def save(data):
    OUT_DIR.mkdir(parents=True, exist_ok=True)
    data['updated_at'] = time.strftime('%Y-%m-%dT%H:%M:%S%z')
    STATUS.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding='utf-8')

def run(args, timeout=None, check=True):
    log('$ ' + ' '.join(map(str,args)))
    r = subprocess.run(args, cwd=str(PROJECT_ROOT), text=True, capture_output=True, timeout=timeout)
    if r.stdout.strip(): log('STDOUT: ' + r.stdout.strip()[:12000])
    if r.stderr.strip(): log('STDERR: ' + r.stderr.strip()[:12000])
    if check and r.returncode != 0: raise RuntimeError(f'cmd failed {r.returncode}: {args}\n{r.stderr}')
    return r

def node_exists(name, group):
    # Do NOT probe a video node via `libtv node <name> -g <group>` here: for some
    # video nodes that command can block for minutes. `node list -g` is quick and
    # returns JSON, so use it as the existence check.
    r = run([LIBTV, 'node', 'list', '-g', group], check=False, timeout=120)
    if r.returncode != 0:
        return False
    try:
        payload = json.loads(r.stdout)
    except Exception:
        log('WARN: failed to parse node list JSON for group ' + group)
        return False
    for node in payload.get('nodes', []):
        if node.get('name') == name or node.get('id') == name:
            return True
    return False

def create_node(scene):
    if node_exists(scene['node'], scene['group']):
        log(f"node exists: {scene['node']}")
        return
    args=[LIBTV,'node','--x','620','--y',scene['y'],'create',scene['node'],'-t','video','-g',scene['group'],'--prompt',scene['prompt'],
          '-s','model=star-video2','-s','modeType=image2video','-s','count=1','-s','ratio=3:4','-s','resolution=720p','-s','duration=15','-s','enableSound=on','-s','searchEnabled=0','-s','autoCompliance=0']
    for ref in scene['refs']:
        args += ['--left-add', ref]
    run(args, timeout=180)

def download(scene):
    DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
    return run([LIBTV,'download','-n',scene['node'],'-g',scene['group'],'-o',str(DOWNLOAD_DIR),'--without-ai-watermark','--vip'], timeout=300, check=False)

def main():
    data={'status':'starting','project_url':'https://www.liblib.tv/canvas?projectId=6739cc5ca9d1415ba4a82b9673f3931e','segments':[]}
    save(data)
    run([LIBTV,'project','use','6739cc5ca9d1415ba4a82b9673f3931e'], timeout=120)
    for scene in SCENES:
        seg={'scene_no':scene['scene_no'],'group':scene['group'],'node':scene['node'],'status':'creating'}
        data['segments'].append(seg); save(data)
        create_node(scene)
        seg['status']='running_seedance'; save(data)
        # no timeout here: LibTV CLI blocks until generation finishes or fails
        r = run([LIBTV,'node',scene['node'],'-g',scene['group'],'--run'], timeout=None, check=False)
        seg['run_returncode']=r.returncode; seg['run_stdout_tail']=r.stdout[-4000:]; seg['run_stderr_tail']=r.stderr[-4000:]
        seg['status']='downloading'; save(data)
        d = download(scene)
        seg['download_returncode']=d.returncode; seg['download_stdout_tail']=d.stdout[-4000:]; seg['download_stderr_tail']=d.stderr[-4000:]
        seg['status']='done_or_attempted'; save(data)
    files=sorted(str(p) for p in DOWNLOAD_DIR.glob('*') if p.is_file())
    data['status']='completed'; data['downloaded_files']=files; data['download_dir']=str(DOWNLOAD_DIR); save(data)
    print(json.dumps(data, ensure_ascii=False, indent=2))

if __name__ == '__main__': main()
