droidrun 0.3.1__py3-none-any.whl → 0.3.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,10 +18,24 @@ logger = logging.getLogger("droidrun")
18
18
 
19
19
  class Trajectory:
20
20
 
21
- def __init__(self):
22
- """Initializes an empty trajectory class."""
21
+ def __init__(self, goal: str = None):
22
+ """Initializes an empty trajectory class.
23
+
24
+ Args:
25
+ goal: The goal/prompt that this trajectory is trying to achieve
26
+ """
23
27
  self.events: List[Event] = []
24
28
  self.screenshots: List[bytes] = []
29
+ self.macro: List[Event] = []
30
+ self.goal = goal or "DroidRun automation sequence"
31
+
32
+ def set_goal(self, goal: str) -> None:
33
+ """Update the goal/description for this trajectory.
34
+
35
+ Args:
36
+ goal: The new goal/prompt description
37
+ """
38
+ self.goal = goal
25
39
 
26
40
 
27
41
  def create_screenshot_gif(self, output_path: str, duration: int = 1000) -> str:
@@ -33,9 +47,10 @@ class Trajectory:
33
47
  duration: Duration for each frame in milliseconds
34
48
 
35
49
  Returns:
36
- Path to the created GIF file
50
+ Path to the created GIF file, or None if no screenshots available
37
51
  """
38
52
  if len(self.screenshots) == 0:
53
+ logger.info("📷 No screenshots available for GIF creation")
39
54
  return None
40
55
 
41
56
  images = []
@@ -62,17 +77,20 @@ class Trajectory:
62
77
  ) -> str:
63
78
  """
64
79
  Save trajectory steps to a JSON file and create a GIF of screenshots if available.
80
+ Also saves the macro sequence as a separate file for replay.
81
+ Creates a dedicated folder for each trajectory containing all related files.
65
82
 
66
83
  Args:
67
- directory: Directory to save the trajectory files
84
+ directory: Base directory to save the trajectory files
68
85
 
69
86
  Returns:
