media-engine 0.1.0__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.
Files changed (70) hide show
  1. cli/clip.py +79 -0
  2. cli/faces.py +91 -0
  3. cli/metadata.py +68 -0
  4. cli/motion.py +77 -0
  5. cli/objects.py +94 -0
  6. cli/ocr.py +93 -0
  7. cli/scenes.py +57 -0
  8. cli/telemetry.py +65 -0
  9. cli/transcript.py +76 -0
  10. media_engine/__init__.py +7 -0
  11. media_engine/_version.py +34 -0
  12. media_engine/app.py +80 -0
  13. media_engine/batch/__init__.py +56 -0
  14. media_engine/batch/models.py +99 -0
  15. media_engine/batch/processor.py +1131 -0
  16. media_engine/batch/queue.py +232 -0
  17. media_engine/batch/state.py +30 -0
  18. media_engine/batch/timing.py +321 -0
  19. media_engine/cli.py +17 -0
  20. media_engine/config.py +674 -0
  21. media_engine/extractors/__init__.py +75 -0
  22. media_engine/extractors/clip.py +401 -0
  23. media_engine/extractors/faces.py +459 -0
  24. media_engine/extractors/frame_buffer.py +351 -0
  25. media_engine/extractors/frames.py +402 -0
  26. media_engine/extractors/metadata/__init__.py +127 -0
  27. media_engine/extractors/metadata/apple.py +169 -0
  28. media_engine/extractors/metadata/arri.py +118 -0
  29. media_engine/extractors/metadata/avchd.py +208 -0
  30. media_engine/extractors/metadata/avchd_gps.py +270 -0
  31. media_engine/extractors/metadata/base.py +688 -0
  32. media_engine/extractors/metadata/blackmagic.py +139 -0
  33. media_engine/extractors/metadata/camera_360.py +276 -0
  34. media_engine/extractors/metadata/canon.py +290 -0
  35. media_engine/extractors/metadata/dji.py +371 -0
  36. media_engine/extractors/metadata/dv.py +121 -0
  37. media_engine/extractors/metadata/ffmpeg.py +76 -0
  38. media_engine/extractors/metadata/generic.py +119 -0
  39. media_engine/extractors/metadata/gopro.py +256 -0
  40. media_engine/extractors/metadata/red.py +305 -0
  41. media_engine/extractors/metadata/registry.py +114 -0
  42. media_engine/extractors/metadata/sony.py +442 -0
  43. media_engine/extractors/metadata/tesla.py +157 -0
  44. media_engine/extractors/motion.py +765 -0
  45. media_engine/extractors/objects.py +245 -0
  46. media_engine/extractors/objects_qwen.py +754 -0
  47. media_engine/extractors/ocr.py +268 -0
  48. media_engine/extractors/scenes.py +82 -0
  49. media_engine/extractors/shot_type.py +217 -0
  50. media_engine/extractors/telemetry.py +262 -0
  51. media_engine/extractors/transcribe.py +579 -0
  52. media_engine/extractors/translate.py +121 -0
  53. media_engine/extractors/vad.py +263 -0
  54. media_engine/main.py +68 -0
  55. media_engine/py.typed +0 -0
  56. media_engine/routers/__init__.py +15 -0
  57. media_engine/routers/batch.py +78 -0
  58. media_engine/routers/health.py +93 -0
  59. media_engine/routers/models.py +211 -0
  60. media_engine/routers/settings.py +87 -0
  61. media_engine/routers/utils.py +135 -0
  62. media_engine/schemas.py +581 -0
  63. media_engine/utils/__init__.py +5 -0
  64. media_engine/utils/logging.py +54 -0
  65. media_engine/utils/memory.py +49 -0
  66. media_engine-0.1.0.dist-info/METADATA +276 -0
  67. media_engine-0.1.0.dist-info/RECORD +70 -0
  68. media_engine-0.1.0.dist-info/WHEEL +4 -0
  69. media_engine-0.1.0.dist-info/entry_points.txt +11 -0
  70. media_engine-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,262 @@
