from __future__ import annotations import re from pathlib import Path from docx import Document from docx.enum.section import WD_SECTION from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT, WD_ROW_HEIGHT_RULE, WD_TABLE_ALIGNMENT from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK, WD_LINE_SPACING from docx.oxml import OxmlElement from docx.oxml.ns import qn from docx.shared import Inches, Pt, RGBColor OUT = Path(r"D:\workspace\digital_me\artifacts\《我为你研墨》音频对齐脚本与分镜提示词.docx") INK = "241C1A" MUTED = "6E625D" CINNABAR = "8B1E1E" GOLD = "B88746" PAPER = "F7F1E7" PALE = "EFE9E1" GRID = "D9D0C6" WHITE = "FFFFFF" BLACK = "000000" FONT = "Microsoft YaHei" SERIF = "SimSun" STYLE_PREFIX = ( "唐代水墨×工笔重彩,2.5D半景深电影感构图;9:16竖屏,1080×1920;" "主色调墨黑、暗红与暖金烛光,冷灰为辅;细腻工笔重彩结合氤氲水墨晕染," "朱砂点睛,胶片颗粒12%,长焦浅景深,24fps电影观感;柔光晕染,主体亮度-1EV;" "烛火为唯一主光源,暖金色温3200K,光线从画面右上方落下,阴影投向左下并保留细节;" "无现代元素,无文字,无水印;画面下沿15%预留字幕安全区;固定机位微动,镜头位移不超过20%。" ) NEGATIVE = ( "现代服装、现代建筑、霓虹灯、塑料质感、欧美面孔、卡通Q版、赛博朋克、过度饱和、纯黑死阴影、" "强烈镜头抖动、快速推拉、广角畸变、人物面部突变、手指畸形、多余肢体、服饰穿模、烛光方向漂移、" "画面文字、字幕、水印、Logo、边框、横屏构图。" ) SCENES = [ dict(n=1, tc="00:00.00–00:14.00", dur="14.00s", part="Intro", frame="图3|掖庭描红的孤女", lyric="你杀了我祖父的那一年 / 我还不会写字", beats=[("00:00.00–00:01.00", "黑场中研墨声先行,画面从黑缓慢显出。"), ("00:01.00–00:04.00", "俯视机位沿高墙缓慢下推,揭示伏案描红的孤女。"), ("00:04.00–00:10.00", "烛光渐亮;女孩笨拙落笔,高墙与长影压住人物。"), ("00:10.00–00:14.00", "推镜放缓并停住,停留在红簪与未写稳的笔画。")], subtitles=[("00:02.00–00:08.00", "你杀了我祖父的那一年"), ("00:08.00–00:14.00", "我还不会写字")], prompt="开场闪回。俯视机位从高耸宫墙顶端缓慢下推约20%,暗蓝粗布囚衣的幼年上官婉儿背对镜头,伏在窄矮木桌前执笔描红;发髻别一支朱砂红簪花,作为画面唯一鲜明暖色。两侧暗红宫墙形成强透视并占画面70%以上,压向中心;墙根孤烛呼吸般摇曳,阴影随火焰轻缓流动。前1秒黑场,前3秒以研墨声为主,第4秒烛光渐亮。女孩落笔笨拙而克制,最后4秒镜头几乎静止,突出“尚不会写字”的脆弱与压迫。", transition="开场黑场1.0s淡入;镜尾以0.8s叠化进入镜2。", split="建议生成两段7s:A段完成俯视下推与人物揭示;B段保持同构图,仅保留落笔、烛火和影子微动。"), dict(n=2, tc="00:14.00–00:30.00", dur="16.00s", part="Verse 1", frame="图13|将尽烛火·人小墙大", lyric="掖庭里长大 / 罪臣的孙女", beats=[("00:14.00–00:18.00", "极缓推进,门洞形高墙逐渐吞没画面边缘。"), ("00:18.00–00:25.00", "镜头停住;人物剪影与将尽红烛形成极端体量反差。"), ("00:25.00–00:30.00", "烛焰变弱、烛泪流下,冷灰空间继续压迫人物。")], subtitles=[("00:15.00–00:22.00", "掖庭里长大"), ("00:22.00–00:30.00", "罪臣的孙女")], prompt="极缓推进约2秒后近乎静止。高耸暗红宫墙围出“门”字形门洞,门洞正中偏下是一支将尽红烛,烛焰仅指甲盖大小;烛旁素衣女子剪影占画面不到8%,人与墙形成极端体量对比。烛泪沿烛身缓缓滑下,在烛台底部凝成暗红蜡堆。整体冷灰偏重,仅烛火周围10%范围保留暖金光晕;让烛焰、烛泪与衣摆承担全部动态,人物始终克制不动。", transition="0.8–1.0s叠化,烛火光斑溶入镜3砚面反光。", split="建议8s+8s同机位续写;第二段用首尾帧延续功能,避免人物比例、门洞结构和烛台位置跳变。"), dict(n=3, tc="00:30.00–00:44.00", dur="14.00s", part="Verse 1", frame="图1|一滴水入砚", lyric="我把恨磨成了墨 / 一滴,不许它响", beats=[("00:30.00–00:33.00", "微距俯拍砚池,水滴在画面上方悬停。"), ("00:33.00–00:36.00", "水滴落入墨心,三圈涟漪与一次极轻画面震颤同步。"), ("00:36.00–00:41.00", "墨气升起至画面三分之一,烛斑在黑镜中虚化。"), ("00:41.00–00:44.00", "墨气散尽,砚面重归寂静。")], subtitles=[("00:32.00–00:37.00", "我把恨磨成了墨"), ("00:37.00–00:44.00", "一滴,不许它响")], prompt="微距俯拍,砚台占画面90%,背景纯黑。水滴从画面上方悬停约2秒后落入墨池正中央;入墨瞬间,以落点为圆心扩散三圈细密涟漪,并产生一次极轻微画面震颤。水雾凝成一缕墨气柱,上升至画面约三分之一后缓慢摇曳消散。墨面如黑镜,映出暖金色焦外烛火光斑;全过程强调静、慢、压抑,水滴的“响”只通过视觉震颤暗示。", transition="1.0s叠化;墨池黑色自然溶入镜4左半脸阴影。", split="建议7s+7s:第一段完成悬滴与落墨;第二段以涟漪首帧衔接,完成墨气升散。"), dict(n=4, tc="00:44.00–00:58.00", dur="14.00s", part="Verse 1", frame="图4|研墨·墨映半脸", lyric="如今我替你,把诏书写得漂亮 / 一笔一划,都是你要的锋芒", beats=[("00:44.00–00:49.00", "中景缓推,女子持续画圆研墨。"), ("00:49.00–00:54.00", "现实半脸与砚中倒影组成对称,墨纹轻晃。"), ("00:54.00–00:58.00", "她停手凝视倒影,睫毛微动,镜头不再前进。")], subtitles=[("00:45.00–00:51.00", "如今我替你,把诏书写得漂亮"), ("00:51.00–00:58.00", "一笔一划,都是你要的锋芒")], prompt="中景缓推约20%。成年上官婉儿低头研墨,发髻插朱砂红簪;右后方暖金烛光照亮右半张脸,左半脸隐入墨色阴影。她手持墨锭缓慢而均匀地画圆,砚中墨面如黑镜,映出模糊半脸,与现实被照亮的半脸形成左右对称——一半真实、一半倒影。墨面倒影随研磨纹理轻晃。第10秒停手,凝视砚中自己,睫毛微动,最后4秒保持压抑静止。", transition="镜尾0.5s压暗,再以0.8s叠化进入祖父面容。", split="7s+7s。A段持续研墨与缓推;B段从同角度继续,动作由研墨减速过渡到停手凝视。"), dict(n=5, tc="00:58.00–01:10.00", dur="12.00s", part="Pre-Chorus 1", frame="图12|砚镜浮祖父面(初现)", lyric="该恨你 / 还是该谢你", beats=[("00:58.00–01:02.00", "中近景缓推;墨面涟漪出现。"), ("01:02.00–01:06.00", "白发祖父的写意面容在砚中浮现,短暂停留。"), ("01:06.00–01:10.00", "面容随墨纹消散,女子瞳孔保留最后一瞬缩影。")], subtitles=[("01:00.00–01:05.00", "该恨你"), ("01:05.00–01:10.00", "还是该谢你")], prompt="中近景缓推约15%。女子俯身凝视砚中墨面,墨面如镜;涟漪从中心荡开后,浮现一位清瘦威严的白发老者写意面容——五官虚化但可辨认为祖父上官仪。面容停留约3秒后随墨纹散成黑色烟雾。右侧烛火轻跳,暖金光晕落在女子侧脸;女子瞳孔中短暂映出祖父面容的微缩倒影。此处情绪是“心结初现”,不可做成惊悚鬼影。", transition="1.0s叠化;墨烟向画面深处延伸为宫廊暗部。", split="可直接生成12s;若平台上限10s,先生成8s主动作,再以4s首尾帧延长完成消散。"), dict(n=6, tc="01:10.00–01:26.00", dur="16.00s", part="Verse 2", frame="图10|搀老帝王(皆背影)", lyric="你老了,手也抖了 / 我扶你上朝", beats=[("01:10.00–01:16.00", "跟随长镜缓推,两人背影进入宫廊纵深。"), ("01:16.00–01:22.00", "帝王袖口微颤、步伐迟缓;女子步伐稳定。"), ("01:22.00–01:26.00", "抵达朝堂暖光前,帝王加重搭臂力道。")], subtitles=[("01:11.00–01:18.00", "你老了,手也抖了"), ("01:18.00–01:26.00", "我扶你上朝")], prompt="幽深唐代宫廊纵深透视,跟随长镜缓慢前推。画面左侧为立髻玄衣的上官婉儿背影,身形挺拔;右侧为身着明黄龙袍的晚年武则天背影,身形微佝偻。婉儿搀扶帝王缓步前行,帝王袖口细微颤动。两侧红柱与灯火缓慢后退,前方朝堂入口暖金光晕渐亮;两人长影投在青砖地面,随烛火轻摆。女子步伐稳,帝王步伐迟缓。第14秒帝王手在女子手臂上加重力道,二人停在光晕边缘。全程不露正脸。", transition="0.5s压暗硬切到掌心特写,让空间尺度骤然收紧。", split="8s+8s同方向运动。第二段必须沿用第一段末帧,保持人物左右位置、衣色、步幅与宫廊透视一致。"), dict(n=7, tc="01:26.00–01:36.00", dur="10.00s", part="Chorus 1|意象过门", frame="图9|掌上起墨风", lyric="(掌上起风 / 婉儿落笔)", beats=[("01:26.00–01:29.00", "女子手掌大特写,掌心墨气聚拢。"), ("01:29.00–01:34.00", "墨气螺旋三圈,速度由慢渐快。"), ("01:34.00–01:36.00", "墨风消散,右上浮出笔尖落纸虚影。")], subtitles=[("01:27.00–01:31.50", "(掌上起风)"), ("01:31.50–01:36.00", "(婉儿落笔)")], prompt="前景大特写一只摊开的女子手掌,占画面约60%。掌心生出黑色墨气漩涡,墨气螺旋上升如风,旋转三圈,速度由慢渐快后消散。另一只手执毛笔,在画面左侧虚化区域蘸墨;右下烛光斜照手指边缘,勾勒暖金边光。墨气消散瞬间,画面右上方隐约浮出笔尖落纸的虚影,完成从“掌上风”到“落笔”的意象连接。手指数量、结构和腕部饰物保持自然稳定。", transition="0.5s快速叠化并卡节拍进入棋盘俯拍。", split="单段10s生成;优先选择手部稳定版本,必要时牺牲墨气复杂度以避免手指畸变。"), dict(n=8, tc="01:36.00–01:48.00", dur="12.00s", part="Chorus 1", frame="图8|黑白棋·两双手", lyric="这盘棋里 / 谁是谁的因果", beats=[("01:36.00–01:42.00", "俯拍微推,两手各持一子悬于棋盘。"), ("01:42.00–01:45.00", "帝王手落黑子,烛火一跳,落点泛墨晕。"), ("01:45.00–01:48.00", "女子手落白子,白梅轻颤,形成回应。")], subtitles=[("01:39.00–01:43.50", "这盘棋里"), ("01:43.50–01:48.00", "谁是谁的因果")], prompt="俯拍青玉围棋盘并极缓推近约10%,棋盘占画面70%。左侧青年女子纤白手腕戴银镯、执白子;右侧老年帝王枯瘦手戴玉扳指、执黑子。棋盘已有零星黑白棋子,暗示对弈已久。第6秒老者手落黑子,烛火同步跳动,落点泛起一圈水墨晕染光纹;第9秒女子手悬停半拍后落下白子,出现同样但更淡的晕染。右下角斜插白梅一枝,随烛光轻颤。两手不得交叉或变形。", transition="0.3s硬切进入镜9,制造“棋局落子—家宅焚毁”的因果冲击。", split="可8s+4s;A段完成对峙与黑子,B段从黑子落定的静帧延续并完成白子。"), dict(n=9, tc="01:48.00–02:00.00", dur="12.00s", part="Pre-Chorus 2", frame="图5|焚宅·递笔(后期分屏)", lyric="该恨你,还是该谢你 / 你毁了我的家,又给了我笔", beats=[("01:48.00–01:54.00", "左屏焚宅、女子剪影不动;右屏手伸向递来的笔。"), ("01:54.00–01:58.00", "中央墨痕下滴,右屏指尖触笔时烛泪落下。"), ("01:58.00–02:00.00", "火焰爆裂与研墨声同步,两屏同时轻震。")], subtitles=[("01:49.00–01:54.00", "该恨你,还是该谢你"), ("01:54.00–02:00.00", "你毁了我的家,又给了我笔")], prompt="后期制作左右对称分屏。左屏:燃烧的唐代宅邸,火焰由下向上舔舐屋檐,飞灰与火星掠过镜头;一名女子剪影立在火前纹丝不动,火光映红半面墙。右屏:烛光下一只年轻女子手缓慢接住画外递来的毛笔,指尖触到笔杆瞬间,一滴烛泪沿烛身滑落。两屏中央以一道竖直墨痕分隔,约第4秒开始向下滴墨,底部晕开。第10秒两屏同时极轻震动,形成火焰爆裂与研墨声的情绪同拍。", transition="镜8至镜9使用0.3s硬切;镜9末端再以0.8s墨色叠化进入砚面。", split="不要让图生视频模型直接生成分屏。分别生成“焚宅”和“递笔”两条12s素材,在剪辑软件中50/50合成并叠加中央墨痕。"), dict(n=10, tc="02:00.00–02:16.00", dur="16.00s", part="Chorus 1|视角升格", frame="图6|砚面墨镜化江山", lyric="我为你研墨,也为自己活 / 仇人的江山,我替她说", beats=[("02:00.00–02:04.00", "俯拍砚面,近景烛火映在墨中。"), ("02:04.00–02:11.00", "镜头升起后拉,砚面变形为水墨宫城与万家灯火。"), ("02:11.00–02:16.00", "全景定住;女子像棋子般立在最高城楼。")], subtitles=[("02:03.00–02:08.00", "我为你研墨,也为自己活"), ("02:08.00–02:16.00", "仇人的江山,我替她说")], prompt="镜头从俯拍砚面开始,前4秒砚中墨面如黑镜映出近景烛火;随后缓慢升起并后拉,墨面自然“化”为水墨宫殿群的夜间俯瞰视角。万家灯火如棋盘星罗,远处宫阙在云雾中隐现。上官婉儿背影立于最高城楼,位于画面上三分之一,剪影仅占画面约5%。镜头升起过程中,近景烛火连续过渡为万家灯火星点;第11秒到达最远景并定格至镜尾,女子独立城楼,既像执笔者也像棋子。", transition="1.2s长叠化进入朱笔微距,宫城灯火收束为朱砂笔尖高光。", split="8s+8s。A段完成砚面与变形起势;B段从宫城初现的末帧继续后拉,完成远景定格。尽量使用首尾帧/运镜延长功能。"), dict(n=11, tc="02:16.00–02:30.00", dur="14.00s", part="Verse 2", frame="图2|朱笔落一道锋", lyric="你不问我恨不恨,只看我写得好不好 / 我把心藏进批注的小字号", beats=[("02:16.00–02:22.00", "笔尖悬停纸面,银镯反光,犹豫被拉长。"), ("02:22.00–02:26.00", "朱笔斜落一道锋,红墨微粒迸溅并晕染。"), ("02:26.00–02:30.00", "笔尖在末端停顿、轻颤,随后抬离纸面。")], subtitles=[("02:17.00–02:23.00", "你不问我恨不恨,只看我写得好不好"), ("02:23.00–02:30.00", "我把心藏进批注的小字号")], prompt="微距特写成年女子执笔右手。蘸饱朱砂红墨的笔尖悬停宣纸上方,右上方烛光暖铺纸面,腕部银镯反射高光。前6秒保持克制悬停,只允许指尖与笔毫轻微颤动;第6秒至第9秒,笔尖从右上向左下落下一道锋利红色斜划,2至3颗红墨微粒从笔锋处迸溅,划痕边缘在宣纸上晕开暗红。落笔后笔尖在末端停顿约2秒,微颤后抬离纸面。确保毛笔结构、手指和朱砂轨迹连续。", transition="0.8–1.0s叠化;朱砂划痕压暗为半开宫门的缝隙。", split="7s+7s。A段专注悬停与犹豫;B段以同一构图完成落笔、停顿和抬笔。"), dict(n=12, tc="02:30.00–02:44.00", dur="14.00s", part="Chorus 2|人物谜面", frame="基准图|人物定调", lyric="你看不透我 / 我也看不透我", beats=[("02:30.00–02:36.00", "静止构图缓推,背影望向半开宫门。"), ("02:36.00–02:39.00", "烛火突然跳动,女子肩头轻颤。"), ("02:39.00–02:44.00", "人物恢复平静,面部始终不可见。")], subtitles=[("02:32.00–02:38.00", "你看不透我"), ("02:38.00–02:44.00", "我也看不透我")], prompt="静止构图中极缓推近约10%。上官婉儿背影立于孤烛旁,发髻别朱砂红簪,长裙拖地;她背对镜头望向半开的宫门,面部全程不可见。右上方暖金烛光勾亮发丝与衣褶边缘,窗外仅见夜色宫阙剪影;烛泪缓慢滑落。第7秒烛火突然跳动一次,女子肩头极轻一颤,随即恢复平静。暗调中保留衣褶与宫门细节,以“始终不见脸”强化彼此与自我的不可看透。", transition="0.5s黑场后进入镜13同一宫廊,使谜面转入秘密。", split="7s+7s同构图;第二段只做烛跳、肩颤和恢复,禁止改变人物站位与宫门开合角度。"), dict(n=13, tc="02:44.00–02:56.00", dur="12.00s", part="Verse 2|秘密浮现", frame="图11|身后血诏虚影", lyric="这一路,我们谁也没提过 / 那道诏", beats=[("02:44.00–02:48.00", "跟随两人背影缓推,地面开始泛红。"), ("02:48.00–02:53.00", "血红诏书虚影展开,两人脚步踏过。"), ("02:53.00–02:56.00", "诏影散为红墨气;女子顿半拍后继续。")], subtitles=[("02:46.00–02:52.00", "这一路,我们谁也没提过"), ("02:52.00–02:56.00", "那道诏")], prompt="与镜6相同的幽深宫廊,但时间更晚、帝王背影更佝偻。跟随长镜缓慢前推,女子与帝王并行前行。青砖地面从二人脚下浮现血红诏书虚影:卷轴缓缓展开,朱砂字迹若隐若现但不可辨读具体内容;脚步踏过诏书虚影。诏影停留约3秒后散为红色墨气,被脚步带起的微风卷走。第10秒女子脚步停顿半拍,似有察觉,随后继续向前。红色只存在于虚影与朱砂,不可污染全片色调。", transition="1.2s叠化;红墨气聚拢成镜14砚面涟漪。", split="可8s+4s,或用10s主素材加2s速度拉伸。续段必须保持两人步向、背影比例和宫廊灯位。"), dict(n=14, tc="02:56.00–03:12.00", dur="16.00s", part="Bridge|情感高潮", frame="图12|砚镜浮祖父面(再现)", lyric="祖父啊,我没替你报仇 / 我活成了比报仇更长的一句咒 / 或者,只是一个想活下去的人", beats=[("02:56.00–03:00.00", "缓推砚面,祖父面容由墨纹中逐渐清晰。"), ("03:00.00–03:05.00", "祖父停留,女子睫毛微动、嘴唇轻颤。"), ("03:05.00–03:08.00", "烛火加剧,烛泪落砚,祖父面容随涟漪化散。"), ("03:08.00–03:12.00", "墨面映出女子自己,与祖父短暂重叠;她缓缓直身,画面转暗。")], subtitles=[("02:57.00–03:02.00", "祖父啊,我没替你报仇"), ("03:02.00–03:07.00", "我活成了比报仇更长的一句咒"), ("03:07.00–03:12.00", "或者,只是一个想活下去的人(30pt,#CCCCCC)")], prompt="中近景缓推约20%,沿用镜5构图但情绪更浓。女子俯身凝视砚中墨面,涟漪荡开后祖父上官仪的白发面容再次浮现;这次更清晰、更温和也更威严,停留约5秒。女子睫毛微动、嘴唇轻颤但不出声。第9秒起烛火摇曳幅度加大,第10秒一滴烛泪落入砚池,涟漪带走祖父面容。面容消散后,墨面映出女子自己的脸,并与祖父轮廓短暂重叠一帧。最后4秒女子缓缓直起身,烛光渐弱,画面渐暗。情绪应是克制后的崩裂与自我承认,不流泪、不嚎哭、不做夸张表演。", transition="镜尾1.0s压黑,接镜15诏书暖黄底;不额外延长总时长。", split="强烈建议8s+8s并生成多版。A段完成祖父清晰出现与人物反应;B段从稳定面容末帧延续,完成烛泪、消散、重叠与直身。"), dict(n=15, tc="03:12.00–03:19.15", dur="7.15s", part="Outro", frame="图7|墨字生宫阙", lyric="墨干了,诏成了 / 风还在掌上", beats=[("03:12.00–03:14.00", "俯拍明黄诏书,墨字晕开。"), ("03:14.00–03:17.00", "笔画离纸化成微缩宫阙,镜头随之升起后拉。"), ("03:17.00–03:19.15", "宫城灯火逐盏熄灭,只剩城楼剪影与一盏灯,尾音归黑。")], subtitles=[("03:13.00–03:17.00", "墨干了,诏成了"), ("03:17.00–03:19.15", "风还在掌上")], prompt="从俯拍明黄诏书开始,前2秒墨字在纸上如水墨缓缓晕开;随后笔画脱离纸面,化为微缩宫阙楼阁并以2.5D方式升起。镜头随墨字上升并后拉,俯瞰夜色大明宫灯火如棋盘,与镜10形成闭环;上官婉儿背影独立最高城楼。第5秒到达远景并定格,最后2.15秒极缓变暗:万家灯火渐次熄灭,仅余城楼剪影与画面正中偏下的一盏灯,最后一盏灯随研墨声尾音熄灭,全黑结束。", transition="03:19.15全黑结束;保留最后0.15s音频尾音,不再追加片尾卡。", split="单段7.15s生成。生成时不加入文字;字幕与全黑尾帧均在剪辑软件中完成。"), ] def set_run_font(run, size=None, bold=None, color=None, name=FONT, italic=None): run.font.name = name run._element.get_or_add_rPr().rFonts.set(qn("w:ascii"), name) run._element.get_or_add_rPr().rFonts.set(qn("w:hAnsi"), name) run._element.get_or_add_rPr().rFonts.set(qn("w:eastAsia"), name) if size is not None: run.font.size = Pt(size) if bold is not None: run.bold = bold if italic is not None: run.italic = italic if color is not None: run.font.color.rgb = RGBColor.from_string(color) def shade_paragraph(paragraph, fill, border=None): ppr = paragraph._p.get_or_add_pPr() shd = ppr.find(qn("w:shd")) if shd is None: shd = OxmlElement("w:shd") ppr.append(shd) shd.set(qn("w:fill"), fill) if border: pbdr = OxmlElement("w:pBdr") left = OxmlElement("w:left") left.set(qn("w:val"), "single") left.set(qn("w:sz"), "18") left.set(qn("w:space"), "8") left.set(qn("w:color"), border) pbdr.append(left) ppr.append(pbdr) def set_cell_shading(cell, fill): tcpr = cell._tc.get_or_add_tcPr() shd = tcpr.find(qn("w:shd")) if shd is None: shd = OxmlElement("w:shd") tcpr.append(shd) shd.set(qn("w:fill"), fill) def set_cell_margins(cell, top=80, start=120, bottom=80, end=120): tcpr = cell._tc.get_or_add_tcPr() mar = tcpr.first_child_found_in("w:tcMar") if mar is None: mar = OxmlElement("w:tcMar") tcpr.append(mar) for tag, value in (("top", top), ("start", start), ("bottom", bottom), ("end", end)): node = mar.find(qn(f"w:{tag}")) if node is None: node = OxmlElement(f"w:{tag}") mar.append(node) node.set(qn("w:w"), str(value)) node.set(qn("w:type"), "dxa") def set_table_geometry(table, widths, indent=120): table.autofit = False table.alignment = WD_TABLE_ALIGNMENT.LEFT tblpr = table._tbl.tblPr tblw = tblpr.find(qn("w:tblW")) if tblw is None: tblw = OxmlElement("w:tblW") tblpr.append(tblw) tblw.set(qn("w:w"), str(sum(widths))) tblw.set(qn("w:type"), "dxa") tblind = tblpr.find(qn("w:tblInd")) if tblind is None: tblind = OxmlElement("w:tblInd") tblpr.append(tblind) tblind.set(qn("w:w"), str(indent)) tblind.set(qn("w:type"), "dxa") grid = table._tbl.tblGrid for child in list(grid): grid.remove(child) for width in widths: col = OxmlElement("w:gridCol") col.set(qn("w:w"), str(width)) grid.append(col) for row in table.rows: row.height_rule = WD_ROW_HEIGHT_RULE.AT_LEAST for idx, cell in enumerate(row.cells): tcw = cell._tc.get_or_add_tcPr().find(qn("w:tcW")) if tcw is None: tcw = OxmlElement("w:tcW") cell._tc.get_or_add_tcPr().append(tcw) tcw.set(qn("w:w"), str(widths[idx])) tcw.set(qn("w:type"), "dxa") cell.width = Inches(widths[idx] / 1440) cell.vertical_alignment = WD_CELL_VERTICAL_ALIGNMENT.CENTER set_cell_margins(cell) def set_repeat_table_header(row): trpr = row._tr.get_or_add_trPr() tbl_header = OxmlElement("w:tblHeader") tbl_header.set(qn("w:val"), "true") trpr.append(tbl_header) def add_page_field(paragraph): paragraph.alignment = WD_ALIGN_PARAGRAPH.RIGHT run = paragraph.add_run("第 ") set_run_font(run, size=8.5, color=MUTED) fld_begin = OxmlElement("w:fldChar") fld_begin.set(qn("w:fldCharType"), "begin") instr = OxmlElement("w:instrText") instr.set(qn("xml:space"), "preserve") instr.text = " PAGE " fld_end = OxmlElement("w:fldChar") fld_end.set(qn("w:fldCharType"), "end") run._r.extend([fld_begin, instr, fld_end]) tail = paragraph.add_run(" 页") set_run_font(tail, size=8.5, color=MUTED) def add_label_para(doc, label, text, after=5, shade=None): p = doc.add_paragraph() p.paragraph_format.space_before = Pt(0) p.paragraph_format.space_after = Pt(after) p.paragraph_format.line_spacing = 1.25 if shade: p.paragraph_format.left_indent = Inches(0.12) p.paragraph_format.right_indent = Inches(0.10) p.paragraph_format.space_before = Pt(5) p.paragraph_format.space_after = Pt(7) shade_paragraph(p, shade, CINNABAR) r = p.add_run(label) set_run_font(r, size=10.2, bold=True, color=CINNABAR) r2 = p.add_run(text) set_run_font(r2, size=10.2, color=INK) return p def add_heading(doc, text, level): p = doc.add_paragraph(style=f"Heading {level}") p.paragraph_format.keep_with_next = True r = p.add_run(text) return p def add_prompt(doc, prompt): p = doc.add_paragraph() p.paragraph_format.left_indent = Inches(0.16) p.paragraph_format.right_indent = Inches(0.12) p.paragraph_format.space_before = Pt(3) p.paragraph_format.space_after = Pt(9) p.paragraph_format.line_spacing = 1.22 shade_paragraph(p, PAPER, GOLD) r = p.add_run(prompt) set_run_font(r, size=9.4, color=INK) return p def parse_timecode(value): minutes, seconds = value.split(":") return int(minutes) * 60 + float(seconds) def format_seconds(value): text = f"{value:.2f}" return text.rstrip("0").rstrip(".") def scene_duration(s): return float(s["dur"].removesuffix("s")) def clean_original_prompt_timing(text): # The reference prompts mixed 12–16 second cues with generator timing. # Remove those cue numbers before adding a single authoritative 10-second schedule. text = re.sub(r"第\d+(?:\.\d+)?秒(?:至第\d+(?:\.\d+)?秒)?", "随后", text) text = re.sub(r"前\d+(?:\.\d+)?秒", "开头阶段", text) text = re.sub(r"最后\d+(?:\.\d+)?秒", "结尾阶段", text) text = re.sub(r"停留约\d+(?:\.\d+)?秒", "短暂停留", text) text = re.sub(r"约\d+(?:\.\d+)?秒", "短暂", text) text = re.sub(r"\d+(?:\.\d+)?秒", "短暂", text) return text.replace("约短暂", "短暂").replace("随后至随后", "随后") def make_10s_prompt(s): scene_start = parse_timecode(s["tc"].split("–")[0]) duration = scene_duration(s) scale = 10.0 / duration schedule = [] for beat_range, action in s["beats"]: beat_start_text, beat_end_text = beat_range.split("–") beat_start = max(0.0, parse_timecode(beat_start_text) - scene_start) beat_end = min(duration, parse_timecode(beat_end_text) - scene_start) scaled_start = beat_start * scale scaled_end = beat_end * scale schedule.append( f"第{format_seconds(scaled_start)}–{format_seconds(scaled_end)}秒:{action}" ) visual = clean_original_prompt_timing(s["prompt"]) return ( STYLE_PREFIX + "生成时长严格10秒,首尾均为可剪辑的稳定画面,不生成字幕。" + "画面与动作总设定:" + visual + "以下10秒动作节拍为唯一时间指令:" + ";".join(schedule) + "。所有关键动作必须在第10秒前完成,动作连续、速度平缓,不突然跳帧。" ) def speed_mapping(s): duration = scene_duration(s) speed = 10.0 / duration * 100.0 if abs(duration - 10.0) < 0.001: return "10秒生成素材以100%速度完整使用,直接占满本镜10秒。" if duration > 10: return ( f"本镜成片目标{format_seconds(duration)}秒:将10秒生成素材设为{speed:.2f}%速度," f"等比拉伸至{format_seconds(duration)}秒;开启光流或帧混合,字幕仍按绝对时间码放置。" ) return ( f"本镜成片目标{format_seconds(duration)}秒:将10秒生成素材设为{speed:.2f}%速度," f"等比压缩至{format_seconds(duration)}秒;镜15须保留最终全黑与0.15秒音频尾音。" ) def configure_styles(doc): styles = doc.styles normal = styles["Normal"] normal.font.name = FONT normal._element.rPr.rFonts.set(qn("w:ascii"), FONT) normal._element.rPr.rFonts.set(qn("w:hAnsi"), FONT) normal._element.rPr.rFonts.set(qn("w:eastAsia"), FONT) normal.font.size = Pt(10.5) normal.font.color.rgb = RGBColor.from_string(INK) normal.paragraph_format.space_before = Pt(0) normal.paragraph_format.space_after = Pt(6) normal.paragraph_format.line_spacing = 1.25 for level, size, before, after, color in [ (1, 16, 18, 10, CINNABAR), (2, 13, 14, 7, INK), (3, 11.5, 10, 5, GOLD), ]: st = styles[f"Heading {level}"] st.font.name = FONT st._element.rPr.rFonts.set(qn("w:ascii"), FONT) st._element.rPr.rFonts.set(qn("w:hAnsi"), FONT) st._element.rPr.rFonts.set(qn("w:eastAsia"), FONT) st.font.size = Pt(size) st.font.bold = True st.font.color.rgb = RGBColor.from_string(color) st.paragraph_format.space_before = Pt(before) st.paragraph_format.space_after = Pt(after) st.paragraph_format.keep_with_next = True for st_name in ("Header", "Footer"): st = styles[st_name] st.font.name = FONT st._element.rPr.rFonts.set(qn("w:eastAsia"), FONT) st.font.size = Pt(8.5) st.font.color.rgb = RGBColor.from_string(MUTED) def add_cover(doc): p = doc.add_paragraph() p.paragraph_format.space_before = Pt(62) p.paragraph_format.space_after = Pt(16) p.alignment = WD_ALIGN_PARAGRAPH.CENTER r = p.add_run("上官婉儿主题音乐视频") set_run_font(r, size=11, bold=True, color=GOLD) p = doc.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER p.paragraph_format.space_after = Pt(8) r = p.add_run("《我为你研墨》") set_run_font(r, size=30, bold=True, color=INK, name=SERIF) p = doc.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER p.paragraph_format.space_after = Pt(28) r = p.add_run("音频对齐脚本与分镜头提示词") set_run_font(r, size=17, color=CINNABAR) p = doc.add_paragraph() p.alignment = WD_ALIGN_PARAGRAPH.CENTER p.paragraph_format.space_after = Pt(36) r = p.add_run("15 镜 · 15 条固定 10 秒提示词 · 199.152 秒音频成片") set_run_font(r, size=10.5, color=MUTED) table = doc.add_table(rows=6, cols=2) set_table_geometry(table, [1900, 7460], indent=120) rows = [ ("音频", "我为你研墨(1).mp3"), ("实测", "03:19.15|48 kHz|立体声|约 184 kbps VBR"), ("叙事", "不会写字的孤女 → 替敌研墨写诏的女官 → 活成比报仇更长的一句咒"), ("输出", "1080×1920|H.264|30fps|8–12 Mbps"), ("版本", "音频对齐执行版|2026-08-11"), ] set_repeat_table_header(table.rows[0]) for j, text in enumerate(("项目", "制作参数")): set_cell_shading(table.cell(0, j), INK) p = table.cell(0, j).paragraphs[0] p.paragraph_format.space_after = Pt(0) r = p.add_run(text) set_run_font(r, size=9.2, bold=True, color=WHITE) for idx, (label, value) in enumerate(rows, start=1): set_cell_shading(table.cell(idx, 0), PALE) for j, text in enumerate((label, value)): p = table.cell(idx, j).paragraphs[0] p.paragraph_format.space_after = Pt(0) p.paragraph_format.line_spacing = 1.15 r = p.add_run(text) set_run_font(r, size=9.6, bold=(j == 0), color=CINNABAR if j == 0 else INK) p = doc.add_paragraph() p.paragraph_format.space_before = Pt(28) p.alignment = WD_ALIGN_PARAGRAPH.CENTER r = p.add_run("制作原则:每条图生视频提示词严格生成10秒,再通过等比变速映射到源音频时间轴;转场不额外延长成片。") set_run_font(r, size=9.5, italic=True, color=MUTED) doc.add_page_break() def add_front_matter(doc): add_heading(doc, "1. 执行摘要", 1) add_label_para(doc, "交付定位|", "本稿把源音频、完整歌词版视觉构想与音频对齐版时间表合并为一份可执行的导演脚本。15个叙事镜头各对应1条固定10秒图生视频提示词,并给出10秒素材到绝对音频时间轴的变速映射。") add_label_para(doc, "音频校验|", "源文件实测 199.152 秒(03:19.15),48 kHz、立体声、约184 kbps VBR。参考稿按199秒规划的镜15末端延长0.15秒,用于保留研墨声/混响尾音。") add_label_para(doc, "时间规则|", "本稿所有区间采用“起点含、终点不含”的剪辑时间码;字幕入出点为执行起稿,最终应在剪映或 Premiere 的波形上按实际咬字前后微调 2–4 帧。转场叠化覆盖相邻素材,不额外增加总片长。") add_label_para(doc, "生成规则|", "每条提示词的生成时长统一为10秒。提示词内的动作节拍已按原镜头时长等比压缩到0–10秒;生成后再按各镜“10秒素材映射”设置速度,即可恢复到199.152秒音频时间轴。") add_heading(doc, "2. 统一视觉与声音基线", 1) add_heading(doc, "2.1 完整统一风格前缀", 2) p = doc.add_paragraph() p.paragraph_format.left_indent = Inches(0.16) p.paragraph_format.right_indent = Inches(0.12) p.paragraph_format.space_after = Pt(9) p.paragraph_format.line_spacing = 1.22 shade_paragraph(p, PAPER, GOLD) r = p.add_run(STYLE_PREFIX) set_run_font(r, size=9.6, color=INK) add_heading(doc, "2.2 全片负面提示词", 2) p = doc.add_paragraph() p.paragraph_format.left_indent = Inches(0.16) p.paragraph_format.right_indent = Inches(0.12) p.paragraph_format.space_after = Pt(9) shade_paragraph(p, "F4EEEE", CINNABAR) r = p.add_run(NEGATIVE) set_run_font(r, size=9.6, color=INK) add_heading(doc, "2.3 字幕、音效与导出", 2) table = doc.add_table(rows=7, cols=2) set_table_geometry(table, [2100, 7260]) specs = [ ("字幕字体", "思源宋体 Heavy / 方正粗宋;主歌词36pt,括号歌词28pt,Bridge第三句30pt"), ("字幕样式", "白色#FFFFFF,黑色描边1.5px;Bridge第三句淡灰#CCCCCC;淡入淡出0.3s"), ("字幕位置", "画面居中下方12%–15%安全区;不写入生成画面,统一后期添加"), ("环境音", "开场与结尾保留研墨声;落墨、落子、火焰爆裂、烛泪等按画面轻量点缀"), ("混音", "原曲全程不断;Bridge可将人声相对压低约2dB,突出古琴独白感"), ("导出", "1080×1920,H.264,30fps,8–12Mbps;成片精确至03:19.15"), ] set_repeat_table_header(table.rows[0]) for j, text in enumerate(("项目", "制作规范")): set_cell_shading(table.cell(0, j), INK) p = table.cell(0, j).paragraphs[0] p.paragraph_format.space_after = Pt(0) r = p.add_run(text) set_run_font(r, size=9.2, bold=True, color=WHITE) for idx, (label, value) in enumerate(specs, start=1): set_cell_shading(table.cell(idx, 0), PALE) for j, text in enumerate((label, value)): p = table.cell(idx, j).paragraphs[0] p.paragraph_format.space_after = Pt(0) r = p.add_run(text) set_run_font(r, size=9.4, bold=(j == 0), color=CINNABAR if j == 0 else INK) add_heading(doc, "3. 15镜音频时间轴与10秒生成规则速览", 1) table = doc.add_table(rows=1, cols=5) widths = [540, 1550, 760, 2730, 3780] set_table_geometry(table, widths) set_repeat_table_header(table.rows[0]) headers = ["镜", "时间码", "成片/生成", "画面", "对应歌词"] for idx, text in enumerate(headers): set_cell_shading(table.cell(0, idx), INK) p = table.cell(0, idx).paragraphs[0] p.alignment = WD_ALIGN_PARAGRAPH.CENTER p.paragraph_format.space_after = Pt(0) r = p.add_run(text) set_run_font(r, size=8.8, bold=True, color=WHITE) for s in SCENES: cells = table.add_row().cells values = [str(s["n"]), s["tc"].replace(".00", ""), s["dur"] + "\n10s", s["frame"], s["lyric"].replace(" / ", "\n")] for idx, text in enumerate(values): p = cells[idx].paragraphs[0] p.paragraph_format.space_after = Pt(0) p.paragraph_format.line_spacing = 1.05 p.alignment = WD_ALIGN_PARAGRAPH.CENTER if idx < 3 else WD_ALIGN_PARAGRAPH.LEFT for line_i, line in enumerate(text.split("\n")): if line_i: p.add_run().add_break() r = p.add_run(line) set_run_font(r, size=8.4, color=INK, bold=(idx == 0)) if s["n"] % 2 == 0: set_cell_shading(cells[idx], "FBF8F4") set_table_geometry(table, widths) add_label_para(doc, "总时长|", "生成素材共15条×10秒;剪辑后成片为199.152秒。镜1之前的1秒黑场包含在镜1内,镜15最后0.15秒保留源音频尾音。", after=8, shade=PAPER) doc.add_page_break() def add_scene(doc, s): h = doc.add_paragraph(style="Heading 1") h.paragraph_format.page_break_before = True h.paragraph_format.keep_with_next = True r = h.add_run(f"镜 {s['n']:02d}|{s['frame'].split('|')[-1]}") set_run_font(r, size=16, bold=True, color=CINNABAR) p = doc.add_paragraph() p.paragraph_format.space_after = Pt(8) p.paragraph_format.keep_with_next = True shade_paragraph(p, PALE, GOLD) items = [("时间码", s["tc"]), ("成片", s["dur"]), ("生成", "10.00s"), ("段落", s["part"]), ("参考图", s["frame"])] for idx, (label, value) in enumerate(items): if idx: sep = p.add_run(" | ") set_run_font(sep, size=8.8, color=GOLD) lr = p.add_run(label + " ") set_run_font(lr, size=8.8, bold=True, color=CINNABAR) vr = p.add_run(value) set_run_font(vr, size=8.8, color=INK) add_label_para(doc, "歌词|", s["lyric"], after=9, shade=PAPER) add_heading(doc, "导演脚本与镜内节拍", 2) for timecode, action in s["beats"]: add_label_para(doc, timecode + "|", action, after=4) add_heading(doc, "字幕与剪辑", 2) for timecode, line in s["subtitles"]: add_label_para(doc, timecode + "|", line, after=4) add_label_para(doc, "转场|", s["transition"], after=7) add_heading(doc, "10秒图生视频提示词(可直接复制)", 2) add_prompt(doc, make_10s_prompt(s)) add_heading(doc, "10秒素材映射到音频时间轴", 3) add_label_para(doc, "变速|", speed_mapping(s), after=7) if s["n"] == 9: add_label_para(doc, "分屏|", "左侧焚宅与右侧递笔各生成一条10秒素材,二者使用同一10秒动作节拍;后期分别做相同变速,再以50/50画面合成。", after=7) else: add_label_para(doc, "衔接|", "优先使用10秒素材的完整动作弧线;变速后如需叠化,叠化区覆盖在本镜首尾稳定画面内,不改变主切点。", after=7) add_label_para(doc, "统一负面提示|", NEGATIVE, after=7, shade="F4EEEE") def add_workflow(doc): h = doc.add_paragraph(style="Heading 1") h.paragraph_format.page_break_before = True r = h.add_run("4. 合成与验收清单") set_run_font(r, size=16, bold=True, color=CINNABAR) add_heading(doc, "4.1 合成顺序", 2) steps = [ ("01|固定10秒生成", "将15条“10秒图生视频提示词”分别送入模型,每条生成时长严格选择10秒;单镜至少保留2–3个备选。"), ("02|镜9分屏", "焚宅与递笔左右两侧各生成一条10秒素材,使用同一动作节拍;后期各占画面50%,中央叠加竖直墨痕。"), ("03|精确铺轨", "先锁定03:19.15音频,再按每镜“10秒素材映射”设置速度并放入绝对时间码;变速后镜尾必须与下一镜切点完全重合。"), ("04|字幕", "字幕独立成轨,不写入AI生成画面;按入出点初排后,以源音频咬字逐帧微调。"), ("05|转场", "叠化覆盖相邻素材,不额外占时;镜8到镜9使用0.3秒硬切,镜14到镜15先压黑再进入暖黄诏书。"), ("06|统一调色", "检查15镜烛光方向、肤色、暗红饱和度、墨黑层次与暖金色温;避免镜间亮度跳变。"), ("07|导出", "1080×1920、H.264、30fps、8–12Mbps;音频不得截断,尾帧在03:19.15归黑。"), ] for label, text in steps: add_label_para(doc, label + " ", text, after=6) add_heading(doc, "4.2 成片验收", 2) checks = [ ("时长", "每条生成素材原始长度均为10.00秒;变速剪辑后总时长精确为03:19.15,转场未增加片长。"), ("人物", "上官婉儿发髻、朱砂簪、服饰与年龄阶段连续;武则天仅在指定镜头出现且均为背影/手部。"), ("光色", "主光始终从右上方落下;墨黑保留层次,暗红与朱砂不过饱和,暖金烛光约3200K。"), ("画面", "无现代元素、文字、水印、Logo、手指畸形、面部突变、穿模、色漂或异常镜头抖动。"), ("字幕", "下沿12%–15%安全区内清晰可读,不遮挡手、砚、棋子与人物关键表情;Bridge第三句样式更轻。"), ("叙事", "镜5与镜14同图不同情绪层级清楚;镜10与镜15形成江山意象闭环;镜9的恨/谢对照一眼可辨。"), ("声音", "原曲全程不断,环境音不过度抢人声,结尾研墨声与尾音自然归黑。"), ] table = doc.add_table(rows=1, cols=2) set_table_geometry(table, [1800, 7560]) set_repeat_table_header(table.rows[0]) for idx, text in enumerate(("验收项", "通过标准")): set_cell_shading(table.cell(0, idx), INK) p = table.cell(0, idx).paragraphs[0] p.paragraph_format.space_after = Pt(0) r = p.add_run(text) set_run_font(r, size=9.2, bold=True, color=WHITE) for label, value in checks: cells = table.add_row().cells set_cell_shading(cells[0], PALE) for idx, text in enumerate((label, value)): p = cells[idx].paragraphs[0] p.paragraph_format.space_after = Pt(0) r = p.add_run(text) set_run_font(r, size=9.2, bold=(idx == 0), color=CINNABAR if idx == 0 else INK) set_table_geometry(table, [1800, 7560]) add_heading(doc, "4.3 参考与修订说明", 2) add_label_para(doc, "参考文件|", "《我为你研墨》图生视频提示词(完整歌词版)与《我为你研墨》完整视频提示词(音频对齐版)。") add_label_para(doc, "本版修订|", "独立复核音频长度为199.152秒;将15个镜头提示词全部统一为严格10秒,并把原成片节拍等比压缩进0–10秒;每镜新增变速百分比,使10秒素材能重新映射到原绝对时间轴。镜9左右两侧各生成10秒后再做分屏。") add_label_para(doc, "使用提示|", "如最终歌词演唱咬字与本稿字幕入出点存在细微差异,以音频波形为准做2–4帧微调,但不要改变15镜主切点,除非同时复核全片叙事节奏。") def build(): OUT.parent.mkdir(parents=True, exist_ok=True) doc = Document() sec = doc.sections[0] sec.page_width = Inches(8.5) sec.page_height = Inches(11) sec.top_margin = Inches(1) sec.bottom_margin = Inches(1) sec.left_margin = Inches(1) sec.right_margin = Inches(1) sec.header_distance = Inches(0.492) sec.footer_distance = Inches(0.492) sec.different_first_page_header_footer = True configure_styles(doc) header = sec.header hp = header.paragraphs[0] hp.alignment = WD_ALIGN_PARAGRAPH.LEFT r = hp.add_run("《我为你研墨》|音频对齐脚本与分镜提示词") set_run_font(r, size=8.5, color=MUTED) footer = sec.footer add_page_field(footer.paragraphs[0]) core = doc.core_properties core.title = "《我为你研墨》音频对齐脚本与分镜头提示词" core.subject = "上官婉儿主题音乐视频制作执行稿" core.author = "" core.keywords = "我为你研墨, 上官婉儿, MV, 分镜, 图生视频, 音频对齐" core.comments = "依据源音频与两份Markdown参考稿整理。" add_cover(doc) add_front_matter(doc) for s in SCENES: add_scene(doc, s) add_workflow(doc) doc.save(OUT) print(OUT) if __name__ == "__main__": build()