70
- Path to the saved trajectory file
87
+ Path to the trajectory folder
71
88
  """
72
89
  os.makedirs(directory, exist_ok=True)
73
90
 
74
91
  timestamp = time.strftime("%Y%m%d_%H%M%S")
75
- base_path = os.path.join(directory, f"trajectory_{timestamp}")
92
+ trajectory_folder = os.path.join(directory, f"trajectory_{timestamp}")
93
+ os.makedirs(trajectory_folder, exist_ok=True)
76
94
 
77
95
  def make_serializable(obj):
78
96
  """Recursively make objects JSON serializable."""
@@ -100,6 +118,7 @@ class Trajectory:
100
118
  else:
101
119
  return obj
102
120
 
121
+ # Save main trajectory events
103
122
  serializable_events = []
104
123
  for event in self.events:
105
124
  event_dict = {
@@ -109,13 +128,209 @@ class Trajectory:
109
128
  }
110
129
  serializable_events.append(event_dict)
111
130
 
112
- json_path = f"{base_path}.json"
113
- with open(json_path, "w") as f:
131
+ trajectory_json_path = os.path.join(trajectory_folder, "trajectory.json")
132
+ with open(trajectory_json_path, "w") as f:
114
133
  json.dump(serializable_events, f, indent=2)
115
134
 
116
- self.create_screenshot_gif(base_path)
135
+ # Save macro sequence as a separate file for replay
136
+ if self.macro:
137
+ macro_data = []
138
+ for macro_event in self.macro:
139
+ macro_dict = {
140
+ "type": macro_event.__class__.__name__,
141
+ **{k: make_serializable(v) for k, v in macro_event.__dict__.items()
142
+ if not k.startswith('_')}
143
+ }
144
+ macro_data.append(macro_dict)
145
+
146
+ macro_json_path = os.path.join(trajectory_folder, "macro.json")
147
+ with open(macro_json_path, "w") as f:
148
+ json.dump({
149
+ "version": "1.0",
150
+ "description": self.goal,
151
+ "timestamp": timestamp,
152
+ "total_actions": len(macro_data),
153
+ "actions": macro_data
154
+ }, f, indent=2)
155
+
156
+ logger.info(f"💾 Saved macro sequence with {len(macro_data)} actions to {macro_json_path}")
157
+
158
+ # Create screenshot GIF
159
+ gif_path = self.create_screenshot_gif(os.path.join(trajectory_folder, "screenshots"))
160
+ if gif_path:
161
+ logger.info(f"🎬 Saved screenshot GIF to {gif_path}")
162
+
163
+ logger.info(f"📁 Trajectory saved to folder: {trajectory_folder}")
164
+ return trajectory_folder
165
+
166
+ @staticmethod
167
+ def load_trajectory_folder(trajectory_folder: str) -> Dict[str, Any]:
168
+ """
169
+ Load trajectory data from a trajectory folder.
170
+
171
+ Args:
172
+ trajectory_folder: Path to the trajectory folder
173
+
174
+ Returns:
175
+ Dictionary containing trajectory data, macro data, and file paths
176
+ """
177
+ result = {
178
+ "trajectory_data": None,
179
+ "macro_data": None,
180
+ "gif_path": None,
181
+ "folder_path": trajectory_folder
182
+ }
183
+
184
+ try:
185
+ # Load main trajectory
186
+ trajectory_json_path = os.path.join(trajectory_folder, "trajectory.json")
187
+ if os.path.exists(trajectory_json_path):
188
+ with open(trajectory_json_path, "r") as f:
189
+ result["trajectory_data"] = json.load(f)
190
+ logger.info(f"📖 Loaded trajectory data from {trajectory_json_path}")
191
+
192
+ # Load macro sequence
193
+ macro_json_path = os.path.join(trajectory_folder, "macro.json")
194
+ if os.path.exists(macro_json_path):
195
+ with open(macro_json_path, "r") as f:
196
+ result["macro_data"] = json.load(f)
197
+ logger.info(f"📖 Loaded macro data from {macro_json_path}")
198
+
199
+ # Check for GIF
200
+ gif_path = os.path.join(trajectory_folder, "screenshots.gif")
201
+ if os.path.exists(gif_path):
202
+ result["gif_path"] = gif_path
203
+ logger.info(f"🎬 Found screenshot GIF at {gif_path}")
204
+
205
+ return result
206
+
207
+ except Exception as e:
208
+ logger.error(f"❌ Error loading trajectory folder {trajectory_folder}: {e}")
209
+ return result
210
+
211
+ @staticmethod
212
+ def load_macro_sequence(macro_file_path: str) -> Dict[str, Any]:
213
+ """
214
+ Load a macro sequence from a saved macro file.
215
+
216
+ Args:
217
+ macro_file_path: Path to the macro JSON file (can be full path or trajectory folder)
218
+
219
+ Returns:
220
+ Dictionary containing the macro sequence data
221
+ """
222
+ # Check if it's a folder path - if so, look for macro.json inside
223
+ if os.path.isdir(macro_file_path):
224
+ macro_file_path = os.path.join(macro_file_path, "macro.json")
225
+
226
+ try:
227
+ with open(macro_file_path, "r") as f:
228
+ macro_data = json.load(f)
229
+
230
+ logger.info(f"📖 Loaded macro sequence with {macro_data.get('total_actions', 0)} actions from {macro_file_path}")
231
+ return macro_data
232
+ except FileNotFoundError:
233
+ logger.error(f"❌ Macro file not found: {macro_file_path}")
234
+ return {}
235
+ except json.JSONDecodeError as e:
236
+ logger.error(f"❌ Error parsing macro file {macro_file_path}: {e}")
237
+ return {}
238
+
239
+ @staticmethod
240
+ def get_macro_summary(macro_data: Dict[str, Any]) -> Dict[str, Any]:
241
+ """
242
+ Get a summary of a macro sequence.
243
+
244
+ Args:
245
+ macro_data: The macro data dictionary
246
+
247
+ Returns:
248
+ Dictionary with statistics about the macro
249
+ """
250
+ if not macro_data or "actions" not in macro_data:
251
+ return {"error": "Invalid macro data"}
252
+
253
+ actions = macro_data["actions"]
254
+
255
+ # Count action types
256
+ action_types = {}
257
+ for action in actions:
258
+ action_type = action.get("action_type", "unknown")
259
+ action_types[action_type] = action_types.get(action_type, 0) + 1
260
+
261
+ # Calculate duration if timestamps are available
262
+ timestamps = [action.get("timestamp") for action in actions if action.get("timestamp")]
263
+ duration = max(timestamps) - min(timestamps) if len(timestamps) > 1 else 0
264
+
265
+ return {
266
+ "version": macro_data.get("version", "unknown"),
267
+ "description": macro_data.get("description", "No description"),
268
+ "total_actions": len(actions),
269
+ "action_types": action_types,
270
+ "duration_seconds": round(duration, 2) if duration > 0 else None,
271
+ "timestamp": macro_data.get("timestamp", "unknown")
272
+ }
117
273
 
118
- return json_path
274
+ @staticmethod
275
+ def print_macro_summary(macro_file_path: str) -> None:
276
+ """
277
+ Print a summary of a macro sequence.
278
+
279
+ Args:
280
+ macro_file_path: Path to the macro JSON file or trajectory folder
281
+ """
282
+ macro_data = Trajectory.load_macro_sequence(macro_file_path)
283
+ if not macro_data:
284
+ print("❌ Could not load macro data")
285
+ return
286
+
287
+ summary = Trajectory.get_macro_summary(macro_data)
288
+
289
+ print("=== Macro Summary ===")
290
+ print(f"File: {macro_file_path}")
291
+ print(f"Version: {summary.get('version', 'unknown')}")
292
+ print(f"Description: {summary.get('description', 'No description')}")
293
+ print(f"Timestamp: {summary.get('timestamp', 'unknown')}")
294
+ print(f"Total actions: {summary.get('total_actions', 0)}")
295
+ if summary.get('duration_seconds'):
296
+ print(f"Duration: {summary['duration_seconds']} seconds")
297
+ print("Action breakdown:")
298
+ for action_type, count in summary.get('action_types', {}).items():
299
+ print(f" - {action_type}: {count}")
300
+ print("=====================")
301
+
302
+ @staticmethod
303
+ def print_trajectory_folder_summary(trajectory_folder: str) -> None:
304
+ """
305
+ Print a comprehensive summary of a trajectory folder.
306
+
307
+ Args:
308
+ trajectory_folder: Path to the trajectory folder
309
+ """
310
+ folder_data = Trajectory.load_trajectory_folder(trajectory_folder)
311
+
312
+ print("=== Trajectory Folder Summary ===")
313
+ print(f"Folder: {trajectory_folder}")
314
+ print(f"Trajectory data: {'✅ Available' if folder_data['trajectory_data'] else '❌ Missing'}")
315
+ print(f"Macro data: {'✅ Available' if folder_data['macro_data'] else '❌ Missing'}")
316
+ print(f"Screenshot GIF: {'✅ Available' if folder_data['gif_path'] else '❌ Missing'}")
317
+
318
+ if folder_data['macro_data']:
319
+ print("\n--- Macro Summary ---")
320
+ summary = Trajectory.get_macro_summary(folder_data['macro_data'])
321
+ print(f"Description: {summary.get('description', 'No description')}")
322
+ print(f"Total actions: {summary.get('total_actions', 0)}")
323
+ if summary.get('duration_seconds'):
324
+ print(f"Duration: {summary['duration_seconds']} seconds")
325
+ print("Action breakdown:")
326
+ for action_type, count in summary.get('action_types', {}).items():
327
+ print(f" - {action_type}: {count}")
328
+
329
+ if folder_data['trajectory_data']:
330
+ print(f"\n--- Trajectory Summary ---")
331
+ print(f"Total events: {len(folder_data['trajectory_data'])}")
332
+
333
+ print("=================================")
119
334
 
120
335
  def get_trajectory_statistics(trajectory_data: Dict[str, Any]) -> Dict[str, Any]:
121
336
  """