1
+ """Telemetry extraction from drone/camera sidecar files."""
2
+
3
+ import logging
4
+ import re
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+
8
+ from media_engine.schemas import TelemetryPoint, TelemetryResult
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def extract_telemetry(
14
+ file_path: str,
15
+ sample_interval: float = 1.0,
16
+ ) -> TelemetryResult | None:
17
+ """Extract telemetry/flight path from video sidecar files.
18
+
19
+ Args:
20
+ file_path: Path to video file
21
+ sample_interval: Sample one point every N seconds (default: 1.0)
22
+
23
+ Returns:
24
+ TelemetryResult with GPS track and camera settings, or None if no telemetry
25
+ """
26
+ path = Path(file_path)
27
+ if not path.exists():
28
+ raise FileNotFoundError(f"Video file not found: {file_path}")
29
+
30
+ # Try DJI SRT sidecar first (has GPS for drones)
31
+ result = _parse_dji_srt_telemetry(file_path, sample_interval)
32
+ if result:
33
+ return result
34
+
35
+ # Try embedded subtitle stream (DJI Pocket, some other cameras)
36
+ result = _parse_embedded_subtitle_telemetry(file_path, sample_interval)
37
+ if result:
38
+ return result
39
+
40
+ # TODO: Add Sony NX5 GPS parsing
41
+ # TODO: Add GoPro telemetry parsing
42
+
43
+ return None
44
+
45
+
46
+ def _parse_dji_srt_telemetry(
47
+ video_path: str,
48
+ sample_interval: float = 1.0,
49
+ ) -> TelemetryResult | None:
50
+ """Parse DJI SRT file for full telemetry track.
51
+
52
+ DJI SRT format (per frame):
53
+ 1
54
+ 00:00:00,000 --> 00:00:00,020
55
+ <font size="28">FrameCnt: 1, DiffTime: 20ms
56
+ 2025-10-15 11:36:32.281
57
+ [iso: 400] [shutter: 1/100.0] [fnum: 2.8] [ev: 0] [ct: 5790]
58
+ [color_md : d_log] [focal_len: 24.00]
59
+ [latitude: 61.05121] [longitude: 7.81233]
60
+ [rel_alt: 47.100 abs_alt: 380.003] </font>
61
+ """
62
+ path = Path(video_path)
63
+
64
+ # Find SRT file
65
+ srt_patterns = [
66
+ path.with_suffix(".SRT"),
67
+ path.with_suffix(".srt"),
68
+ ]
69
+
70
+ srt_path = None
71
+ for pattern in srt_patterns:
72
+ if pattern.exists():
73
+ srt_path = pattern
74
+ break
75
+
76
+ if not srt_path:
77
+ return None
78
+
79
+ try:
80
+ with open(srt_path, encoding="utf-8") as f:
81
+ content = f.read()
82
+
83
+ # Split into subtitle blocks
84
+ blocks = re.split(r"\n\n+", content)
85
+
86
+ points: list[TelemetryPoint] = []
87
+ last_timestamp = -sample_interval # Ensure first point is captured
88
+
89
+ for block in blocks:
90
+ # Parse timestamp from SRT format: 00:00:00,000 --> 00:00:01,000
91
+ time_match = re.search(r"(\d{2}):(\d{2}):(\d{2}),(\d{3})\s*-->", block)
92
+ if not time_match:
93
+ continue
94
+
95
+ hours = int(time_match.group(1))
96
+ minutes = int(time_match.group(2))
97
+ seconds = int(time_match.group(3))
98
+ millis = int(time_match.group(4))
99
+ timestamp = hours * 3600 + minutes * 60 + seconds + millis / 1000
100
+
101
+ # Sample at specified interval
102
+ if timestamp - last_timestamp < sample_interval:
103
+ continue
104
+ last_timestamp = timestamp
105
+
106
+ # Parse GPS
107
+ lat_match = re.search(r"\[latitude:\s*([-\d.]+)\]", block)
108
+ lon_match = re.search(r"\[longitude:\s*([-\d.]+)\]", block)
109
+
110
+ if not (lat_match and lon_match):
111
+ continue
112
+
113
+ lat = float(lat_match.group(1))
114
+ lon = float(lon_match.group(1))
115
+
116
+ # Skip invalid coordinates
117
+ if lat == 0 and lon == 0:
118
+ continue
119
+
120
+ # Parse altitudes
121
+ abs_alt_match = re.search(r"abs_alt:\s*([-\d.]+)", block)
122
+ rel_alt_match = re.search(r"rel_alt:\s*([-\d.]+)", block)
123
+
124
+ # Parse datetime: 2025-10-15 11:36:32.281
125
+ recorded_at = None
126
+ dt_match = re.search(r"(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d+)", block)
127
+ if dt_match:
128
+ try:
129
+ recorded_at = datetime.strptime(dt_match.group(1), "%Y-%m-%d %H:%M:%S.%f")
130
+ except ValueError:
131
+ pass
132
+
133
+ # Parse camera settings
134
+ iso_match = re.search(r"\[iso:\s*(\d+)\]", block)
135
+ shutter_match = re.search(r"\[shutter:\s*1/([\d.]+)\]", block)
136
+ fnum_match = re.search(r"\[fnum:\s*([\d.]+)\]", block)
137
+ focal_match = re.search(r"\[focal_len:\s*([\d.]+)\]", block)
138
+ color_match = re.search(r"\[color_md\s*:\s*(\w+)\]", block)
139
+
140
+ point = TelemetryPoint(
141
+ timestamp=timestamp,
142
+ recorded_at=recorded_at,
143
+ latitude=lat,
144
+ longitude=lon,
145
+ altitude=float(abs_alt_match.group(1)) if abs_alt_match else None,
146
+ relative_altitude=(float(rel_alt_match.group(1)) if rel_alt_match else None),
147
+ iso=int(iso_match.group(1)) if iso_match else None,
148
+ shutter=1 / float(shutter_match.group(1)) if shutter_match else None,
149
+ aperture=float(fnum_match.group(1)) if fnum_match else None,
150
+ focal_length=float(focal_match.group(1)) if focal_match else None,
151
+ color_mode=color_match.group(1) if color_match else None,
152
+ )
153
+ points.append(point)
154
+
155
+ if not points:
156
+ return None
157
+
158
+ # Calculate duration from last timestamp
159
+ duration = points[-1].timestamp if points else 0
160
+
161
+ return TelemetryResult(
162
+ source="dji_srt",
163
+ sample_rate=1.0 / sample_interval,
164
+ duration=duration,
165
+ points=points,
166
+ )
167
+
168
+ except Exception as e:
169
+ logger.warning(f"Error parsing DJI SRT telemetry {srt_path}: {e}")
170
+ return None
171
+
172
+
173
+ def _parse_embedded_subtitle_telemetry(
174
+ video_path: str,
175
+ sample_interval: float = 1.0,
176
+ ) -> TelemetryResult | None:
177
+ """Parse embedded subtitle stream for telemetry (DJI Pocket, etc.).
178
+
179
+ Some cameras embed telemetry as subtitle tracks instead of external SRT files.
180
+ Format is similar to SRT: F/1.8, SS 293.11, ISO 110, EV -0.3
181
+ """
182
+ import subprocess
183
+
184
+ try:
185
+ # Extract subtitle stream using ffmpeg
186
+ cmd = [
187
+ "ffmpeg",
188
+ "-i",
189
+ video_path,
190
+ "-map",
191
+ "0:s:0",
192
+ "-f",
193
+ "srt",
194
+ "-",
195
+ ]
196
+
197
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
198
+ if result.returncode != 0 or not result.stdout:
199
+ return None
200
+
201
+ content = result.stdout
202
+
203
+ # Parse subtitle blocks (simpler format than full SRT)
204
+ # Format: F/1.8, SS 293.11, ISO 110, EV -0.3,
205
+ blocks = re.split(r"\n\n+", content)
206
+
207
+ points: list[TelemetryPoint] = []
208
+ last_timestamp = -sample_interval
209
+
210
+ for block in blocks:
211
+ # Parse timestamp
212
+ time_match = re.search(r"(\d{2}):(\d{2}):(\d{2}),(\d{3})\s*-->", block)
213
+ if not time_match:
214
+ continue
215
+
216
+ hours = int(time_match.group(1))
217
+ minutes = int(time_match.group(2))
218
+ seconds = int(time_match.group(3))
219
+ millis = int(time_match.group(4))
220
+ timestamp = hours * 3600 + minutes * 60 + seconds + millis / 1000
221
+
222
+ if timestamp - last_timestamp < sample_interval:
223
+ continue
224
+ last_timestamp = timestamp
225
+
226
+ # Parse DJI Pocket format: F/1.8, SS 293.11, ISO 110, EV -0.3
227
+ aperture_match = re.search(r"F/([\d.]+)", block)
228
+ shutter_match = re.search(r"SS\s+([\d.]+)", block)
229
+ iso_match = re.search(r"ISO\s+(\d+)", block)
230
+
231
+ # This format doesn't have GPS, but has camera settings
232
+ if aperture_match or iso_match:
233
+ point = TelemetryPoint(
234
+ timestamp=timestamp,
235
+ latitude=0.0, # No GPS in this format
236
+ longitude=0.0,
237
+ aperture=float(aperture_match.group(1)) if aperture_match else None,
238
+ shutter=(1 / float(shutter_match.group(1)) if shutter_match else None),
239
+ iso=int(iso_match.group(1)) if iso_match else None,
240
+ )
241
+ points.append(point)
242
+
243
+ if not points:
244
+ return None
245
+
246
+ # Filter out points with no GPS (0,0)
247
+ # For embedded subtitles, we keep exposure-only data
248
+ duration = points[-1].timestamp if points else 0
249
+
250
+ return TelemetryResult(
251
+ source="embedded_subtitle",
252
+ sample_rate=1.0 / sample_interval,
253
+ duration=duration,
254
+ points=points,
255
+ )
256
+
257
+ except subprocess.TimeoutExpired:
258
+ logger.warning(f"Timeout extracting embedded subtitles from {video_path}")
259
+ return None
260
+ except Exception as e:
261
+ logger.warning(f"Error parsing embedded subtitle telemetry: {e}")
262
+ return None