Skip to content

Commit cf4d717

Browse files
committed
Add support for comma connect URL inputs
Closes #49
1 parent 284eb0e commit cf4d717

File tree

3 files changed

+220
-5
lines changed

3 files changed

+220
-5
lines changed

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ predict:
2121
./cog/generate.sh
2222
../cog/cog predict
2323

24+
predict-url-wide:
25+
./cog/generate.sh
26+
../cog/cog predict -i route="https://connect.comma.ai/a2a0ccea32023010/1690488163535/1690488170140" -i renderType=wide
27+
2428
predict-wide:
2529
./cog/generate.sh
2630
../cog/cog predict -i renderType=wide

predict.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
import downloader
1111

1212
import ffmpeg_clip
13+
import route_or_url
14+
15+
MIN_LENGTH_SECONDS = 5
16+
MAX_LENGTH_SECONDS = 120
1317

1418

1519
class Predictor(BasePredictor):
@@ -20,21 +24,26 @@ def setup(self) -> None:
2024
def predict(
2125
self,
2226
renderType: str = Input(
23-
description="Render Type. UI is very slow but has the UI. 360 is slow too. The rest are quite fast transcodes. Note: 🌐 360 requires viewing the video file in VLC or uploading to YouTube to see the 360 effect.",
27+
description="Render Type. UI is very slow but has the UI burned in. Forward, Wide, and Driver are fast transcodes; you may want to use them to do quick previews. 360 is slow due to CPU processing. Note: 🌐 360 requires viewing the video file in VLC or uploading to YouTube to pan around in a sphere.",
2428
choices=["ui", "forward", "wide", "driver", "360"],
2529
default="ui",
2630
),
2731
route: str = Input(
28-
description="Route ID (w/ Segment Number OK but the segment number will be ignored in favor of start seconds) "
32+
description="Route ID (w/ Segment Number OK but the segment number will be ignored in favor of start seconds) OR comma connect URL (e.g. https://connect.comma.ai/fe18f736cb0d7813/1698620773416/1698620855707 )"
2933
" (⚠️ ROUTE MUST BE PUBLIC! You can set this temporarily in Connect.)"
30-
' (⚠️ Ensure all data from forward and wide cameras and "Logs" to be rendered have been uploaded; See README for more info)',
34+
' (⚠️ Ensure all necessary data for the render type is uploaded. UI requires forward, wide, and log. 360 requires wide and driver. Forward, Wide, and Driver require their respective camera files uploaded. If you aren\'t sure, upload all files. Please see the README for more info.)',
3135
default="a2a0ccea32023010|2023-07-27--13-01-19",
3236
),
3337
startSeconds: int = Input(
34-
description="Start time in seconds", ge=0, default=50
38+
description=
39+
"Start time in seconds (Ignored if comma connect URL input is used)",
40+
ge=0,
41+
default=50
3542
),
3643
lengthSeconds: int = Input(
37-
description="Length of clip in seconds", ge=5, le=120, default=20
44+
description="Length of clip in seconds (Ignored if comma connect URL input is used, however the minimum and maximum lengths are still enforced)", ge=MIN_LENGTH_SECONDS,
45+
le=MAX_LENGTH_SECONDS,
46+
default=20
3847
),
3948
smearAmount: int = Input(
4049
description="(UI Render only) Smear amount (Let the video start this time before beginning recording, useful for making sure the radar △, if present, is rendered at the start if necessary)",
@@ -66,7 +75,26 @@ def predict(
6675
os.remove("./shared/cog-clip.mp4")
6776

6877
# Print the notes
78+
print("NOTES:")
6979
print(notes)
80+
print("")
81+
82+
parsed_input_route_or_url = route_or_url.parseRouteOrUrl(
83+
route_or_url=route, start_seconds=startSeconds, length_seconds=lengthSeconds
84+
)
85+
route = parsed_input_route_or_url.route
86+
startSeconds = parsed_input_route_or_url.start_seconds
87+
lengthSeconds = parsed_input_route_or_url.length_seconds
88+
89+
# Enforce the minimum and maximum lengths
90+
if lengthSeconds < MIN_LENGTH_SECONDS:
91+
raise ValueError(
92+
f"Length must be at least {MIN_LENGTH_SECONDS} seconds. Got {lengthSeconds} seconds."
93+
)
94+
if lengthSeconds > MAX_LENGTH_SECONDS:
95+
raise ValueError(
96+
f"Length must be at most {MAX_LENGTH_SECONDS} seconds. Got {lengthSeconds} seconds."
97+
)
7098

7199
# Get the dongle ID from the route. It's everything before the first pipe.
72100
dongleID = route.split("|")[0]

route_or_url.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# Parses a route or URL string, throwing an exception if it's invalid.
2+
3+
import dataclasses
4+
5+
from urllib.parse import urlparse
6+
7+
import requests
8+
9+
# Dataclass for a parsed route or URL
10+
11+
12+
@dataclasses.dataclass
13+
class ParsedRouteOrURL:
14+
route: str
15+
start_seconds: int
16+
length_seconds: int
17+
18+
19+
def parseRouteOrUrl(
20+
route_or_url: str, start_seconds: int, length_seconds: int
21+
) -> ParsedRouteOrURL:
22+
# if the route_or_url is a route, just return it
23+
# Assume that a route is a string with a pipe in it
24+
if "|" in route_or_url:
25+
return ParsedRouteOrURL(route_or_url, start_seconds, length_seconds)
26+
27+
# Check if the URL is like this:
28+
# https://connect.comma.ai/a2a0ccea32023010/1690488084000/1690488085000
29+
# * Hostname is connect.comma.ai
30+
# * Path is "dongle id"/"start time"/"end time"
31+
# * Start time and end time are in milliseconds since the epoch
32+
# * Start time is before end time
33+
34+
# Parse the URL
35+
parsed_url = urlparse(route_or_url)
36+
37+
# Check the hostname
38+
if parsed_url.hostname != "connect.comma.ai":
39+
raise ValueError("Invalid hostname in URL")
40+
41+
# Check the path
42+
path_parts = parsed_url.path.split("/")
43+
# There should be three parts
44+
if len(path_parts) != 4:
45+
raise ValueError("Invalid path in URL")
46+
# The first part should be the dongle ID
47+
dongle_id = path_parts[1]
48+
# The second part should be the start time
49+
start_time = int(path_parts[2])
50+
# The third part should be the end time
51+
end_time = int(path_parts[3])
52+
# Start time should be before end time
53+
if start_time >= end_time:
54+
raise ValueError("Invalid start and end times in URL")
55+
56+
# The above URL is equivalent to this API call:
57+
# https://api.comma.ai/v1/devices/a2a0ccea32023010/routes_segments?end=1690488851596&start=1690488081496
58+
59+
# Make the API call
60+
api_url = f"https://api.comma.ai/v1/devices/{dongle_id}/routes_segments?end={end_time}&start={start_time}"
61+
response = requests.get(api_url)
62+
# Check the response
63+
if response.status_code != 200:
64+
raise ValueError("Invalid API response")
65+
66+
json = response.json()
67+
68+
# Response (Excerpt) is like this
69+
# [
70+
# {
71+
# "fullname": "a2a0ccea32023010|2023-07-27--13-01-19",
72+
# "segment_end_times": [
73+
# 1690488142995,
74+
# 1690488203050,
75+
# 1690488263032,
76+
# 1690488322998,
77+
# 1690488383009,
78+
# 1690488443000,
79+
# 1690488503010,
80+
# 1690488563006,
81+
# 1690488623013,
82+
# 1690488683016,
83+
# 1690488743014,
84+
# 1690488803019,
85+
# 1690488851596
86+
# ],
87+
# "segment_numbers": [
88+
# 0,
89+
# 1,
90+
# 2,
91+
# 3,
92+
# 4,
93+
# 5,
94+
# 6,
95+
# 7,
96+
# 8,
97+
# 9,
98+
# 10,
99+
# 11,
100+
# 12
101+
# ],
102+
# "segment_start_times": [
103+
# 1690488081496,
104+
# 1690488143038,
105+
# 1690488203035,
106+
# 1690488263028,
107+
# 1690488323037,
108+
# 1690488383025,
109+
# 1690488443035,
110+
# 1690488503030,
111+
# 1690488563038,
112+
# 1690488623040,
113+
# 1690488683035,
114+
# 1690488743039,
115+
# 1690488803035
116+
# ],
117+
# }
118+
# ]
119+
# And keep in mind there can be multiple unrelated routes in the response.
120+
# It seems filtering does not work and it returns unrelated routes.
121+
# Try to find the route of interest
122+
# As an example, https://connect.comma.ai/a2a0ccea32023010/1690488152777/1690488186013
123+
# Should return
124+
# Route: a2a0ccea32023010|2023-07-27--13-01-19
125+
# Start Seconds: 71
126+
# Length Seconds: 104
127+
# Ignore all the milliseconds too
128+
129+
# Discover what the start and end times of each route returned are
130+
matched_route = None
131+
132+
for route_info in json:
133+
start_in_route = False
134+
end_in_route = False
135+
# Assume the first segment_start_time is the start of the route
136+
route_start_time = route_info["segment_start_times"][0]
137+
# Assume the last segment_end_time is the end of the route
138+
route_end_time = route_info["segment_end_times"][-1]
139+
# Check if the start time is in the route
140+
if start_time >= route_start_time and start_time <= route_end_time:
141+
start_in_route = True
142+
# Check if the end time is in the route
143+
if end_time >= route_start_time and end_time <= route_end_time:
144+
end_in_route = True
145+
# If both the start and end times are in the route, we found our match
146+
if start_in_route and end_in_route:
147+
matched_route = route_info
148+
break
149+
150+
# If we didn't find a match, throw an exception
151+
if matched_route is None:
152+
raise ValueError(f"Route not found from URL. Route is possibly private. Visit the URL {route_or_url} and make sure Public is toggled under the \"More Info\" drop-down.")
153+
154+
# Get the route name
155+
route_name = matched_route["fullname"]
156+
# Compute the start seconds
157+
start_seconds = (start_time - route_start_time) // 1000
158+
# Compute the length seconds
159+
length_seconds = (end_time - start_time) // 1000
160+
161+
# Return the parsed route
162+
return ParsedRouteOrURL(route_name, start_seconds, length_seconds)
163+
164+
165+
# Make an argparse test for this
166+
if __name__ == "__main__":
167+
import argparse
168+
169+
parser = argparse.ArgumentParser(description="parse a route or URL")
170+
parser.add_argument("route_or_url", type=str, help="Route or URL to parse")
171+
parser.add_argument("start_seconds", type=int, help="Start time in seconds")
172+
parser.add_argument(
173+
"length_seconds", type=int, help="Length of the segment to render"
174+
)
175+
args = parser.parse_args()
176+
177+
parsed_route = parseRouteOrUrl(
178+
args.route_or_url, args.start_seconds, args.length_seconds
179+
)
180+
181+
print(f"Route: {parsed_route.route}")
182+
print(f"Start Seconds: {parsed_route.start_seconds}")
183+
print(f"Length Seconds: {parsed_route.length_seconds}")

0 commit comments

Comments
 (0)