Skip to content

Commit

Permalink
Add support for comma connect URL inputs
Browse files Browse the repository at this point in the history
Closes #49
  • Loading branch information
nelsonjchen committed Oct 30, 2023
1 parent 284eb0e commit cf4d717
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 5 deletions.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ predict:
./cog/generate.sh
../cog/cog predict

predict-url-wide:
./cog/generate.sh
../cog/cog predict -i route="https://connect.comma.ai/a2a0ccea32023010/1690488163535/1690488170140" -i renderType=wide

predict-wide:
./cog/generate.sh
../cog/cog predict -i renderType=wide
Expand Down
38 changes: 33 additions & 5 deletions predict.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
import downloader

import ffmpeg_clip
import route_or_url

MIN_LENGTH_SECONDS = 5
MAX_LENGTH_SECONDS = 120


class Predictor(BasePredictor):
Expand All @@ -20,21 +24,26 @@ def setup(self) -> None:
def predict(
self,
renderType: str = Input(
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.",
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.",
choices=["ui", "forward", "wide", "driver", "360"],
default="ui",
),
route: str = Input(
description="Route ID (w/ Segment Number OK but the segment number will be ignored in favor of start seconds) "
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 )"
" (⚠️ ROUTE MUST BE PUBLIC! You can set this temporarily in Connect.)"
' (⚠️ Ensure all data from forward and wide cameras and "Logs" to be rendered have been uploaded; See README for more info)',
' (⚠️ 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.)',
default="a2a0ccea32023010|2023-07-27--13-01-19",
),
startSeconds: int = Input(
description="Start time in seconds", ge=0, default=50
description=
"Start time in seconds (Ignored if comma connect URL input is used)",
ge=0,
default=50
),
lengthSeconds: int = Input(
description="Length of clip in seconds", ge=5, le=120, default=20
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,
le=MAX_LENGTH_SECONDS,
default=20
),
smearAmount: int = Input(
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)",
Expand Down Expand Up @@ -66,7 +75,26 @@ def predict(
os.remove("./shared/cog-clip.mp4")

# Print the notes
print("NOTES:")
print(notes)
print("")

parsed_input_route_or_url = route_or_url.parseRouteOrUrl(
route_or_url=route, start_seconds=startSeconds, length_seconds=lengthSeconds
)
route = parsed_input_route_or_url.route
startSeconds = parsed_input_route_or_url.start_seconds
lengthSeconds = parsed_input_route_or_url.length_seconds

# Enforce the minimum and maximum lengths
if lengthSeconds < MIN_LENGTH_SECONDS:
raise ValueError(
f"Length must be at least {MIN_LENGTH_SECONDS} seconds. Got {lengthSeconds} seconds."
)
if lengthSeconds > MAX_LENGTH_SECONDS:
raise ValueError(
f"Length must be at most {MAX_LENGTH_SECONDS} seconds. Got {lengthSeconds} seconds."
)

# Get the dongle ID from the route. It's everything before the first pipe.
dongleID = route.split("|")[0]
Expand Down
183 changes: 183 additions & 0 deletions route_or_url.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# Parses a route or URL string, throwing an exception if it's invalid.

import dataclasses

from urllib.parse import urlparse

import requests

# Dataclass for a parsed route or URL


@dataclasses.dataclass
class ParsedRouteOrURL:
route: str
start_seconds: int
length_seconds: int


def parseRouteOrUrl(
route_or_url: str, start_seconds: int, length_seconds: int
) -> ParsedRouteOrURL:
# if the route_or_url is a route, just return it
# Assume that a route is a string with a pipe in it
if "|" in route_or_url:
return ParsedRouteOrURL(route_or_url, start_seconds, length_seconds)

# Check if the URL is like this:
# https://connect.comma.ai/a2a0ccea32023010/1690488084000/1690488085000
# * Hostname is connect.comma.ai
# * Path is "dongle id"/"start time"/"end time"
# * Start time and end time are in milliseconds since the epoch
# * Start time is before end time

# Parse the URL
parsed_url = urlparse(route_or_url)

# Check the hostname
if parsed_url.hostname != "connect.comma.ai":
raise ValueError("Invalid hostname in URL")

# Check the path
path_parts = parsed_url.path.split("/")
# There should be three parts
if len(path_parts) != 4:
raise ValueError("Invalid path in URL")
# The first part should be the dongle ID
dongle_id = path_parts[1]
# The second part should be the start time
start_time = int(path_parts[2])
# The third part should be the end time
end_time = int(path_parts[3])
# Start time should be before end time
if start_time >= end_time:
raise ValueError("Invalid start and end times in URL")

# The above URL is equivalent to this API call:
# https://api.comma.ai/v1/devices/a2a0ccea32023010/routes_segments?end=1690488851596&start=1690488081496

# Make the API call
api_url = f"https://api.comma.ai/v1/devices/{dongle_id}/routes_segments?end={end_time}&start={start_time}"
response = requests.get(api_url)
# Check the response
if response.status_code != 200:
raise ValueError("Invalid API response")

json = response.json()

# Response (Excerpt) is like this
# [
# {
# "fullname": "a2a0ccea32023010|2023-07-27--13-01-19",
# "segment_end_times": [
# 1690488142995,
# 1690488203050,
# 1690488263032,
# 1690488322998,
# 1690488383009,
# 1690488443000,
# 1690488503010,
# 1690488563006,
# 1690488623013,
# 1690488683016,
# 1690488743014,
# 1690488803019,
# 1690488851596
# ],
# "segment_numbers": [
# 0,
# 1,
# 2,
# 3,
# 4,
# 5,
# 6,
# 7,
# 8,
# 9,
# 10,
# 11,
# 12
# ],
# "segment_start_times": [
# 1690488081496,
# 1690488143038,
# 1690488203035,
# 1690488263028,
# 1690488323037,
# 1690488383025,
# 1690488443035,
# 1690488503030,
# 1690488563038,
# 1690488623040,
# 1690488683035,
# 1690488743039,
# 1690488803035
# ],
# }
# ]
# And keep in mind there can be multiple unrelated routes in the response.
# It seems filtering does not work and it returns unrelated routes.
# Try to find the route of interest
# As an example, https://connect.comma.ai/a2a0ccea32023010/1690488152777/1690488186013
# Should return
# Route: a2a0ccea32023010|2023-07-27--13-01-19
# Start Seconds: 71
# Length Seconds: 104
# Ignore all the milliseconds too

# Discover what the start and end times of each route returned are
matched_route = None

for route_info in json:
start_in_route = False
end_in_route = False
# Assume the first segment_start_time is the start of the route
route_start_time = route_info["segment_start_times"][0]
# Assume the last segment_end_time is the end of the route
route_end_time = route_info["segment_end_times"][-1]
# Check if the start time is in the route
if start_time >= route_start_time and start_time <= route_end_time:
start_in_route = True
# Check if the end time is in the route
if end_time >= route_start_time and end_time <= route_end_time:
end_in_route = True
# If both the start and end times are in the route, we found our match
if start_in_route and end_in_route:
matched_route = route_info
break

# If we didn't find a match, throw an exception
if matched_route is None:
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.")

# Get the route name
route_name = matched_route["fullname"]
# Compute the start seconds
start_seconds = (start_time - route_start_time) // 1000
# Compute the length seconds
length_seconds = (end_time - start_time) // 1000

# Return the parsed route
return ParsedRouteOrURL(route_name, start_seconds, length_seconds)


# Make an argparse test for this
if __name__ == "__main__":
import argparse

parser = argparse.ArgumentParser(description="parse a route or URL")
parser.add_argument("route_or_url", type=str, help="Route or URL to parse")
parser.add_argument("start_seconds", type=int, help="Start time in seconds")
parser.add_argument(
"length_seconds", type=int, help="Length of the segment to render"
)
args = parser.parse_args()

parsed_route = parseRouteOrUrl(
args.route_or_url, args.start_seconds, args.length_seconds
)

print(f"Route: {parsed_route.route}")
print(f"Start Seconds: {parsed_route.start_seconds}")
print(f"Length Seconds: {parsed_route.length_seconds}")

0 comments on commit cf4d717

Please sign in to comment.