#!/usr/bin/env python3 """第1集 镜02~09 Seedance批量生成 · 适配V3-R4不良人风格""" import os, sys, json, time, subprocess from pathlib import Path ROOT = Path('D:/WorkBuddy/guanghulab/video-ai-system') env = {} for line in open(ROOT/".env"): line = line.strip() if not line or line.startswith("#"): continue if "=" in line: k, v = line.split("=", 1) env[k.strip()] = v.strip() AK = env["JIMENG_API_KEY"] URL = env["JIMENG_BASE_URL"] OUT = ROOT / "outputs/shots" OUT.mkdir(parents=True, exist_ok=True) STYLE = "不良人硬核写实风格,厚重电影级光影,画面质感与CHAR-003苏白V3-R4统一" SHOTS = [ { # 镜02 "id": 2, "dur": 4, "p": "中国修仙世界百宗会招生广场,远景俯瞰。巨大广场上人山人海,各大宗门招生殿阵列排布,鎏金牌匾和灵光法阵交相辉映。穿修仙服饰的宗门弟子在各招生处前排起长队,人群熙熙攘攘热闹非凡。镜头固定俯拍微移。修仙世界大气磅礴,竖屏。" + STYLE }, { # 镜03 "id": 3, "dur": 5, "p": "修仙世界角落破旧招生点,从牌匾特写拉到中景。先特写一块悬挂在旧木杆上的破旧木牌匾——古朴天道宗三字风化斑驳。镜头缓慢后拉,露出站在牌匾下的18岁修仙少年苏白——白衫青纱袍,半束黑发,半侧身淡然从容的少年宗主。他站在寒酸招生点前,一牌一桌一广告牌就是全部家当。角落冷清,远处模糊可见其他豪华招生殿。" + STYLE }, { # 镜04 "id": 4, "dur": 5, "p": "修仙世界天道宗招生点前,中景平视。18岁白衫青纱袍少年宗主苏白站在破旧摊位旁,旁边竖立着A架粗麻广告牌。周围聚集了五六个人对他指指点点,有人面露嘲讽之色交头接耳。苏白双手叉腰自信喊话不惧嘲笑。修仙百宗会角落场景,广告牌无文字留白。固定机位轻微呼吸感。" + STYLE }, { # 镜05 "id": 5, "dur": 5, "p": "修仙世界天道宗角落招生点,中景拉近。围观者散去了,一阵风吹起苏白脚下落叶,白衫青纱袍的18岁少年宗主独自叹气,手扶额头看向远处灵霄宗豪华招生殿的方向。远处鎏金牌匾模糊可见。氛围孤独清冷但不绝望。镜头从固定机位缓慢推到苏白面部近景。" + STYLE }, { # 镜06 "id": 6, "dur": 5, "p": "修仙世界灵霄宗招生殿前,中景平视。蓝色执事袍银色灵纹的中年王执事正襟危坐紫檀案台后,手执朱砂灵笔在鉴定卷轴上画下一个红色叉号。他面容冷漠不屑,对面站着18岁粗麻布衣的清瘦少年诸葛风——剑眉星目苍白肤色,面露不可置信,双手攥拳指甲掐入掌心。灵霄宗鎏金牌匾高悬上方,背景排队修仙者如龙。固定机位,双角色对比冲突。" + STYLE }, { # 镜07 "id": 7, "dur": 4, "p": "修仙世界灵霄宗招生殿前,中景俯视。一个灰色素袍的普通中年男子从人群中挤出,微躬身双手捧布包袱递给王执事。蓝袍执事掂了掂袋子伸手一摸,表情从冷漠变成眉开眼笑。谄媚送礼与势利变脸的反差画面。" + STYLE }, { # 镜08 "id": 8, "dur": 4, "p": "粗麻布衣少年诸葛风特写,他的指甲深深陷入掌心留下旧疤痕,满脸屈辱嘴角紧抿。画面开始快速闪切模糊处理——闪切一:两位老人倒在血泊中,暗紫锦缎金丝绣边的华贵员外俯视鄙夷。闪切二:少年被踩住脑袋。闪切回到诸葛风面部,他的眼底开始燃烧仇恨火焰。情绪爆发,闪切做模糊化。" + STYLE }, { # 镜09 "id": 9, "dur": 5, "p": "修仙世界天道宗角落招生点,中景跟拍转固定。粗麻布衣的清瘦少年诸葛风穿过人群走来,他的眼睛忽然亮起——看到了天道宗破旧牌匾和广告牌上的信息。他走到摊位前鼓起勇气询问,白衫青纱袍的少年宗主苏白微笑回应,拍拍胸脯自信承诺。最后苏白手搭诸葛风肩膀,定格在信任建立的一刻。两人站在杆挂牌匾下的简陋招生点前。" + STYLE } ] def submit(prompt, duration): payload = { "model": "doubao-seedance-2-0-260128", "content": [{"type": "text", "text": prompt}], "duration": duration, "resolution": "720p" } r = subprocess.run(["curl", "-s", "-m", "60", "--noproxy", "*", "-X", "POST", f"{URL}/contents/generations/tasks", "-H", f"Authorization: Bearer {AK}", "-H", "Content-Type: application/json", "-d", json.dumps(payload)], capture_output=True, text=True, timeout=70) try: return json.loads(r.stdout) except: return {} def poll_dl(task_id, out_path, max_attempts=90): for i in range(max_attempts): time.sleep(10) r = subprocess.run(["curl", "-s", "-m", "30", "--noproxy", "*", f"{URL}/contents/generations/tasks/{task_id}"], capture_output=True, text=True, timeout=40, env={**os.environ, "Authorization": f"Bearer {AK}"}) try: data = json.loads(r.stdout) except: continue st = (data.get("status") or "").lower() if i % 5 == 0: print(f" poll {i+1}: {st}") if st in ["succeeded", "completed", "done"]: vurl = data.get("content",{}).get("video_url") if vurl: subprocess.run(["curl", "-s", "-m", "300", "-L", "--noproxy", "*", "-o", str(out_path), vurl], capture_output=True, timeout=310) if out_path.exists() and out_path.stat().st_size > 1024: return True, out_path return False, data if st in ["failed", "error"]: return False, data return False, {} results = [] for shot in SHOTS: sid = shot["id"] dur = shot["dur"] print(f"\n镜{sid:02d} · {dur}秒 · 提交中...") resp = submit(shot["p"], dur) task_id = resp.get("id") if not task_id: print(f" ❌ 无任务ID") results.append({"shot": sid, "ok": False}) continue print(f" task: {task_id}") dst = OUT / f"ep01-shot{sid:02d}.mp4" ok, _ = poll_dl(task_id, dst) if ok: print(f" ✅ {dst.name} ({dst.stat().st_size//1024}KB)") results.append({"shot": sid, "ok": True, "file": str(dst)}) else: print(f" ❌ 失败") results.append({"shot": sid, "ok": False}) time.sleep(5) print(f"\n{'='*40}") for r in results: st = "✅" if r["ok"] else "❌" print(f" {st} 镜{r['shot']:02d}") print(f"{'='*40}")