camera_tree.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import os
  2. import asyncio
  3. import json
  4. from typing import Optional, Dict, Any, List
  5. from taskflow import FileIOHandler
  6. from api_modules.ark_client_async import AsyncArkClient
  7. from api_modules.ark_client import ArkMessage, APIError
  8. from taskflow import get_logger
  9. io_handler = FileIOHandler()
  10. logger = get_logger("examples.video_create.mcps.camera_tree")
  11. system_prompt_camera_tree = \
  12. """
  13. [角色]
  14. 你是一位专业的视频剪辑专家,擅长多机位镜头分析与场景结构建模。你深谙影视语言,能够理解景别(如全景、中景、特写)与内容包含关系,并能根据镜头描述推断机位间的层级结构。
  15. [任务]
  16. 你的任务是分析输入的机位数据,构建"机位树"。该树状结构表示父机位内容包含子机位内容的关系。具体而言,你需要为每个机位识别其父机位(若存在),并确定依赖镜头索引(即父机位素材中包含子机位内容的具体镜头)。若某机位无父机位,则输出None。
  17. [输入]
  18. 输入为一系列机位数据,序列由<CAMERA_SEQ>和</CAMERA_SEQ>包裹。
  19. 每个机位包含该机位拍摄的镜头序列,由<CAMERA_N>和</CAMERA_N>包裹,其中N为机位索引。
  20. 以下为输入格式示例:
  21. <CAMERA_SEQ>
  22. <CAMERA_0>
  23. shot 0:街道中景。爱丽丝和鲍勃相向而行。
  24. shot 2:街道中景。爱丽丝和鲍勃相拥。
  25. </CAMERA_0>
  26. <CAMERA_1>
  27. shot 1:爱丽丝面部特写。她认出鲍勃时表情从惊讶转为欣喜。
  28. </CAMERA_1>
  29. </CAMERA_SEQ>
  30. [输出]
  31. 严格遵循以下JSON格式输出:
  32. ```json
  33. {
  34. "camera_tree": [
  35. {
  36. "parent_cam_idx": // 父机位的索引。如果机位没有父级(例如根机位),则设置为**None**。例如:0、1、None。
  37. "parent_shot_idx": // 依赖镜头的索引。如果机位没有父级(例如根机位),则设置为**None**。例如:0、3、None。
  38. "reason": // 选择父机位的原因。如果机位没有父级,应解释为什么它是根机位。例如:父机位的视野涵盖了子机位的视野(从中景到特写)
  39. "is_parent_fully_covers_child": // 父机位是否完全覆盖子机位的内容。如果机位没有父级,则设置为**None**。例如:True、False、None。
  40. "missing_info": // 子镜头中父镜头未涵盖的缺失元素。如果父镜头完全覆盖子镜头,则设置为**None**。例如:罗宇尘的正面视角、None。
  41. },
  42. // 更多机位的父机位信息
  43. ]
  44. }
  45. ```
  46. [要求]
  47. - 所有输出值(不包括键名)的语言必须与输入语言保持一致。
  48. - 内容包容性检查:父机位应尽可能在特定画面中完全包含子机位内容(例如父中景双人镜头应涵盖子过肩反打镜头)。通过对比关键词(如角色、动作、场景)分析镜头描述,确保父镜头视场能覆盖子镜头。
  49. - 过渡流畅度优先:优先选择更大景别作为父机位,例如全景→中景或中景→特写。相邻父子节点的景别差异应尽可能小,严禁直接从远景跳切到特写(除非绝对必要)。
  50. - 时间邻近性:每个机位由其对应的首个画面描述确定父机位位置,父机位的画面索引应尽可能接近子机位的首个画面索引。
  51. - 逻辑一致性:机位树必须无环,避免循环依赖。若某镜头被多个潜在父机位包含,则选择最佳匹配(基于景别和内容)。若无合适父机位则输出None。
  52. - 当缺乏更广视角时,选择视场重叠最大的镜头作为父镜头(信息重合度最高者),或正反打镜头可互为父子。当两个机位可互为父子时,索引较小者作为索引较大者的父机位。
  53. - 仅允许存在一个无父机位的根机位。
  54. - 描述镜头缺失元素时,需仔细比对父子镜头细节。例如父镜头是角色A与B侧身相对的中景,子镜头是角色A的正脸特写时,需注明子镜头缺失角色A的正面视角信息。
  55. - 首个机位必须作为机位树的根节点。
  56. - **camera_tree**中每个元素代表一个机位的父机位信息;如果机位没有父级(例如根机位),则设置为None。列表的长度应与机位数量相同。
  57. """
  58. human_prompt_camera_tree = \
  59. """
  60. <CAMERA_SEQ>
  61. {camera_seq_str}
  62. </CAMERA_SEQ>
  63. """
  64. async def create_camera_tree(
  65. client: AsyncArkClient,
  66. storyboard: Dict,
  67. ) -> Dict[str, Any]:
  68. cameras = []
  69. for shot in storyboard["storyboard"]:
  70. if shot["cam_idx"] not in [camera["idx"] for camera in cameras]:
  71. cameras.append({"idx": shot["cam_idx"], "active_shot_idxs": [shot["idx"]]})
  72. else:
  73. cameras[shot["cam_idx"]]["active_shot_idxs"].append(shot["idx"])
  74. camera_seq_str = ""
  75. for cam in cameras:
  76. camera_seq_str += f"<CAMERA_{cam['idx']}>\n"
  77. for shot_idx in cam["active_shot_idxs"]:
  78. camera_seq_str += f"Shot {shot_idx}: {storyboard['storyboard'][shot_idx]['visual_desc']}\n"
  79. camera_seq_str += f"</CAMERA_{cam['idx']}>\n"
  80. user_prompt = human_prompt_camera_tree.format(camera_seq_str=camera_seq_str)
  81. user_message = ArkMessage(role="user")
  82. user_message.add_text(user_prompt)
  83. try:
  84. response = await client.chat(
  85. model="doubao-seed-1-6-251015",
  86. messages=[user_message],
  87. system_prompt=system_prompt_camera_tree,
  88. )
  89. logger.info(f"创建机位树成功")
  90. camera_tree = client.get_response_text(response)
  91. camera_tree = io_handler.string_to_json(camera_tree)
  92. for idx, item in enumerate(camera_tree["camera_tree"]):
  93. item["active_shot_idxs"] = cameras[idx]["active_shot_idxs"]
  94. storyboard |= camera_tree
  95. return storyboard
  96. except APIError as e:
  97. logger.error(f"API错误: {e}")
  98. raise e
  99. if __name__ == "__main__":
  100. json_file = "./output/run_20251215_164241/step4_storyboard.json"
  101. with open(json_file, "r", encoding="utf-8") as f:
  102. shot_descriptions = json.load(f)
  103. shot = shot_descriptions["storyboard"][0]
  104. async def main():
  105. async with AsyncArkClient() as client:
  106. camera_tree = await create_camera_tree(client=client, storyboard=shot)
  107. print(camera_tree)
  108. asyncio.run(main())