返回 ViMax
camera_image_generator.py
根目录 / agents / camera_image_generator.py
1 import os
2 import logging
3 import cv2
4 from typing import List, Tuple, Union, Optional
5 from pydantic import BaseModel, Field
6 from tenacity import retry, stop_after_attempt
7 from langchain_core.messages import HumanMessage, SystemMessage
8 from langchain_core.output_parsers import PydanticOutputParser
9 from utils.robust_json_parser import TrailingCommaTolerantPydanticOutputParser as PydanticOutputParser
10 from scenedetect import open_video, SceneManager, split_video_ffmpeg
11 from scenedetect.detectors import ContentDetector
12
13 from interfaces import ShotDescription, ShotBriefDescription, Camera, ImageOutput, VideoOutput
14
15
16 from moviepy import VideoFileClip
17 from PIL import Image
18
19
20 system_prompt_template_select_reference_camera = \
21 """
22 [Role]
23 You are a professional video editing expert specializing in multi-camera shot analysis and scene structure modeling. You have deep knowledge of cinematic language, enabling you to understand shot sizes (e.g., wide shot, medium shot, close-up) and content inclusion relationships. You can infer hierarchical structures between camera positions based on corresponding shot descriptions.
24
25 [Task]
26 Your task is to analyze the input camera position data to construct a "camera position tree". This tree structure represents a relationship where a parent camera's content encompasses that of a child camera. Specifically, you need to identify the parent camera for each camera position (if one exists) and determine the dependent shot indices (i.e., the specific shots within the parent camera's footage that contain the child camera's content). If a camera position has no parent, output None.
27
28 [Input]
29 The input is a sequence of cameras. The sequence will be enclosed within <CAMERA_SEQ> and </CAMERA_SEQ>.
30 Each camera contains a sequence of shots filmed by the camera, which will be enclosed within <CAMERA_N> and </CAMERA_N>, where N is the index of the camera.
31
32 Below is an example of the input format:
33
34 <CAMERA_SEQ>
35 <CAMERA_0>
36 Shot 0: Medium shot of the street. Alice and Bob are walking towards each other.
37 Shot 2: Medium shot of the street. Alice and Bob hug each other.
38 </CAMERA_0>
39 <CAMERA_1>
40 Shot 1: Close-up of the Alice's face. Her expression shifts from surprise to delight as she recognizes Bob.
41 </CAMERA_1>
42 </CAMERA_SEQ>
43
44
45 [Output]
46 {format_instructions}
47
48 [Guidelines]
49 - The language of all output values (not include keys) should be consistent with the language of the input.
50 - Content Inclusion Check: The parent camera should as fully as possible contain the child camera's content in certain shots (e.g., a parent medium two-shot encompasses a child over-the-shoulder reverse shot). Analyze shot descriptions by comparing keywords (e.g., characters, actions, setting) to ensure the parent shot's field of view covers the child shot's.
51 - Transition Smoothness Priority: Larger shot size as parent camera is preferred, such as Wide Shot -> Medium Shot or Medium Shot -> Close-up. The shot sizes of adjacent parent and child nodes should be as similar as possible. A direct transition from a long shot to a close-up is not allowed unless absolutely necessary.
52 - Temporal Proximity: Each camera is described by its corresponding first shot, and the parent camera is located based on the description of the first shot. The shot index of the parent camera should be as close as possible to the first shot index of the child camera.
53 - Logical Consistency: The camera tree should be acyclic, avoid circular dependencies. If a camera is contained by multiple potential parents, select the best match (based on shot size and content). If there is no suitable parent camera, output None.
54 - When a broader perspective is not available, choose the shot with the largest overlapping field of view as the parent (the one with the most information overlap), or a shot can also serve as the parent of a reverse shot. When two cameras can be the parent of each other, choose the one with the smaller index as the parent of the camera with the larger index.
55 - Only one camera can exist without a parent.
56 - When describing the elements lost in a shot, carefully compare the details between the parent shot and the child shot. For example, the parent shot is a medium shot of Character A and Character B facing each other (both in profile to the camera), while the child shot is a close-up of Character A (with Character A facing the camera directly). In this case, the child shot lacks the frontal view information of Character A.
57 - The first camera must be the root of the camera tree.
58 """
59
60
61 human_prompt_template_select_reference_camera = \
62 """
63 <CAMERA_SEQ>
64 {camera_seq_str}
65 </CAMERA_SEQ>
66 """
67
68
69 class CameraParentItem(BaseModel):
70 parent_cam_idx: Optional[int] = Field(
71 default=None,
72 description="The index of the parent camera. Set to None if the camera has no parent (e.g., for a root camera).",
73 examples=[0, 1, None],
74 )
75 parent_shot_idx: Optional[int] = Field(
76 default=None,
77 description="The index of the dependent shot. Set to None if the camera has no parent (e.g., for a root camera).",
78 examples=[0, 3, None],
79 )
80 reason: str = Field(
81 description="The reason for the selection of the parent camera. If the camera has no parent, it should explain why it's a root camera.",
82 examples=[
83 "The parent shot's field of view covers the child shot's field of view (from medium shot to close-up)",
84 "The parent shot and the child shot have a shot/reverse shot relationship.",
85 "CAMERA_0 (Shot 0) establishes the entire scene and contains all characters and the setting. It is the root camera." # 补充 LLM 实际输出的例子
86 ],
87 )
88 is_parent_fully_covers_child: Optional[bool] = Field(
89 default=None,
90 description="Whether the parent camera fully covers the child camera's content. Set to None if the camera has no parent.",
91 examples=[True, False, None],
92 )
93 missing_info: Optional[str] = Field(
94 default=None,
95 description="The missing elements in the child shot that are not covered by the parent shot. If the parent shot fully covers the child shot, set this to None.",
96 examples=[
97 "The frontal view of Alice.",
98 None,
99 ],
100 )
101
102 class CameraTreeResponse(BaseModel):
103 camera_parent_items: List[Optional[CameraParentItem]] = Field(
104 description="The parent camera items for each camera. If a camera has no parent, set this to None. The length of the list should be the same as the number of cameras.",
105 )
106
107
108
109 class CameraImageGenerator:
110
111 def __init__(
112 self,
113 chat_model,
114 image_generator,
115 video_generator,
116 ):
117 self.chat_model = chat_model
118 self.image_generator = image_generator
119 self.video_generator = video_generator
120
121
122 async def construct_camera_tree(
123 self,
124 cameras: List[Camera],
125 shot_descs: List[Union[ShotDescription, ShotBriefDescription]],
126 ) -> List[Camera]:
127 parser = PydanticOutputParser(pydantic_object=CameraTreeResponse)
128 shot_desc_by_idx = {shot.idx: shot for shot in shot_descs}
129
130 camera_seq_str = "<CAMERA_SEQ>\n"
131 for cam in cameras:
132 camera_seq_str += f"<CAMERA_{cam.idx}>\n"
133 for shot_idx in cam.active_shot_idxs:
134 shot_desc = shot_desc_by_idx.get(shot_idx)
135 if shot_desc is None:
136 raise ValueError(f"Camera {cam.idx} references missing shot {shot_idx}")
137 camera_seq_str += f"Shot {shot_idx}: {shot_desc.visual_desc}\n"
138 camera_seq_str += f"</CAMERA_{cam.idx}>\n"
139 camera_seq_str += "</CAMERA_SEQ>"
140
141 messages = [
142 SystemMessage(content=system_prompt_template_select_reference_camera.format(format_instructions=parser.get_format_instructions())),
143 HumanMessage(content=human_prompt_template_select_reference_camera.format(camera_seq_str=camera_seq_str)),
144 ]
145
146 chain = self.chat_model | parser
147 response: CameraTreeResponse = await chain.ainvoke(messages)
148 parent_items = response.camera_parent_items
149 if len(parent_items) != len(cameras):
150 raise ValueError(f"Camera tree response length mismatch: expected {len(cameras)}, got {len(parent_items)}")
151
152 valid_camera_idxs = {cam.idx for cam in cameras}
153 valid_shot_idxs = set(shot_desc_by_idx)
154 parent_by_camera = {}
155 for cam, parent_cam_item in zip(cameras, parent_items):
156 parent_cam_idx = parent_cam_item.parent_cam_idx if parent_cam_item is not None else None
157 parent_shot_idx = parent_cam_item.parent_shot_idx if parent_cam_item is not None else None
158 if parent_cam_idx is not None and parent_cam_idx not in valid_camera_idxs:
159 raise ValueError(f"Camera {cam.idx} has invalid parent camera {parent_cam_idx}")
160 if parent_cam_idx == cam.idx:
161 raise ValueError(f"Camera {cam.idx} cannot be its own parent")
162 if parent_shot_idx is not None and parent_shot_idx not in valid_shot_idxs:
163 raise ValueError(f"Camera {cam.idx} has invalid parent shot {parent_shot_idx}")
164 parent_by_camera[cam.idx] = parent_cam_idx
165
166 for cam in cameras:
167 seen = set()
168 current = cam.idx
169 while parent_by_camera.get(current) is not None:
170 current = parent_by_camera[current]
171 if current in seen:
172 raise ValueError(f"Camera tree contains a cycle involving camera {cam.idx}")
173 seen.add(current)
174
175 for cam, parent_cam_item in zip(cameras, parent_items):
176 cam.parent_cam_idx = parent_cam_item.parent_cam_idx if parent_cam_item is not None else None
177 cam.parent_shot_idx = parent_cam_item.parent_shot_idx if parent_cam_item is not None else None
178 cam.reason = parent_cam_item.reason if parent_cam_item is not None else None
179 cam.is_parent_fully_covers_child = parent_cam_item.is_parent_fully_covers_child if parent_cam_item is not None else None
180 cam.missing_info = parent_cam_item.missing_info if parent_cam_item is not None else None
181 return cameras
182
183
184 async def generate_transition_video(
185 self,
186 first_shot_visual_desc: str,
187 second_shot_visual_desc: str,
188 first_shot_ff_path: str,
189 progress=None,
190 ) -> VideoOutput:
191
192 prompt = f"Two shots. The transition between the shots is a cut to. The style of the two shots should be consistent."
193 prompt += f"\nThe first shot description: {first_shot_visual_desc}."
194 prompt += f"\nThe second shot description: {second_shot_visual_desc}."
195 reference_image_paths = [first_shot_ff_path]
196 video_output = await self.video_generator.generate_single_video(
197 prompt=prompt,
198 reference_image_paths=reference_image_paths,
199 progress=progress,
200 )
201 return video_output
202
203
204 def get_new_camera_image(
205 self,
206 transition_video_path: str,
207 ) -> ImageOutput:
208 video = open_video(transition_video_path)
209 scene_manager = SceneManager()
210 scene_manager.add_detector(ContentDetector())
211 scene_manager.detect_scenes(video, show_progress=False)
212 scene_list = scene_manager.get_scene_list()
213 output_dir = os.path.join(os.path.dirname(transition_video_path), "cache")
214 os.makedirs(output_dir, exist_ok=True)
215 split_video_ffmpeg(transition_video_path, scene_list, output_dir, show_progress=True)
216
217
218 video_name = os.path.basename(transition_video_path).split('.')[0]
219 second_video_path = os.path.join(output_dir, f"{video_name}-Scene-002.mp4")
220 if os.path.exists(second_video_path):
221 # use first frame of second shot as new camera image
222 clip = VideoFileClip(second_video_path)
223 ff = clip.get_frame(0)
224 ff = Image.fromarray(ff.astype('uint8'), 'RGB')
225 return ImageOutput(fmt="pil", ext="png", data=ff)
226 else:
227 # use last frame of transition video to instead
228 clip = VideoFileClip(transition_video_path)
229 lf_time = clip.duration - (1 / clip.fps)
230 lf_time = max(0, lf_time)
231 lf = clip.get_frame(lf_time)
232 lf = Image.fromarray(lf.astype('uint8'), 'RGB')
233 return ImageOutput(fmt="pil", ext="png", data=lf)
234
235
236 async def generate_first_frame(
237 self,
238 shot_desc: ShotDescription,
239 character_portrait_path_and_text_pairs: List[Tuple[str, str]],
240 ) -> ImageOutput:
241 prompt = ""
242 reference_image_paths = []
243 for i,(path, text )in enumerate(character_portrait_path_and_text_pairs):
244 prompt += f"Image {i}: {text}\n"
245 reference_image_paths.append(path)
246 prompt += f"Generate an image based on the following description: {shot_desc.ff_desc}."
247 image_output = await self.image_generator.generate_single_image(
248 prompt=prompt,
249 reference_image_paths=reference_image_paths,
250 size="1600x900",
251 )
252 return image_output
253
254
255
256 def _validate_camera_tree(cameras: List[Camera]) -> None:
257 """Reject parent assignments that would deadlock frame generation."""
258 by_idx = {cam.idx: cam for cam in cameras}
259 for cam in cameras:
260 if cam.parent_cam_idx is None:
261 continue
262 if cam.parent_cam_idx == cam.idx:
263 raise ValueError(f"Camera {cam.idx} lists itself as its parent.")
264 if cam.parent_cam_idx not in by_idx:
265 raise ValueError(f"Camera {cam.idx} references unknown parent camera {cam.parent_cam_idx}.")
266 for cam in cameras:
267 seen = set()
268 current = cam
269 while current.parent_cam_idx is not None:
270 if current.idx in seen:
271 raise ValueError(f"Cycle detected in camera parent graph involving camera {current.idx}.")
272 seen.add(current.idx)
273 current = by_idx[current.parent_cam_idx]
274
274 lines PYTHON