#!/usr/bin/env python3
import json
import os
import re
import subprocess
import sys
import 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'
PROJECT_ID = '6739cc5ca9d1415ba4a82b9673f3931e'
RUN_ID = '20260530_174949'
SHEETS_DIR = PROJECT_ROOT / 'deliverables/storyboard_sheets_20260530'
PROMPT_MD = SHEETS_DIR / '动态视频提示词_按4组分镜_20260530.md'
OUT_DIR = PROJECT_ROOT / 'outputs/libtv-cli-seedance-4x15-20260530_174949'
DOWNLOAD_DIR = OUT_DIR / 'videos'
STATUS_PATH = OUT_DIR / 'status.json'
LOG_PATH = OUT_DIR / 'run.log'

# Directly use the four user-made LibTV canvas groups and their existing storyboard image nodes.
SCENES = [
    {
        'scene_no': 1,
        'group': '开场',
        'title': '古代文物造型转现代、轻颤苏醒、站起卡通化',
        'refs': ['i-6SjoCxfKTJ', 'i-Kpy6XaZQlk', 'i-gbrjBBS1yb', 'i-j1aoO9eusM'],
    },
    {
        'scene_no': 2,
        'group': '古币成精',
        'title': '刚成人的钱币小角色行走、一枚受惊轻跃',
        'refs': ['i-q0F6haxerK', 'i-1BQiwj8Z6R', 'i-rcZQgXXJ5m', 'i-zeWHX8x4dT'],
    },
    {
        'scene_no': 3,
        'group': '产品进入',
        'title': '三枚硬币逃跑、被钱袋包吸成金色光点并成为纹样',
        'refs': ['i-16I71wUAM5', 'i-EVUUh2fB4c', 'i-5UJyqPpokk', 'i-Qi3tk9R4WJ'],
    },
    {
        'scene_no': 4,
        'group': '产品展示',
        'title': '钱袋包纹样亮起、手提与背包产品展示',
        # group list order was reversed, but using node IDs preserves all four refs.
        'refs': ['i-8Fd3CjhaZ5', 'i-qIyPx61jxx', 'i-ZHrw5BOqN4', 'i-JkZeSA6Trp'],
    },
]


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


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


def run_cmd(args, check=True, timeout=None):
    log('$ ' + ' '.join([str(a) for a in args]))
    env = os.environ.copy()
    res = subprocess.run(args, cwd=str(PROJECT_ROOT), env=env, text=True, capture_output=True, timeout=timeout)
    if res.stdout.strip():
        log('STDOUT: ' + res.stdout.strip()[:12000])
    if res.stderr.strip():
        log('STDERR: ' + res.stderr.strip()[:12000])
    if check and res.returncode != 0:
        raise RuntimeError(f'command failed {res.returncode}: {args}\nSTDOUT={res.stdout}\nSTDERR={res.stderr}')
    return res


def parse_json_loose(text: str):
    text = (text or '').strip()
    if not text:
        return None
    lines = [ln for ln in text.splitlines() if ln.strip()]
    for candidate in [text, lines[-1] if lines else '']:
        try:
            return json.loads(candidate)
        except Exception:
            pass
    return None


def extract_scene_section(text: str, n: int) -> str:
    m = re.search(rf'^# 分镜{n}｜.*$', text, flags=re.M)
    if not m:
        raise RuntimeError(f'cannot find section for scene {n}')
    start = m.start()
    m_next = re.search(r'^# 分镜\d+｜|^# 可选：整条连续视频提交稿', text[m.end():], flags=re.M)
    end = m.end() + m_next.start() if m_next else len(text)
    return text[start:end].strip()


def make_prompt(scene_no: int, title: str, section: str) -> str:
    return f'''请用 Seedance 2.0 生成第 {scene_no} 段视频，15 秒，3:4，720p。\n\n本段主题：{title}\n\n重要要求：\n- 以上游连接的 4 张分镜图作为本段参考，按照分镜顺序形成连续单画面视频；不要生成九宫格、分镜板、拼贴图或画中画。\n- 严格按下面提示词生成，不要新增分镜里没有的角色、logo、文字、产品结构或夸张机械装置。\n- 钱币角色和黄色钱袋包的外形、材质、颜色、纹样以分镜图为准。\n- 声音只要环境声和动作音效，不要背景音乐，不要旋律型 BGM。\n\n{section}\n'''


def ffprobe(path: Path) -> dict:
    cmd = ['ffprobe','-v','error','-show_entries','format=duration:stream=codec_type,width,height','-of','json',str(path)]
    try:
        res = subprocess.run(cmd, text=True, capture_output=True, timeout=30)
        if res.returncode != 0:
            return {'ok': False, 'error': res.stderr.strip()}
        data = json.loads(res.stdout)
        video_stream = next((s for s in data.get('streams', []) if s.get('codec_type') == 'video'), {})
        audio_stream = next((s for s in data.get('streams', []) if s.get('codec_type') == 'audio'), None)
        return {'ok': True, 'duration': float(data.get('format', {}).get('duration', 0) or 0), 'width': video_stream.get('width'), 'height': video_stream.get('height'), 'has_audio': audio_stream is not None}
    except Exception as e:
        return {'ok': False, 'error': str(e)}


