{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.11","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":102335,"databundleVersionId":12518947,"sourceType":"competition"},{"sourceId":244070137,"sourceType":"kernelVersion"}],"dockerImageVersionId":31040,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"## Description\n\n- This version is bug-fixed and additional feature is implemented\n   - bug fix: load quaternion with scalar-first order\n   - feature: visualize acc_world and acc_linear\n\noriginal code: https://github.com/childmindresearch/helios-render-kaggle","metadata":{}},{"cell_type":"code","source":"%load_ext autoreload\n%autoreload 2","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-06T12:39:01.484027Z","iopub.execute_input":"2025-06-06T12:39:01.484450Z","iopub.status.idle":"2025-06-06T12:39:01.512911Z","shell.execute_reply.started":"2025-06-06T12:39:01.484418Z","shell.execute_reply":"2025-06-06T12:39:01.511765Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"! pip install -r /kaggle/input/clone-herios-data-visualizer/helios-render-kaggle/requirements.txt","metadata":{"trusted":true,"_kg_hide-output":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%%writefile helios_render.py\n\"\"\"\nHelios Data Visualizer\n\nInstallation:\n1. Create a virtual environment:\n   python -m venv venv\n   source venv/bin/activate  # On Windows: venv/Scripts/activate\n\n2. Install dependencies:\n   pip install -r requirements.txt\n\n3. Install FFmpeg (required for video generation):\n   - Windows: Download from https://ffmpeg.org/download.html and add to PATH\n   - macOS: brew install ffmpeg\n   - Linux: sudo apt install ffmpeg\n\nUsage:\n   python helios_render.py --csv cmi-detect-behavior-with-sensor-data/train.csv --subject SUBJ_032761 --gesture \"Wave hello\"\n   python helios_render.py --csv cmi-detect-behavior-with-sensor-data/train.csv --subject SUBJ_032761 --gesture \"Neck - pinch skin\" --sequence_index 3\n\nUpdates:\n\nv2:\n- filter by handedness\n- bug fix on loading rotation matrix (scalar-first order)\n- align arm mesh to sensor axes\n- change projection to \"xz\" plane\n\"\"\"\n\nimport argparse\nimport os\nimport pathlib\nimport re\nimport subprocess\nimport tempfile\nimport time\nfrom pathlib import Path\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport polars as pl\nimport pyvista as pv\nfrom scipy.spatial.transform import Rotation as R\nfrom tqdm import tqdm\n\nWINDOW_SIZE = [1280, 720]\nVIDEO_SIZE = (16, 9)\nDPI = 120\nTOF_GRID_SIZE = (8, 8)\nNUM_TOF_SENSORS = 5\nNUM_THERMOPILE_SENSORS = 5\n\nCOLORS = {\n    \"background\": [0.05, 0.05, 0.07],\n    \"background_top\": [0.12, 0.12, 0.16],\n    \"mesh\": \"#e0e0e8\",\n    \"text\": \"#ffffff\",\n    \"text_secondary\": \"#e0e0e8\",\n    \"text_tertiary\": \"#c0c0d0\",\n    \"title_bg\": \"#1a1a2e\",\n    \"figure_bg\": \"#0a0a12\",\n    \"spine\": \"#4a4a6a\",\n}\n\n\ndef align_mesh_to_sensor_axes(mesh):\n    rot_fix = R.from_euler(\"z\", -90, degrees=True)\n    transform = create_transform_matrix(rot_fix.as_matrix())\n    mesh.transform(transform, inplace=True)\n    return mesh\n\n\ndef sanitize_filename(name):\n    return re.sub(r'[\\\\/*?:\"<>|]', \"_\", name)\n\n\ndef create_transform_matrix(rotation_matrix):\n    \"\"\"Create 4x4 transformation matrix from 3x3 rotation matrix.\"\"\"\n    transform_matrix = np.eye(4)\n    transform_matrix[:3, :3] = rotation_matrix\n    return transform_matrix\n\n\ndef extract_tof_data(row, sensor_num):\n    \"\"\"Extract ToF sensor data and reshape to 8x8 grid.\"\"\"\n    tof_data = []\n    for i in range(64):\n        column_name = f\"tof_{sensor_num}_v{i}\"\n        if column_name in row.keys():\n            value = row[column_name]\n            tof_data.append(np.nan if value == -1 else float(value))\n        else:\n            tof_data.append(np.nan)\n    return np.array(tof_data).reshape(TOF_GRID_SIZE)\n\n\ndef load_data(csv_path, demographic_path, filters=None, use_cache=True):\n    \"\"\"Load and filter data with optional caching.\"\"\"\n    csv_path = pathlib.Path(csv_path)\n    cache_dir = pathlib.Path(CACHE_DIR)\n    cache_dir.mkdir(exist_ok=True)\n    cache_file = cache_dir / f\"{csv_path.stem}.parquet\"\n\n    if use_cache and cache_file.exists():\n        print(f\"Loading data from cache: {cache_file}\")\n        start_time = time.time()\n        df = pl.read_parquet(cache_file)\n        print(f\"Loaded from cache in {time.time() - start_time:.2f} seconds\")\n    else:\n        print(f\"Loading data from {csv_path}...\")\n        start_time = time.time()\n        df = pl.read_csv(csv_path)\n        demographics_df = pl.read_csv(demographic_path)\n        df = df.join(demographics_df, on=\"subject\", how=\"left\")\n        print(f\"Loaded CSV in {time.time() - start_time:.2f} seconds\")\n\n        if use_cache:\n            print(f\"Creating cache file: {cache_file}\")\n            df.write_parquet(cache_file)\n\n    # Apply filters\n    if filters:\n        for column, value in filters.items():\n            if value:\n                df = df.filter(pl.col(column) == value)\n                if df.height == 0:\n                    raise ValueError(f\"No data found for {column} = {value}\")\n                print(f\"Filtered for {column}: {value}\")\n\n    unique_sequence_ids = df[\"sequence_id\"].unique(maintain_order=True).to_list()\n    if len(unique_sequence_ids) == 0:\n        raise ValueError(\"No valid sequences found in the dataset\")\n\n    print(f\"Found {len(unique_sequence_ids)} sequences\")\n    return df, unique_sequence_ids\n\n\ndef load_arm_mesh():\n    \"\"\"Load arm mesh from file.\"\"\"\n    mesh_path = Path(MESH_DIR) / \"arm.obj\"\n    if not os.path.exists(mesh_path):\n        raise FileNotFoundError(f\"Required arm mesh file not found: {mesh_path}\")\n\n    print(f\"Loading arm mesh from: {mesh_path}\")\n    return pv.read(mesh_path)\n\n\ndef check_ffmpeg():\n    \"\"\"Check if FFmpeg is available.\"\"\"\n    try:\n        subprocess.run(\n            [\"ffmpeg\", \"-version\"],\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE,\n            check=True,\n        )\n        return True\n    except (subprocess.SubprocessError, FileNotFoundError):\n        return False\n\n\ndef prepare_sequence_data(df: pl.DataFrame, sequence_id):\n    \"\"\"Prepare sequence data for rendering.\"\"\"\n    sequence_df = df.filter(pl.col(\"sequence_id\") == sequence_id).clone()\n    sequence_df = sequence_df.sort(\"sequence_counter\")\n\n    sequence_data = []\n    for row in sequence_df.iter_rows(named=True):\n        quat = [\n            float(row[col]) for col in [\"rot_w\", \"rot_x\", \"rot_y\", \"rot_z\"]\n        ]  # scalar_first order\n        acc = [float(row[col]) for col in [\"acc_x\", \"acc_y\", \"acc_z\"]]\n\n        sequence_data.append(\n            {\n                \"row\": row,\n                \"sequence_counter\": int(row[\"sequence_counter\"]),\n                \"row_id\": row[\"row_id\"],\n                \"quat\": quat,\n                \"acc\": acc,\n                \"gesture\": row[\"gesture\"],\n                \"behavior\": row[\"behavior\"],\n                \"orientation\": row[\"orientation\"],\n                \"phase\": row[\"phase\"],\n                \"sequence_type\": row[\"sequence_type\"],\n            }\n        )\n\n    return sequence_data, sequence_df[\"gesture\"][0]\n\n\ndef setup_plotter():\n    \"\"\"Setup PyVista plotter with consistent lighting and camera.\"\"\"\n    plotter = pv.Plotter(notebook=False, off_screen=True, window_size=WINDOW_SIZE)\n    plotter.set_background(COLORS[\"background\"], top=COLORS[\"background_top\"])\n    plotter.add_axes(interactive=True, line_width=2)  # type: ignore\n    plotter.camera_position = \"yz\"\n    plotter.camera.up = [0, 0, 1]  # type: ignore\n\n    # Remove default lights and add custom lighting\n    plotter.remove_all_lights()\n    light_configs = [\n        {\"position\": (0, 10, 10), \"color\": [1, 1, 1], \"intensity\": 0.7},\n        {\"position\": (10, -5, 0), \"color\": [0.9, 0.9, 1], \"intensity\": 0.5},\n        {\"position\": (-10, -10, -10), \"color\": [0.7, 0.7, 0.8], \"intensity\": 0.3},\n    ]\n\n    for config in light_configs:\n        light = pv.Light(\n            position=config[\"position\"],\n            focal_point=(0, 0, 0),\n            color=config[\"color\"],\n            intensity=config[\"intensity\"],\n        )\n        plotter.add_light(light)\n\n    return plotter\n\n\ndef create_sensor_subplot(fig, frame_data, sensor_num, subplot_pos):\n    \"\"\"Create a single ToF sensor visualization.\"\"\"\n    ax = plt.subplot(subplot_pos)\n    ax.set_facecolor(COLORS[\"figure_bg\"])\n\n    tof_data = extract_tof_data(frame_data[\"row\"], sensor_num)\n    ax.imshow(tof_data, cmap=plt.cm.plasma, vmin=0, vmax=254, interpolation=\"bilinear\")  # type: ignore\n    ax.set_title(\n        f\"ToF {sensor_num}\",\n        fontsize=10,\n        color=COLORS[\"text_secondary\"],\n        fontweight=\"medium\",\n    )\n    ax.set_xticks([])\n    ax.set_yticks([])\n\n    for spine in ax.spines.values():\n        spine.set_edgecolor(COLORS[\"spine\"])\n        spine.set_linewidth(0.8)\n\n\ndef create_thermopile_subplot(fig, frame_data, subplot_pos):\n    \"\"\"Create thermopile sensor visualization.\"\"\"\n    ax = plt.subplot(subplot_pos)\n    ax.set_facecolor(COLORS[\"figure_bg\"])\n\n    # Extract thermopile data\n    therm_data = []\n    for j in range(1, NUM_THERMOPILE_SENSORS + 1):\n        col_name = f\"thm_{j}\"\n        if col_name in frame_data[\"row\"].keys():\n            therm_data.append(float(frame_data[\"row\"][col_name]))\n        else:\n            therm_data.append(np.nan)\n\n    y_pos = np.arange(NUM_THERMOPILE_SENSORS)\n\n    # Normalize temperatures for color mapping (20-40°C range)\n    temp_min, temp_max = 20, 40\n    normalized_temps = [\n        (temp - temp_min) / (temp_max - temp_min) if not np.isnan(temp) else 0.5\n        for temp in therm_data\n    ]\n\n    # colors based on temperature values\n    colors = plt.cm.magma([max(0, min(1, norm_temp)) for norm_temp in normalized_temps])  # type: ignore\n\n    ax.barh(y_pos, therm_data, color=colors, height=0.7, edgecolor=\"none\")\n\n    ax.set_xlim(20, 40)  # Set min and max temperature range in Celsius\n\n    ax.set_yticks(y_pos)\n    ax.set_yticklabels(\n        [f\"{j + 1}\" for j in range(NUM_THERMOPILE_SENSORS)],\n        fontsize=10,\n        color=COLORS[\"text_secondary\"],\n    )\n    ax.set_title(\n        \"Thermopile\", fontsize=10, color=COLORS[\"text_secondary\"], fontweight=\"medium\"\n    )\n    ax.set_xlabel(\"°C\", fontsize=10, color=COLORS[\"text_tertiary\"])\n    ax.tick_params(\n        axis=\"both\", which=\"major\", labelsize=9, colors=COLORS[\"text_tertiary\"]\n    )\n    ax.grid(axis=\"x\", linestyle=\"--\", alpha=0.15, color=COLORS[\"text_tertiary\"])\n\n    for spine in ax.spines.values():\n        spine.set_edgecolor(COLORS[\"spine\"])\n        spine.set_linewidth(0.8)\n\n\ndef create_frame_info_text(frame_data):\n    \"\"\"Generate frame information text.\"\"\"\n    return (\n        f\"Subject: {frame_data['row']['subject']} | \"\n        f\"Gesture: {frame_data['gesture']} | \"\n        f\"Orientation: {frame_data['orientation']} | \"\n        f\"Phase: {frame_data['phase']} | \"\n        f\"Counter: {frame_data['sequence_counter']}\"\n    )\n\n\ndef render_frame_with_sensors(\n    plotter,\n    mesh,\n    original_points,\n    frame_data,\n    frame_index,\n    temp_dir,\n    acc_scale=0.02,\n    linear_acc_scale=0.05,\n    length_eps=1e-6,\n):\n    \"\"\"Render a single frame with sensor data.\"\"\"\n    plotter.clear_actors()\n\n    # rotate arm mesh\n    mesh.points = original_points.copy()\n    rot = R.from_quat(frame_data[\"quat\"], scalar_first=True)  # rot=[w, x, y, z]\n    R_mat = rot.as_matrix()\n    transform = create_transform_matrix(R_mat)\n    mesh.transform(transform, inplace=True)\n\n    # add arm mesh to plotter\n    plotter.add_mesh(\n        mesh,\n        color=COLORS[\"mesh\"],\n        specular=0.3,\n        specular_power=5,\n        ambient=0.5,\n        diffuse=0.6,\n        smooth_shading=False,\n    )\n\n    # plot acc vector in the world coordinate\n    raw_acc = np.array(frame_data[\"acc\"])  # [acc_x, acc_y, acc_z]\n    world_acc = rot.apply(raw_acc)\n    gravity = np.array([0.0, 0.0, 9.81])\n    linear_acc = world_acc - gravity\n\n    # world acc vector\n    arrow_length = np.linalg.norm(world_acc) * acc_scale\n    if arrow_length > length_eps:\n        arrow_dir = world_acc / arrow_length\n        arrow = pv.Arrow(start=(0, 0, 0), direction=arrow_dir, scale=arrow_length)  # type: ignore\n        plotter.add_mesh(arrow, color=\"red\", name=f\"acc_arrow_{frame_index}\")\n\n    # linear acc vector\n    arrow_length = np.linalg.norm(linear_acc) * linear_acc_scale\n    if arrow_length > length_eps:\n        arrow_dir = linear_acc / arrow_length\n        arrow = pv.Arrow(start=(0, 0, 0), direction=arrow_dir, scale=arrow_length)  # type: ignore\n        plotter.add_mesh(arrow, color=\"green\", name=f\"linear_acc_{frame_index}\")\n\n    # take a screenshot of the 3D view\n    pv_img = plotter.screenshot(return_img=True)\n\n    # create composite figure\n    fig = plt.figure(figsize=VIDEO_SIZE, dpi=DPI, facecolor=COLORS[\"figure_bg\"])\n    gs = plt.GridSpec(2, 1, height_ratios=[2, 1], figure=fig)  # type: ignore\n\n    ax_3d = plt.subplot(gs[0])\n    ax_3d.imshow(pv_img)\n    ax_3d.set_xticks([])\n    ax_3d.set_yticks([])\n    ax_3d.axis(\"off\")\n    ax_3d.set_title(\n        create_frame_info_text(frame_data),\n        fontsize=11,\n        color=COLORS[\"text\"],\n        backgroundcolor=COLORS[\"title_bg\"],\n        pad=10,\n        fontweight=\"medium\",\n    )\n\n    # sensor subplots\n    gs_sensors = plt.GridSpec(1, 6, wspace=0.3, figure=fig)  # type: ignore\n    gs_sensors.update(top=0.45, bottom=0.05, left=0.05, right=0.95)\n\n    for j in range(NUM_TOF_SENSORS):\n        create_sensor_subplot(fig, frame_data, j + 1, gs_sensors[0, j])\n\n    create_thermopile_subplot(fig, frame_data, gs_sensors[0, 5])\n\n    # save frame\n    frame_file = os.path.join(temp_dir, f\"frame_{frame_index:05d}.png\")\n    plt.savefig(\n        frame_file,\n        facecolor=COLORS[\"figure_bg\"],\n        bbox_inches=\"tight\",\n        pad_inches=0.1,\n        dpi=DPI,\n    )\n    plt.close(fig)\n    return frame_file\n\n\ndef create_video_from_frames(frame_files, output_file, framerate, temp_dir):\n    \"\"\"Combine frames into video using FFmpeg.\"\"\"\n    print(f\"Combining frames into video: {output_file}\")\n    os.makedirs(os.path.dirname(os.path.abspath(output_file)), exist_ok=True)\n\n    ffmpeg_cmd = [\n        \"ffmpeg\",\n        \"-y\",\n        \"-framerate\",\n        str(framerate),\n        \"-i\",\n        os.path.join(temp_dir, \"frame_%05d.png\"),\n        \"-c:v\",\n        \"libx264\",\n        \"-profile:v\",\n        \"high\",\n        \"-crf\",\n        \"18\",\n        \"-pix_fmt\",\n        \"yuv420p\",\n        \"-vf\",\n        \"scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2,setsar=1:1\",\n        output_file,\n    ]\n\n    try:\n        subprocess.run(ffmpeg_cmd, check=True)\n        print(f\"Video successfully created: {output_file}\")\n\n        # Cleanup\n        for file in frame_files:\n            os.remove(file)\n        os.rmdir(temp_dir)\n\n    except subprocess.SubprocessError as e:\n        print(f\"Error creating video with FFmpeg: {e}\")\n        print(f\"Individual frames are saved in {temp_dir}\")\n\n\ndef render_animation(sequence_data, output_file, framerate=10):\n    \"\"\"Main rendering function with sensor visualizations.\"\"\"\n    if not check_ffmpeg():\n        raise RuntimeError(\n            \"FFmpeg is required but not found in system PATH. Please install FFmpeg.\"\n        )\n\n    mesh = load_arm_mesh()\n    mesh = align_mesh_to_sensor_axes(mesh)  # Align mesh to sensor axes\n    original_points = mesh.points.copy()\n    temp_dir = tempfile.mkdtemp()\n    print(f\"Creating composite frames in: {temp_dir}\")\n\n    plotter = setup_plotter()\n    frame_files = []\n\n    print(f\"Rendering {len(sequence_data)} frames...\")\n    for i, frame_data in enumerate(tqdm(sequence_data, desc=\"Creating frames\")):\n        frame_file = render_frame_with_sensors(\n            plotter, mesh, original_points, frame_data, i, temp_dir\n        )\n        frame_files.append(frame_file)\n\n    plotter.close()\n    create_video_from_frames(frame_files, output_file, framerate, temp_dir)\n\n\ndef main():\n    parser = argparse.ArgumentParser(\n        description=\"Render 3D motion data animation from sensor data\"\n    )\n    parser.add_argument(\"--subject\", help=\"Subject ID to filter (e.g., SUBJ_059520)\")\n    parser.add_argument(\"--gesture\", help=\"Gesture description to filter\")\n    parser.add_argument(\"--behavior\", help=\"Behavior description to filter\")\n    parser.add_argument(\"--phase\", help=\"Phase to filter\")\n    parser.add_argument(\"--orientation\", help=\"Orientation to filter\")\n    parser.add_argument(\n        \"--handedness\",\n        type=int,\n        default=1,\n        help=\"Handedness to filter (0=left, 1=right)\",\n    )\n    parser.add_argument(\n        \"--framerate\", type=int, default=10, help=\"Frame rate for output video\"\n    )\n    parser.add_argument(\n        \"--sequence_index\",\n        type=int,\n        default=0,\n        help=\"Index of the sequence to render (if multiple sequences match)\",\n    )\n    parser.add_argument(\n        \"--no-cache\", action=\"store_true\", help=\"Disable parquet caching\"\n    )\n\n    args = parser.parse_args()\n    os.makedirs(OUTPUT_DIR, exist_ok=True)\n\n    if not check_ffmpeg():\n        raise RuntimeError(\n            \"FFmpeg is required but not found in system PATH. Please install it:\\n\"\n            \"  - Windows: Download from https://ffmpeg.org/download.html and add to PATH\\n\"\n            \"  - macOS: Run 'brew install ffmpeg' (requires Homebrew)\\n\"\n            \"  - Linux: Run 'sudo apt install ffmpeg' or equivalent for your distro\"\n        )\n\n    # Prepare filters\n    filters = {\n        \"subject\": args.subject,\n        \"gesture\": args.gesture,\n        \"behavior\": args.behavior,\n        \"phase\": args.phase,\n        \"orientation\": args.orientation,\n        \"handedness\": args.handedness,\n    }\n    print(\"👀 Applying filters:\")\n    for key, value in filters.items():\n        if value is not None:\n            print(f\"  {key}: {value}\")\n\n    df, unique_sequence_ids = load_data(\n        CSV_PATH, DEMOGRAPHIC_PATH, filters, use_cache=not args.no_cache\n    )\n\n    # Select sequence\n    if args.sequence_index >= len(unique_sequence_ids):\n        print(\n            f\"Warning: Selected index {args.sequence_index} out of range. Using first sequence.\"\n        )\n        selected_sequence_id = unique_sequence_ids[0]\n    else:\n        selected_sequence_id = unique_sequence_ids[args.sequence_index]\n\n    sequence_data, gesture_name = prepare_sequence_data(df, selected_sequence_id)\n\n    # output filename\n    subject = (\n        args.subject\n        or df.filter(pl.col(\"sequence_id\") == selected_sequence_id)[\"subject\"][0]\n    )\n    safe_gesture_name = gesture_name.replace(\" - \", \"_\").replace(\" \", \"_\")\n    safe_gesture_name = sanitize_filename(safe_gesture_name)\n    output_file = os.path.join(\n        OUTPUT_DIR, f\"{subject}_{safe_gesture_name}_{selected_sequence_id}.mp4\"\n    )\n\n    render_animation(sequence_data, output_file, args.framerate)\n\n    print(\"Animation complete!\")\n    print(f\"Output file: {output_file}\")\n\n\nif __name__ == \"__main__\":\n    import os\n    import pyvista as pv\n    from pathlib import Path\n\n    \n    pv.start_xvfb()\n    \n    CSV_PATH = Path(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train.csv\")\n    DEMOGRAPHIC_PATH = Path(\"/kaggle/input/cmi-detect-behavior-with-sensor-data/train_demographics.csv\")\n    MESH_DIR = Path(\"/kaggle/input/clone-herios-data-visualizer/helios-render-kaggle\")\n    OUTPUT_DIR = Path(\"movie\")\n    CACHE_DIR = Path(\"/tmp/cache\")\n    main()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-06T12:39:16.265715Z","iopub.execute_input":"2025-06-06T12:39:16.266079Z","iopub.status.idle":"2025-06-06T12:39:16.294513Z","shell.execute_reply.started":"2025-06-06T12:39:16.266052Z","shell.execute_reply":"2025-06-06T12:39:16.293356Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"NON_TARGET_GESTURES = [\n    \"Drink from bottle/cup\",\n    \"Feel around in tray and pull out an object\",\n    \"Glasses on/off\",\n    \"Pinch knee/leg skin\",\n    \"Pull air toward your face\",\n    \"Scratch knee/leg skin\",\n    \"Text on phone\",\n    \"Wave hello\",\n    \"Write name in air\",\n    \"Write name on leg\",\n]\nTARGET_GESTURES = [\n    \"Above ear - pull hair\",\n    \"Cheek - pinch skin\",\n    \"Eyebrow - pull hair\",\n    \"Eyelash - pull hair\",\n    \"Forehead - pull hairline\",\n    \"Forehead - scratch\",\n    \"Neck - pinch skin\",\n    \"Neck - scratch\",\n]\nGESTURES = NON_TARGET_GESTURES + TARGET_GESTURES\nORIENTATIONS = [\n    \"Lie on Back\",\n    \"Lie on Side - Non Dominant\",\n    \"Seated Lean Non Dom - FACE DOWN\",\n    \"Seated Straight\",\n]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-06T12:39:16.301216Z","iopub.execute_input":"2025-06-06T12:39:16.301586Z","iopub.status.idle":"2025-06-06T12:39:16.319166Z","shell.execute_reply.started":"2025-06-06T12:39:16.301559Z","shell.execute_reply":"2025-06-06T12:39:16.318066Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import subprocess\n\nfor orientation in ORIENTATIONS:\n    for gesture in GESTURES:\n        try:\n            command = [\n                \"python\",\n                \"helios_render.py\",\n                \"--gesture\", gesture,\n                \"--orientation\", orientation,\n            ]\n            subprocess.run(command, check=True)\n        except:\n            print(\"⚠️ Failed to generate movie. Skipping...\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-06-06T12:40:19.272108Z","iopub.execute_input":"2025-06-06T12:40:19.272502Z","execution_failed":"2025-06-06T12:42:33.852Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"! rm *.py","metadata":{"trusted":true,"execution":{"execution_failed":"2025-06-06T12:42:33.852Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!ls -l","metadata":{"trusted":true,"execution":{"execution_failed":"2025-06-06T12:42:33.853Z"}},"outputs":[],"execution_count":null}]}