{"metadata":{"kernelspec":{"name":"python3","display_name":"Python 3","language":"python"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":127283,"databundleVersionId":15593191,"isSourceIdPinned":false,"sourceType":"competition"}],"dockerImageVersionId":31259,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# ACCIDENT Baseline: Bounding Box Size Dynamics\n\n## Competition Recap\n\nThis notebook provides a **baseline solution** for an ACCIDENT competition. The goal is to detect three key aspects of traffic accidents in video footage:\n\n1. **When** did the accident happen? (temporal detection - time in seconds)\n2. **Where** did it happen? (spatial detection - location coordinates within the frame)\n3. **What type** of accident was it? (classification - e.g., t-bone, rear-end, single vehicle, etc.)\n\n## Baseline Approach: Object Size Dynamics\n\n### The Core Idea\n\nWhen vehicles collide, their bounding boxes (the rectangular regions around detected objects) undergo dramatic changes:\n- Boxes **overlap** as vehicles come into contact\n- Boxes **merge or split** due to occlusion\n- Box sizes **change abruptly** when impact occurs\n\n### The Method\n\nOur baseline solution addresses the temporal and spatial detection requirements through a multi-step pipeline:\n\n1. **Detect objects** in each video frame using YOLO (You Only Look Once) object detection\n2. **Calculate total bounding box area** per frame: $A_t = \\sum_k \\text{area}(\\text{box}_{k,t})$\n   - This aggregates all detected objects, making the method robust to temporary tracking issues\n3. **Find the change point** using Kernel Change-Point Detection (KernelCPD)\n   - This algorithm identifies when the box area pattern changes most dramatically\n   - The most significant change point is predicted as the accident moment\n4. **Locate the accident spatially** by finding the closest pair of objects in the predicted accident frame\n   - Calculate bounding box centers for all detected objects\n   - Find the two objects with minimum distance between their centers\n   - Use the midpoint between these two objects as the accident location\n   - Normalize coordinates to frame dimensions (0-1 range)\n  \n**Note**: This baseline only addresses temporal detection (when) and spatial detection (where). For accident type classification (what), we use default \"single\" (most common type).","metadata":{}},{"cell_type":"markdown","source":"### Imports & constants","metadata":{}},{"cell_type":"code","source":"!pip install ruptures ultralytics lap","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import cv2\nimport json\nimport pandas as pd\nfrom tqdm import tqdm\nimport ruptures as rpt\nimport math\nfrom itertools import combinations\n\nfrom pathlib import Path\nfrom typing import cast, Generator\n\nimport numpy as np\nimport torch\nfrom ultralytics import YOLO\n\n# Define paths and load video files\nDETECTIONS_PATH = Path(\"./inference-yolo11x\")\nDETECTIONS_PATH.mkdir(parents=True, exist_ok=True)\nDATASET_PATH = Path(\"/kaggle/input/accident\")\nvideo_paths = list((DATASET_PATH / \"videos\").iterdir())\nmetadata_df = pd.read_csv(DATASET_PATH/\"test_metadata.csv\", index_col=\"path\")","metadata":{"execution":{"iopub.status.busy":"2026-02-07T21:13:39.009646Z","iopub.execute_input":"2026-02-07T21:13:39.010010Z","iopub.status.idle":"2026-02-07T21:13:43.644414Z","shell.execute_reply.started":"2026-02-07T21:13:39.009972Z","shell.execute_reply":"2026-02-07T21:13:43.643460Z"},"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Testing Configuration\n\n**For Development/Testing**: By default, this notebook processes tiny subset of data(recommended for initial runs), you can tweak the `take = 2` line to set the subset size. The full (test) datasets runs for 8-12+ hours.\n\n**For Full Submission**: Comment out or remove the `take = 2` line to process all videos in the dataset.","metadata":{}},{"cell_type":"code","source":"# Limit the number of videos processed for testing purposes\ntake = None\ntake = 5 # comment out this line to run on all\nvideo_paths = video_paths[:(take or len(video_paths))]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-07T21:13:43.645616Z","iopub.execute_input":"2026-02-07T21:13:43.646055Z","iopub.status.idle":"2026-02-07T21:13:43.651722Z","shell.execute_reply.started":"2026-02-07T21:13:43.646011Z","shell.execute_reply":"2026-02-07T21:13:43.650869Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Step 1: Object Detection with YOLO v11\n\nIn this step, we process each video frame to detect and track objects (primarily vehicles) using **YOLO v11 X-Large** model. This is a state-of-the-art object detection model that can identify and track multiple objects across video frames.\n\n**Key configuration parameters**:\n- **Resolution**: 1280x1280 pixels - higher resolution improves accuracy but increases processing time\n- **Batch size**: 8 frames - processes multiple frames simultaneously for efficiency\n- **Confidence threshold**: 0.15 - filters out low-confidence detections to reduce false positives\n\n### Output\n\nFor each video, we generate a JSON file containing:\n- **Frames**: Frame indices where detections occurred\n- **Bounding boxes**: Coordinates of detected objects in each frame (x1, y1, x2, y2 format)\n- **Path**: Relative path to the video file, acting as a video id\n\n### Optional Data (Currently Unused)\n\nThe code also captures (but doesn't use in this baseline):\n- `class_ids`: What type of object was detected (car, truck, etc.)\n- `track_ids`: Unique IDs for tracking objects across frames\n    - ByteTrack tracker is used to maintain object identities across frames\n- `confidences`: Detection confidence scores\n\n**💡 AI Improvement Suggestion**: These unused features could be valuable for:\n- Filtering detections by confidence threshold\n- Using object types to improve accident type classification\n- Using track IDs to analyze vehicle trajectories before collision","metadata":{}},{"cell_type":"code","source":"# Algorithm parameters:\nYOLO_MODEL_PATH=\"./yolo11x.pt\" # Used pre-trained YOLO model.\nYOLO_IMAGE_RESOLUTION=1280  # Input image resolution for YOLO (must be multiple of 32). Higher = more accurate but slower\nYOLO_BATCH_SIZE=8  # Number of frames processed simultaneously. Increase for faster processing if GPU memory allows\nCONFIDENCE_THRESHOLD = 0.15  # Minimum confidence score for object detection (0-1). Lower = more detections but more false positives\nCUDA_DEVICE_ID = 0  # GPU device ID to use (0 for first GPU). Set to None or use CPU if no GPU available\n\nclass Tracker:\n    def __init__(self, batch_size: int = YOLO_BATCH_SIZE):\n        self.batch_size = batch_size\n\n        self.model = YOLO(YOLO_MODEL_PATH)\n        device = torch.device(f\"cuda:{CUDA_DEVICE_ID}\" if torch.cuda.is_available() else \"cpu\")\n        self.model.to(device)\n        print(f\"Using YOLO model: {YOLO_MODEL_PATH} at {device.type} device.\")\n    \n    def track(\n            self, batches: Generator[list[cv2.typing.MatLike], None, None]\n    ) -> dict:\n        \"\"\"Inference on images.\n        \n        Args:\n            batches: Iterable of batched images.\n        Returns:\n            List of processed predictions.\n        \"\"\"\n        bboxes = []\n        frames_indices = []\n        class_ids, track_ids, confidences = [], [], []\n        try:\n            for batch_index, batch in enumerate(batches):\n                results = self.model.track(\n                    batch,\n                    imgsz=YOLO_IMAGE_RESOLUTION,  # must be a multiple of stride 32\n                    verbose=False,\n                    tracker=\"bytetrack.yaml\",\n                    persist=True,\n                    conf=CONFIDENCE_THRESHOLD,\n                )\n\n                for i, x in enumerate(results):\n                    if x.boxes is not None and x.boxes.is_track:\n                        frames_indices.append(batch_index * self.batch_size + i)\n                        bboxes.append(x.boxes.xyxy.cpu().numpy().tolist())\n                        class_ids.append(x.boxes.cls.cpu().numpy().astype(int).tolist())\n                        track_ids.append(x.boxes.id.cpu().numpy().tolist())\n                        confidences.append(x.boxes.conf.cpu().numpy().tolist())\n        except Exception as e:\n            print(f\"ERROR: {e}\")\n        finally:\n            assert (\n                hasattr(self.model, \"predictor\") \n                and self.model.predictor is not None\n                and hasattr(self.model.predictor, \"trackers\") \n                and self.model.predictor.trackers is not None\n            )\n            \n            for tracker in self.model.predictor.trackers:\n                tracker.reset()\n\n        return {\n            \"frames\": frames_indices,\n            \"bboxes\": bboxes,\n            \"class_ids\": class_ids,\n            \"track_ids\": track_ids,\n            \"confidences\": confidences,\n        }\n\n    def load_batched(self, cap: cv2.VideoCapture):\n        \"\"\"Load video frames in batches.\n\n        Args:\n            cap: Opened cv2.VideoCapture object.\n        Yields:\n            Batches of video frames.\n        \"\"\"\n        while True:\n            batch: list[cv2.typing.MatLike] = []\n            for _ in range(self.batch_size):\n                ret, frame = cap.read()\n                if not ret:\n                    return\n                batch.append(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))\n            yield batch\n    \n    def process_video_file(self, video_path: Path) -> dict:\n        \"\"\"Track objects in a video file.\n\n        Args:\n            video_path: Path to a video file.\n        Returns:\n            Dict with list of detections and other relevant dat\n        \"\"\"\n        cap = None\n        try:\n            cap = cv2.VideoCapture(video_path)\n\n            detections = self.track(self.load_batched(cap))\n            detections = {\n                **detections,\n                \"path\": str(video_path.relative_to(DATASET_PATH).as_posix())\n            }\n        finally:\n            if cap:\n                cap.release()\n\n        return detections","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-07T21:13:43.652985Z","iopub.execute_input":"2026-02-07T21:13:43.653300Z","iopub.status.idle":"2026-02-07T21:13:43.703215Z","shell.execute_reply.started":"2026-02-07T21:13:43.653258Z","shell.execute_reply":"2026-02-07T21:13:43.702097Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"tracker = Tracker()\n\nfor filename in tqdm(video_paths):\n    filename = cast(Path, filename)\n    predictions_path =  DETECTIONS_PATH / filename.with_suffix(\".json\").name\n\n    # Skip if predictions already exist\n    if predictions_path.exists():\n        print(f\"Skipping {predictions_path}. Predictions already exists.\")\n        continue\n\n    # Process video\n    predictions = tracker.process_video_file(filename)\n\n    # Save predictions\n    with open(predictions_path, \"w\") as f:\n        json.dump(predictions, f)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-07T21:13:43.705350Z","iopub.execute_input":"2026-02-07T21:13:43.705748Z","iopub.status.idle":"2026-02-08T00:50:23.042125Z","shell.execute_reply.started":"2026-02-07T21:13:43.705718Z","shell.execute_reply":"2026-02-08T00:50:23.040998Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Step 2: Temporal Detection - When Did the Accident Happen?\n\n### The Problem\n\nWe need to identify the exact moment (frame number and time) when the accident occurred in each video.\n\n### Our Approach: Bounding Box Size Change Detection\n\nThe function `find_bbox_size_change()` implements our temporal detection heuristic:\n\n1. **Calculate total bounding box area per frame**: Sum up the area of all detected bounding boxes in each frame\n2. **Normalize the areas**: Convert to proportions (0-1) to make the method scale-invariant\n3. **Apply Kernel Change-Point Detection**: Use the `ruptures` library's KernelCPD algorithm to find when the box area pattern changes most dramatically\n4. **Return the change point**: The frame where the biggest change occurred is our predicted accident moment\n\n### How It Works\n\n**KernelCPD (Kernel Change-Point Detection)**: This algorithm looks for points where the statistical distribution of the data changes. In our case, it finds when the bounding box area pattern shifts (indicating collision).\nWe use a Radial Basis Function (RBF) kernel, which is good at detecting smooth changes in patterns.\n\n### Edge Cases Handled\n\n- **No detections in video**: If no objects are detected, returns the middle frame\n- **Empty frames**: If a frame has no detections, uses the previous frame's area value or 0 on first frame\n\n### Output\n\nFor each video, we get:\n- `frame`: The predicted frame number where the accident occurred\n- `accident_time`: The time in seconds (calculated as `frame / fps`)\n\n**💡 AI Improvement Suggestions**:\n- Use multiple change-point detection algorithms and ensemble their results\n- Incorporate velocity/acceleration of bounding boxes\n- Use temporal models (LSTM, Transformer) to learn accident patterns\n- Consider the rate of change, not just the magnitude","metadata":{}},{"cell_type":"code","source":"def find_bbox_size_change(detections: dict) -> tuple[int, np.ndarray]:\n    \"\"\"\n    Find biggest change in bbox sizes in video.\n\n    Returns:\n        int: A frame with biggest change in bbox sizes\n    \"\"\"\n    bbox_sizes = []\n    for bboxes in detections[\"bboxes\"]:\n        if len(bboxes) == 0:\n            if len(bbox_sizes) > 0: \n                # If no bboxes detected in current frame, use the last known bbox size (assuming objects are still present but not detected)\n                bbox_sizes.append(bbox_sizes[-1])\n            else:\n                # No bboxes detected (yet), default to 0\n                bbox_sizes.append(0)\n        else:\n            bbox_pixel_size = sum([abs(x2 - x1) * abs(y2 - y1) for x1, y1, x2, y2 in bboxes])\n            bbox_sizes.append(bbox_pixel_size)\n    \n    if np.sum(bbox_sizes) == 0:\n        # No bboxes detected in video, return the middle frame\n        return len(detections[\"frames\"])//2, bbox_sizes\n    bbox_sizes = np.array(bbox_sizes)\n    bbox_sizes = bbox_sizes / np.sum(bbox_sizes)\n\n    changes = rpt.KernelCPD(kernel=\"rbf\").fit_predict(bbox_sizes, n_bkps=1)\n    assert changes and len(changes) >= 1\n    return ((detections[\"frames\"][changes[0]] + detections[\"frames\"][changes[0]+1]) / 2), bbox_sizes","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T00:50:23.043405Z","iopub.execute_input":"2026-02-08T00:50:23.043686Z","iopub.status.idle":"2026-02-08T00:50:23.052261Z","shell.execute_reply.started":"2026-02-08T00:50:23.043658Z","shell.execute_reply":"2026-02-08T00:50:23.051330Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"results: dict[Path, dict] = {}\nfor video in tqdm(video_paths):\n    video = cast(Path, video)\n    with open(DETECTIONS_PATH / video.with_suffix(\".json\").name, \"r\") as f:\n        info = json.load(f)\n\n    # Get video metadata to calculate FPS and convert frame number to time\n    metadata = metadata_df.loc[info[\"path\"]]\n    fps = metadata[\"no_frames\"] / metadata[\"duration\"]\n    \n    # Find frame with biggest change in bbox sizes\n    frame, _ = find_bbox_size_change(info)\n    results[info[\"path\"]] = {\"frame\": frame, \"accident_time\": frame / fps}\n    ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T00:50:23.053328Z","iopub.execute_input":"2026-02-08T00:50:23.053653Z","iopub.status.idle":"2026-02-08T00:50:23.150622Z","shell.execute_reply.started":"2026-02-08T00:50:23.053618Z","shell.execute_reply":"2026-02-08T00:50:23.149766Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Step 3: Spatial Detection - Where Did the Accident Happen?\n\n### The Problem\n\nGiven the predicted accident frame from Step 2, we need to identify the **location** (x, y coordinates) within the frame where the accident occurred.\n\n### Our Approach: Closest Object Pair\n\nThe spatial detection heuristic works as follows:\n\n1. **Extract bounding boxes from the accident frame**: Get all detected objects at the predicted accident moment\n2. **Calculate bounding box centers**: For each detected object, find the center point of its bounding box\n3. **Find the closest pair**: Among all detected objects, find the two objects whose centers are closest together\n4. **Use the midpoint**: The accident location is predicted as the midpoint between these two closest centers\n5. **Normalize coordinates**: Convert pixel coordinates to normalized coordinates (0-1) relative to frame dimensions\n\n### Edge Cases\n\n- **Only one object detected**: Use that object's center as the accident location\n- **No objects detected**: Default to the center of the frame (0.5, 0.5)\n\n### Output Format\n\nCoordinates are normalized (0-1 range):\n- `center_x`: X coordinate (0 = left edge, 1 = right edge)\n- `center_y`: Y coordinate (0 = top edge, 1 = bottom edge)\n\n**💡 AI Improvement Suggestions**:\n- Consider object sizes when finding collision points (larger objects might indicate the collision)\n- Use trajectory analysis to predict where objects will intersect\n- Analyze overlap regions between bounding boxes\n- Use attention mechanisms to focus on collision regions","metadata":{}},{"cell_type":"code","source":"def euclidean_distance(p1, p2):\n    return math.sqrt((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2)\n\ndef midpoint(p1, p2):\n    return ((p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2)\n\nfor video in tqdm(video_paths):\n    with open(DETECTIONS_PATH / video.with_suffix(\".json\").name, \"r\") as f:\n        info = json.load(f)\n\n    result = results[info[\"path\"]]\n    accident_frame = int(result[\"frame\"])\n    metadata = metadata_df.loc[info[\"path\"]]\n\n    bboxes = next(bboxes for frame, bboxes in zip(info[\"frames\"], info[\"bboxes\"]) if frame == accident_frame)\n    if len(bboxes) == 0: # no detections in the accident frame, use frame center\n        result[\"center_x\"] = 0.5\n        result[\"center_y\"] = 0.5\n        continue\n\n    bbox_centers = [midpoint((x1, y1), (x2, y2)) for (x1, y1, x2, y2) in bboxes]\n    if len(bbox_centers) == 1: # only one detection, use its center\n        result[\"center_x\"] = bbox_centers[0][0] / metadata[\"width\"]\n        result[\"center_y\"] = bbox_centers[0][1] / metadata[\"height\"]\n        continue\n\n    (c1, c2) = min(\n        combinations(bbox_centers, 2),\n        key=lambda pair: euclidean_distance(pair[0], pair[1])\n    )\n\n    # prediction = midpoint of those two centers\n    prediction_center = midpoint(c1, c2)\n    result[\"center_x\"] = prediction_center[0] / metadata[\"width\"]\n    result[\"center_y\"] = prediction_center[1] / metadata[\"height\"]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T00:50:23.151597Z","iopub.execute_input":"2026-02-08T00:50:23.151921Z","iopub.status.idle":"2026-02-08T00:50:23.212358Z","shell.execute_reply.started":"2026-02-08T00:50:23.151876Z","shell.execute_reply":"2026-02-08T00:50:23.211301Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Step 4. Export `submission.csv`\n\nFinally, its submission time. For each test video, write one row to `submission.csv` containing the predicted `accident_time` in seconds\n\nThe remaining required fields are filled with fixed default values:\n\n- `type: \"single\"`\n\nThis ensures the submission format is valid while development focuses on predicting accident timing.\n\n**💡 This is a major area for improvement!**","metadata":{}},{"cell_type":"code","source":"# Put the default type:\nfor result in results.values():\n    result[\"type\"] = \"single\"\n    \nresults_df = pd.DataFrame([{\"path\": str(path), **i, \"frame\": None } for path, i in results.items()]).drop(columns=[\"frame\"])\nresults_df.to_csv(\"submission.csv\", index=False)\nresults_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T00:50:23.213836Z","iopub.execute_input":"2026-02-08T00:50:23.214222Z","iopub.status.idle":"2026-02-08T00:50:23.254208Z","shell.execute_reply.started":"2026-02-08T00:50:23.214177Z","shell.execute_reply":"2026-02-08T00:50:23.253302Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Bonus step: Visualization 📈\n\nAs a final step, we include a simple qualitative visualization to help interpret the baseline predictions.\n\nWhat the visualization shows\n\n- **Left panel**: the video frame at the predicted accident time, with the predicted accident location highlighted.\n\n- **Right panel**: the evolution of the normalized overal bounding box size over time, with a vertical line marking the predicted accident moment.","metadata":{}},{"cell_type":"code","source":"import cv2\nimport numpy as np\nimport matplotlib.pyplot as plt\n\ndef show_video_card(\n    video_path,\n    values_per_frame,\n    fps,\n    accident_time,\n    center_x=None,\n    center_y=None,\n    title=None,\n):\n    # ---- Load accident frame ----\n    cap = cv2.VideoCapture(video_path)\n    frame_idx = int(accident_time * fps)\n    cap.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)\n\n    ret, frame = cap.read()\n    cap.release()\n\n    if not ret:\n        raise RuntimeError(f\"Could not read frame from {video_path}\")\n\n    frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)\n\n    # Optional: draw accident location\n    if center_x is not None and center_y is not None:\n        h, w, _ = frame.shape\n        px = int(center_x * w)\n        py = int(center_y * h)\n        cv2.circle(frame, (px, py), 10, (255, 0, 0), -1)\n\n    # ---- Prepare signal plot ----\n    time = np.arange(len(values_per_frame)) / fps\n\n    # ---- Build card ----\n    fig, (ax_img, ax_plot) = plt.subplots(\n        1, 2, figsize=(14, 5), gridspec_kw={\"width_ratios\": [1, 1.3]}\n    )\n\n    # Image panel\n    ax_img.imshow(frame)\n    ax_img.set_title(f\"Predicted accident at {accident_time:.2f}s\")\n    ax_img.axis(\"off\")\n\n    # Signal panel\n    ax_plot.plot(time, values_per_frame)\n    ax_plot.axvline(accident_time)\n    ax_plot.set_xlabel(\"Time (seconds)\")\n    ax_plot.set_ylabel(\"Bounding boxes size (normalized)\")\n    ax_plot.set_title(\"Bounding box size dynamics\")\n\n    if title:\n        fig.suptitle(title)\n\n    plt.tight_layout()\n    plt.show()\n\nresults: dict[Path, dict] = {}\nfor _, row in results_df.iterrows():\n    video = DATASET_PATH / row[\"path\"]\n    with open(DETECTIONS_PATH / video.with_suffix(\".json\").name, \"r\") as f:\n        info = json.load(f)\n\n    metadata = metadata_df.loc[info[\"path\"]]\n    fps = metadata[\"no_frames\"] / metadata[\"duration\"]\n    \n    frame, sizes = find_bbox_size_change(info)\n\n    show_video_card(\n        video_path=DATASET_PATH / row[\"path\"],\n        values_per_frame=sizes,\n        fps=fps,\n        accident_time=row[\"accident_time\"],\n        center_x=row[\"center_x\"],\n        center_y=row[\"center_y\"],\n        title=f\"{row['path']} predicted as {row['type']}\",\n    )","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-02-08T00:50:23.255249Z","iopub.execute_input":"2026-02-08T00:50:23.255515Z","iopub.status.idle":"2026-02-08T00:50:26.888116Z","shell.execute_reply.started":"2026-02-08T00:50:23.255487Z","shell.execute_reply":"2026-02-08T00:50:26.887260Z"}},"outputs":[],"execution_count":null}]}