def download_node(video_node: str, group: str, scene_no: int):
    # CLI download writes original file names; then rename/copy behavior is CLI-defined.
    before = {p.name for p in DOWNLOAD_DIR.glob('*') if p.is_file()}
    res = run_cmd([LIBTV, 'download', '-n', video_node, '-g', group, '-o', str(DOWNLOAD_DIR), '--without-ai-watermark', '--vip'], check=False, timeout=300)
    after = sorted([p for p in DOWNLOAD_DIR.glob('*') if p.is_file() and p.name not in before])
    # If no new name appeared, still record all current files.
    return res, after


def main():
    OUT_DIR.mkdir(parents=True, exist_ok=True)
    DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
    # append log separation for reruns
    log('=== RUN START: use existing canvas groups ===')
    status = {'status': 'starting', 'project_id': PROJECT_ID, 'project_url': f'https://www.liblib.tv/canvas?projectId={PROJECT_ID}', 'segments': []}
    save_status(status)

    if not PROMPT_MD.exists():
        raise SystemExit(f'prompt_missing: {PROMPT_MD}')
    prompt_text = PROMPT_MD.read_text(encoding='utf-8')

    run_cmd([LIBTV, 'project', 'use', PROJECT_ID], timeout=120)

    for scene in SCENES:
        scene_no = scene['scene_no']
        group = scene['group']
        title = scene['title']
        video_node = f'Seedance视频{scene_no:02d}_{RUN_ID}'
        y = str((scene_no - 1) * 420)
        segment = {'scene_no': scene_no, 'group': group, 'title': title, 'video_node': video_node, 'reference_nodes': scene['refs'], 'status': 'creating_and_running'}
        status['segments'].append(segment)
        save_status(status)

        # Sanity-list the group before connecting, so status/log contains evidence of the refs.
        list_res = run_cmd([LIBTV, 'node', 'list', '-g', group], timeout=120)
        segment['group_list'] = parse_json_loose(list_res.stdout)
        save_status(status)

        section = extract_scene_section(prompt_text, scene_no)
        prompt = make_prompt(scene_no, title, section)
        prompt_path = OUT_DIR / f'prompt_segment_{scene_no:02d}.txt'
        prompt_path.write_text(prompt, encoding='utf-8')
        segment['prompt_path'] = str(prompt_path)

        cmd = [
            LIBTV, 'node', '--x', '620', '--y', y, 'create', video_node,
            '-t', 'video', '-g', group,
            '--prompt', prompt,
            '-s', 'model=star-video2',
            '-s', 'modeType=mixed2video',
            '-s', 'count=1',
            '-s', 'ratio=3:4',
            '-s', 'resolution=720p',
            '-s', 'duration=15',
            '-s', 'enableSound=on',
            '-s', 'search_enabled=0',
        ]
        for ref in scene['refs']:
            cmd += ['--left-add', ref]
        cmd += ['--run']
        res = run_cmd(cmd, timeout=1200)
        segment['status'] = 'run_returned'
        segment['node_stdout_json'] = parse_json_loose(res.stdout)
        segment['node_stderr_tail'] = (res.stderr or '')[-6000:]
        save_status(status)

        segment['status'] = 'downloading'
        save_status(status)
        dl_res, new_files = download_node(video_node, group, scene_no)
        segment['download_returncode'] = dl_res.returncode
        segment['download_stdout'] = dl_res.stdout[-4000:]
        segment['download_stderr'] = dl_res.stderr[-4000:]
        segment['new_downloaded_files'] = [str(p) for p in new_files]
        segment['status'] = 'completed_or_download_attempted'
        save_status(status)

    media_files = sorted([p for p in DOWNLOAD_DIR.glob('*') if p.is_file()])
    probes = []
    for p in media_files:
        probes.append({'file': str(p), 'bytes': p.stat().st_size, 'ffprobe': ffprobe(p) if p.suffix.lower() in ['.mp4','.mov','.webm'] else {'ok': False, 'note': 'not_video_ext'}})
    status['status'] = 'completed'
    status['download_dir'] = str(DOWNLOAD_DIR)
    status['downloaded_files'] = [str(p) for p in media_files]
    status['ffprobe'] = probes
    save_status(status)
    print(json.dumps(status, ensure_ascii=False, indent=2), flush=True)

if __name__ == '__main__':
    try:
        main()
    except Exception as e:
        OUT_DIR.mkdir(parents=True, exist_ok=True)
        err = {'status': 'failed', 'error': str(e), 'updated_at': time.strftime('%Y-%m-%dT%H:%M:%S%z')}
        STATUS_PATH.write_text(json.dumps(err, ensure_ascii=False, indent=2), encoding='utf-8')
        log('FAILED: ' + str(e))
        raise