@@ -181,4 +396,36 @@ class Trajectory:
181
396
  print(f"Execution steps: {stats['execution_steps']}")
182
397
  print(f"Successful executions: {stats['successful_executions']}")
183
398
  print(f"Failed executions: {stats['failed_executions']}")
184
- print("==========================")
399
+ print("==========================")
400
+
401
+
402
+ # Example usage:
403
+ """
404
+ # Save a trajectory with a specific goal (automatically creates folder structure)
405
+ trajectory = Trajectory(goal="Open settings and check battery level")
406
+ # ... add events and screenshots to trajectory ...
407
+ folder_path = trajectory.save_trajectory()
408
+
409
+ # Or update the goal later
410
+ trajectory.set_goal("Navigate to Settings and find device info")
411
+
412
+ # Load entire trajectory folder
413
+ folder_data = Trajectory.load_trajectory_folder(folder_path)
414
+ trajectory_events = folder_data['trajectory_data']
415
+ macro_actions = folder_data['macro_data']
416
+ gif_path = folder_data['gif_path']
417
+
418
+ # Load just the macro from folder
419
+ macro_data = Trajectory.load_macro_sequence(folder_path)
420
+
421
+ # Print summaries
422
+ Trajectory.print_trajectory_folder_summary(folder_path)
423
+ Trajectory.print_macro_summary(folder_path)
424
+
425
+ # Example folder structure created:
426
+ # trajectories/
427
+ # └── trajectory_20250108_143052/
428
+ # ├── trajectory.json # Full trajectory events
429
+ # ├── macro.json # Macro sequence with goal as description
430
+ # └── screenshots.gif # Screenshot animation
431
+ """