Toly-89 commited on
Commit
38f4fc3
·
verified ·
1 Parent(s): 9880670

Upload phase_b/tools/broll_randomizer.py

Browse files
Files changed (1) hide show
  1. phase_b/tools/broll_randomizer.py +180 -0
phase_b/tools/broll_randomizer.py ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 方案 B v2 落地工具 #2:B-Roll 随机组合 + CapCut 操作指令生成
3
+
4
+ 输入:批量生成的 ScriptVariant JSON
5
+ 输出:
6
+ 1. 每条视频的 B-Roll 随机分配(24段素材,8动作×3角度)
7
+ 2. 防查重参数(翻转/滤镜/裁剪)
8
+ 3. CapCut 逐条操作手册
9
+ 4. 20天发布排期表
10
+
11
+ 使用方法:
12
+ python broll_randomizer.py --input batch_result.json --output ./production/
13
+ """
14
+
15
+ import json
16
+ import random
17
+ import os
18
+ import csv
19
+ import argparse
20
+ from pathlib import Path
21
+ from datetime import datetime, timedelta
22
+
23
+ # 24 段素材:8 个动作 × 3 个角度
24
+ BROLL_POOL = {
25
+ "product_show": {"angle_A":"broll_01_product_front.mp4", "angle_B":"broll_02_product_side.mp4", "angle_C":"broll_03_product_topdown.mp4"},
26
+ "texture_squeeze": {"angle_A":"broll_04_squeeze_front.mp4", "angle_B":"broll_05_squeeze_side.mp4", "angle_C":"broll_06_squeeze_45deg.mp4"},
27
+ "texture_spread": {"angle_A":"broll_07_spread_front.mp4", "angle_B":"broll_08_spread_side_slowmo.mp4", "angle_C":"broll_09_spread_topdown.mp4"},
28
+ "face_apply": {"angle_A":"broll_10_apply_front.mp4", "angle_B":"broll_11_apply_side.mp4", "angle_C":None},
29
+ "absorption": {"angle_A":"broll_12_absorb_front.mp4", "angle_B":"broll_13_absorb_sidecloseup.mp4", "angle_C":None},
30
+ "paper_test": {"angle_A":"broll_14_paper_front.mp4", "angle_B":"broll_15_paper_side.mp4", "angle_C":"broll_16_paper_topdown.mp4"},
31
+ "result_glow": {"angle_A":"broll_17_glow_front.mp4", "angle_B":"broll_18_glow_sidelight.mp4", "angle_C":None},
32
+ "cta": {"angle_A":"broll_19_cta_front.mp4", "angle_B":"broll_20_cta_side.mp4", "angle_C":"broll_21_cta_topdown.mp4"},
33
+ }
34
+
35
+ FORMAT_BROLL_ACTIONS = {
36
+ "before_after": ["product_show","face_apply","result_glow","texture_spread","cta"],
37
+ "day_in_life": ["product_show","face_apply","texture_spread","absorption","result_glow","cta"],
38
+ "unboxing_test": ["product_show","texture_squeeze","texture_spread","face_apply","result_glow","cta"],
39
+ "reaction_duet": ["product_show","texture_spread","face_apply","result_glow","cta"],
40
+ "viral_challenge": ["product_show","face_apply","result_glow","cta"],
41
+ "educational_hack": ["texture_squeeze","texture_spread","absorption","result_glow","cta"],
42
+ "comparison_test": ["product_show","texture_spread","face_apply","result_glow","cta"],
43
+ "texture_asmr": ["texture_squeeze","texture_spread","absorption","product_show","cta"],
44
+ "storytelling": ["product_show","face_apply","texture_spread","result_glow","cta"],
45
+ "trend_setter": ["product_show","face_apply","result_glow","cta"],
46
+ "ingredient_deep_dive": ["texture_squeeze","texture_spread","absorption","result_glow","cta"],
47
+ "transformation_journey": ["product_show","face_apply","absorption","result_glow","cta"],
48
+ "myth_busting": ["product_show","texture_spread","absorption","result_glow","cta"],
49
+ }
50
+
51
+ FLIP_OPTIONS = [False, True]
52
+ FILTER_OPTIONS = ["原色","暖调","冷调","褪色","日系","复古","清新","美食","电影","自然"]
53
+ CROP_OPTIONS = ["无裁剪","放大105%","放大110%","居中裁剪","上移10%"]
54
+
55
+
56
+ def assign_broll(video_format: str, seed: int = None) -> list[dict]:
57
+ if seed is not None: random.seed(seed)
58
+ actions = FORMAT_BROLL_ACTIONS.get(video_format, FORMAT_BROLL_ACTIONS["before_after"])
59
+ assigned = []
60
+ for action in actions:
61
+ pool = BROLL_POOL.get(action, {})
62
+ available = [k for k, v in pool.items() if v is not None]
63
+ if not available: continue
64
+ chosen = random.choice(available)
65
+ assigned.append({
66
+ "action": action, "angle": chosen, "file": pool[chosen],
67
+ "flip": random.choice(FLIP_OPTIONS),
68
+ "filter": random.choice(FILTER_OPTIONS),
69
+ "crop": random.choice(CROP_OPTIONS),
70
+ })
71
+ return assigned
72
+
73
+
74
+ def generate_production_plan(variants_raw: list, output_dir: str) -> dict:
75
+ os.makedirs(output_dir, exist_ok=True)
76
+ random.seed(42)
77
+ valid = [v for v in variants_raw if v.get("video_format") in FORMAT_BROLL_ACTIONS]
78
+ plan = []
79
+ used = set()
80
+ for i, v in enumerate(valid):
81
+ broll = assign_broll(v["video_format"], seed=42 + i * 7)
82
+ fp = "|".join(b["file"] for b in broll) + str(broll[0]["flip"])
83
+ if fp in used:
84
+ broll = assign_broll(v["video_format"], seed=42 + i * 7 + 999)
85
+ fp = "|".join(b["file"] for b in broll) + str(broll[0]["flip"])
86
+ used.add(fp)
87
+ capcut_steps = _capcut_steps(v, broll, i+1)
88
+ plan.append({"sequence":i+1,"variant_id":v["variant_id"],"video_format":v["video_format"],
89
+ "hook_text":v.get("hook_style",""),"caption":v.get("caption_local",""),
90
+ "hashtags":v.get("hashtags",[]),"broll_assignment":broll,"fingerprint":fp,
91
+ "capcut_steps":capcut_steps})
92
+
93
+ schedule = _make_schedule(plan)
94
+ plan_path = os.path.join(output_dir, "production_plan.json")
95
+ with open(plan_path,"w",encoding="utf-8") as f:
96
+ json.dump({"total_videos":len(plan),"generated_at":datetime.now().isoformat(),"production_plan":plan},f,ensure_ascii=False,indent=2)
97
+ md_path = os.path.join(output_dir, "CAPCUT_操作手册.md")
98
+ _make_md(plan, schedule, md_path)
99
+ csv_path = os.path.join(output_dir, "发布排期表.csv")
100
+ _make_csv(schedule, csv_path)
101
+ return {"plan_file":plan_path,"manual_file":md_path,"schedule_csv":csv_path,"total":len(plan)}
102
+
103
+
104
+ def _capcut_steps(variant: dict, broll: list, seq: int) -> list[str]:
105
+ fn = f"output_{seq:03d}_{variant['variant_id']}.mp4"
106
+ steps = [
107
+ f"1. 打开模板「beauty_sunscreen_template」→ 另存为「{variant['variant_id']}」",
108
+ f"2. 口播音轨:拖入 /tts/{variant['variant_id']}.mp3",
109
+ f"3. 前置 Hook 自拍:录 5 秒手机自拍,说:「{variant.get('hook_style','')[:50]}...」",
110
+ ]
111
+ for j, b in enumerate(broll):
112
+ fn = " → 水平翻转" if b["flip"] else ""
113
+ steps.append(f"4.{j+1}. 视频轨-素材{j+1}:拖入 /broll/{b['file']}{fn}")
114
+ steps.append(f"5. 滤镜:{broll[0]['filter']}")
115
+ steps.append("6. 自动字幕 → 语言选越南语 → 确认")
116
+ steps.append(f"7. 文案:{variant.get('caption_local','')[:80]}")
117
+ steps.append(f"8. 导出 → /output/{fn}")
118
+ steps.append(f"9. 发布时粘贴 Hashtags:{' '.join(variant.get('hashtags',[])[:5])}")
119
+ return steps
120
+
121
+
122
+ def _make_schedule(plan: list) -> list:
123
+ slots = ["07:30","12:30","19:00","20:00","21:00"]
124
+ schedule = []
125
+ start = datetime.now().replace(hour=0,minute=0,second=0,microsecond=0)+timedelta(days=1)
126
+ for i, item in enumerate(plan):
127
+ day = start + timedelta(days=i//5)
128
+ h,m = int(slots[i%5].split(":")[0]), int(slots[i%5].split(":")[1])
129
+ pt = day.replace(hour=h, minute=m)
130
+ schedule.append({
131
+ "video_seq":item["sequence"],"variant_id":item["variant_id"],
132
+ "video_format":item["video_format"],
133
+ "publish_date":pt.strftime("%Y-%m-%d"),"publish_time":pt.strftime("%H:%M"),
134
+ "publish_datetime":pt.isoformat(),
135
+ "hashtags":" ".join(item.get("hashtags",[])[:5]),
136
+ })
137
+ return schedule
138
+
139
+
140
+ def _make_md(plan, schedule, path):
141
+ lines = ["# CapCut 批量视频生产 — 操作手册","",f"总视频数: {len(plan)}","",
142
+ "## 准备工作","",
143
+ "□ /broll/ 有 24 个素材文件 (broll_01 ~ broll_21)",
144
+ "□ /tts/ 有所有口播 .mp3",
145
+ "□ CapCut 模板「beauty_sunscreen_template」已创建",
146
+ "□ 滤镜已安装(暖调/冷调/日系/复古/清新/自然/电影)","",
147
+ "## 逐条操作(前 5 条示例)",""]
148
+ for item in plan[:5]:
149
+ lines.append(f"### {item['sequence']}. `{item['variant_id']}` ({item['video_format']})")
150
+ for s in item["capcut_steps"]: lines.append(f"- {s}")
151
+ lines.append("")
152
+ lines.append(f"*(共 {len(plan)} 条,完整见 production_plan.json)*\n\n## 发布排期(前 3 天)\n")
153
+ lines.append("| 日期 | 时间 | 视频 | 格式 |")
154
+ lines.append("|---|---|---|---|")
155
+ for s in schedule[:15]:
156
+ lines.append(f"| {s['publish_date']} | {s['publish_time']} | #{s['video_seq']} | {s['video_format']} |")
157
+ with open(path,"w",encoding="utf-8") as f: f.write("\n".join(lines))
158
+
159
+
160
+ def _make_csv(schedule, path):
161
+ with open(path,"w",newline="",encoding="utf-8") as f:
162
+ w = csv.DictWriter(f,fieldnames=["publish_date","publish_time","video_seq","variant_id","video_format","hashtags"])
163
+ w.writeheader()
164
+ for s in schedule:
165
+ w.writerow({"publish_date":s["publish_date"],"publish_time":s["publish_time"],
166
+ "video_seq":s["video_seq"],"variant_id":s["variant_id"],
167
+ "video_format":s["video_format"],"hashtags":s["hashtags"]})
168
+
169
+
170
+ if __name__ == "__main__":
171
+ parser = argparse.ArgumentParser()
172
+ parser.add_argument("--input", required=True)
173
+ parser.add_argument("--output", default="./production")
174
+ args = parser.parse_args()
175
+ with open(args.input,"r",encoding="utf-8") as f: data = json.load(f)
176
+ variants = data.get("variants",[])
177
+ result = generate_production_plan(variants, args.output)
178
+ print(f"\n✅ 生产计划已生成!{result['total']} 条视频")
179
+ print(f" 手册: {result['manual_file']}")
180
+ print(f" 排期: {result['schedule_csv']}")