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.
- cli/clip.py +79 -0
- cli/faces.py +91 -0
- cli/metadata.py +68 -0
- cli/motion.py +77 -0
- cli/objects.py +94 -0
- cli/ocr.py +93 -0
- cli/scenes.py +57 -0
- cli/telemetry.py +65 -0
- cli/transcript.py +76 -0
- media_engine/__init__.py +7 -0
- media_engine/_version.py +34 -0
- media_engine/app.py +80 -0
- media_engine/batch/__init__.py +56 -0
- media_engine/batch/models.py +99 -0
- media_engine/batch/processor.py +1131 -0
- media_engine/batch/queue.py +232 -0
- media_engine/batch/state.py +30 -0
- media_engine/batch/timing.py +321 -0
- media_engine/cli.py +17 -0
- media_engine/config.py +674 -0
- media_engine/extractors/__init__.py +75 -0
- media_engine/extractors/clip.py +401 -0
- media_engine/extractors/faces.py +459 -0
- media_engine/extractors/frame_buffer.py +351 -0
- media_engine/extractors/frames.py +402 -0
- media_engine/extractors/metadata/__init__.py +127 -0
- media_engine/extractors/metadata/apple.py +169 -0
- media_engine/extractors/metadata/arri.py +118 -0
- media_engine/extractors/metadata/avchd.py +208 -0
- media_engine/extractors/metadata/avchd_gps.py +270 -0
- media_engine/extractors/metadata/base.py +688 -0
- media_engine/extractors/metadata/blackmagic.py +139 -0
- media_engine/extractors/metadata/camera_360.py +276 -0
- media_engine/extractors/metadata/canon.py +290 -0
- media_engine/extractors/metadata/dji.py +371 -0
- media_engine/extractors/metadata/dv.py +121 -0
- media_engine/extractors/metadata/ffmpeg.py +76 -0
- media_engine/extractors/metadata/generic.py +119 -0
- media_engine/extractors/metadata/gopro.py +256 -0
- media_engine/extractors/metadata/red.py +305 -0
- media_engine/extractors/metadata/registry.py +114 -0
- media_engine/extractors/metadata/sony.py +442 -0
- media_engine/extractors/metadata/tesla.py +157 -0
- media_engine/extractors/motion.py +765 -0
- media_engine/extractors/objects.py +245 -0
- media_engine/extractors/objects_qwen.py +754 -0
- media_engine/extractors/ocr.py +268 -0
- media_engine/extractors/scenes.py +82 -0
- media_engine/extractors/shot_type.py +217 -0
- media_engine/extractors/telemetry.py +262 -0
- media_engine/extractors/transcribe.py +579 -0
- media_engine/extractors/translate.py +121 -0
- media_engine/extractors/vad.py +263 -0
- media_engine/main.py +68 -0
- media_engine/py.typed +0 -0
- media_engine/routers/__init__.py +15 -0
- media_engine/routers/batch.py +78 -0
- media_engine/routers/health.py +93 -0
- media_engine/routers/models.py +211 -0
- media_engine/routers/settings.py +87 -0
- media_engine/routers/utils.py +135 -0
- media_engine/schemas.py +581 -0
- media_engine/utils/__init__.py +5 -0
- media_engine/utils/logging.py +54 -0
- media_engine/utils/memory.py +49 -0
- media_engine-0.1.0.dist-info/METADATA +276 -0
- media_engine-0.1.0.dist-info/RECORD +70 -0
- media_engine-0.1.0.dist-info/WHEEL +4 -0
- media_engine-0.1.0.dist-info/entry_points.txt +11 -0
- 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
|