diff --git a/.github/workflows/build_installers.yml b/.github/workflows/build_installers.yml new file mode 100644 index 0000000..516eb07 --- /dev/null +++ b/.github/workflows/build_installers.yml @@ -0,0 +1,136 @@ +name: Build Installers + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: + +jobs: + build: + name: Build for ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + include: + - os: ubuntu-latest + godot_executable: godot + godot_export_preset: "Linux/X11" + artifact_name: AvatarStream-Linux + python_binary_name: holistic_tracker + launcher_name: run + zip_name: AvatarStream-Linux.zip + export_output: AvatarStream.x86_64 + - os: windows-latest + godot_executable: godot.exe + godot_export_preset: "Windows Desktop" + artifact_name: AvatarStream-Windows + python_binary_name: holistic_tracker.exe + launcher_name: run.exe + zip_name: AvatarStream-Windows.zip + export_output: AvatarStream.exe + - os: macos-latest + godot_executable: godot + godot_export_preset: "macOS" + artifact_name: AvatarStream-macOS + python_binary_name: holistic_tracker + launcher_name: run + zip_name: AvatarStream-macOS.zip + export_output: AvatarStream.zip + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Setup Godot + uses: chickensoft-games/setup-godot@v1 + with: + version: 4.3.0 + use-dotnet: false + + - name: Install Dependencies + run: | + pip install -r game/AvatarStream/scripts/python/requirements.txt + pip install pyinstaller + + - name: Build Python Tracker + run: | + pyinstaller --onefile --distpath dist --workpath build/pyinstaller --specpath build/pyinstaller game/AvatarStream/scripts/python/holistic_tracker.py + + - name: Build Launcher + run: | + pyinstaller --onefile --distpath dist --workpath build/pyinstaller --specpath build/pyinstaller run.py + + - name: Download Godot Export Templates + run: | + mkdir -p ~/.local/share/godot/export_templates/4.3.stable + curl -L https://github.com/godotengine/godot/releases/download/4.3-stable/Godot_v4.3-stable_export_templates.tpz -o export_templates.tpz + unzip -q export_templates.tpz -d temp_templates + mv temp_templates/templates/* ~/.local/share/godot/export_templates/4.3.stable/ + rm -rf temp_templates export_templates.tpz + if: runner.os != 'Windows' + + - name: Download Godot Export Templates (Windows) + run: | + New-Item -ItemType Directory -Force -Path "$env:APPDATA\Godot\export_templates\4.3.stable" + Invoke-WebRequest -Uri https://github.com/godotengine/godot/releases/download/4.3-stable/Godot_v4.3-stable_export_templates.tpz -OutFile export_templates.tpz + Expand-Archive export_templates.tpz -DestinationPath temp_templates + Move-Item -Path "temp_templates\templates\*" -Destination "$env:APPDATA\Godot\export_templates\4.3.stable" -Force + Remove-Item -Recurse -Force temp_templates + Remove-Item export_templates.tpz + if: runner.os == 'Windows' + + - name: Create Build Directory + run: mkdir -p build/${{ matrix.artifact_name }} + + - name: Export Godot Project + run: | + godot --headless --path game/AvatarStream --export-release "${{ matrix.godot_export_preset }}" ../../build/${{ matrix.artifact_name }}/${{ matrix.export_output }} + if: runner.os != 'Windows' + + - name: Export Godot Project (Windows) + run: | + godot --headless --path game/AvatarStream --export-release "${{ matrix.godot_export_preset }}" ../../build/${{ matrix.artifact_name }}/${{ matrix.export_output }} + if: runner.os == 'Windows' + + - name: Organize Files (Linux) + if: runner.os == 'Linux' + run: | + cp dist/${{ matrix.python_binary_name }} build/${{ matrix.artifact_name }}/ + cp dist/${{ matrix.launcher_name }} build/${{ matrix.artifact_name }}/ + chmod +x build/${{ matrix.artifact_name }}/${{ matrix.launcher_name }} + chmod +x build/${{ matrix.artifact_name }}/${{ matrix.python_binary_name }} + chmod +x build/${{ matrix.artifact_name }}/${{ matrix.export_output }} + + - name: Organize Files (macOS) + if: runner.os == 'macOS' + run: | + cp dist/${{ matrix.python_binary_name }} build/${{ matrix.artifact_name }}/ + cp dist/${{ matrix.launcher_name }} build/${{ matrix.artifact_name }}/ + + # Unzip the Godot export + unzip -q "build/${{ matrix.artifact_name }}/${{ matrix.export_output }}" -d "build/${{ matrix.artifact_name }}/" + rm "build/${{ matrix.artifact_name }}/${{ matrix.export_output }}" + + # Ensure executable permissions + chmod +x build/${{ matrix.artifact_name }}/${{ matrix.launcher_name }} + chmod +x build/${{ matrix.artifact_name }}/${{ matrix.python_binary_name }} + + - name: Organize Files (Windows) + if: runner.os == 'Windows' + run: | + Copy-Item "dist\${{ matrix.python_binary_name }}" "build\${{ matrix.artifact_name }}\" + Copy-Item "dist\${{ matrix.launcher_name }}" "build\${{ matrix.artifact_name }}\" + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.artifact_name }} + path: build/${{ matrix.artifact_name }} diff --git a/game/AvatarStream/export_presets.cfg b/game/AvatarStream/export_presets.cfg index adb1176..7886d78 100644 --- a/game/AvatarStream/export_presets.cfg +++ b/game/AvatarStream/export_presets.cfg @@ -1,51 +1,139 @@ [preset.0] -name = "macOS" -platform = "macOS" -runnable = true -export_path = "build/macos/AvatarStream.dmg" -features = "universal" -binary_format = "universal" +name="macOS" +platform="macOS" +runnable=true +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="build/macos/AvatarStream.zip" +encryption_include_filters="" +encryption_exclude_filters="" +encrypt_pck=false +encrypt_directory=false +script_export_mode=1 [preset.0.options] -"application/name" = "AvatarStream" -"application/info" = "An application for streaming avatar animations." -"application/icon" = "res://icon.svg" -"application/identifier" = "com.example.avatarstream" -"application/signature" = "" -"application/short_version" = "1.0" -"application/version" = "1.0" -"application/copyright" = "" -"application/sandbox" = true -"display/high_res" = true -"privacy/camera_usage_description" = "This app uses the camera to track your movements and animate the avatar." -"privacy/microphone_usage_description" = "" -"privacy/speech_recognition_usage_description" = "" -"privacy/photos_library_usage_description" = "" -"privacy/contacts_usage_description" = "" -"privacy/calendars_usage_description" = "" -"privacy/reminders_usage_description" = "" -"privacy/location_usage_description" = "" -"privacy/location_always_usage_description" = "" -"privacy/motion_usage_description" = "" -"privacy/nfc_reader_usage_description" = "" -"privacy/bluetooth_always_usage_description" = "" -"privacy/bluetooth_peripheral_usage_description" = "" -"privacy/file_access/downloads_folder" = "read_write" -"privacy/file_access/pictures_folder" = "read_write" -"privacy/file_access/music_folder" = "read_write" -"privacy/file_access/movies_folder" = "read_write" -"privacy/file_access/documents_folder" = "read_write" -"privacy/file_access/user_selected_folder" = "read_write" -# IMPORTANT: Replace the placeholder values below with your own Apple Developer account information. -# You can find your signing identity in the Keychain Access app. -"codesign/identity" = "" -"codesign/timestamp" = true -"codesign/hardened_runtime" = true -"codesign/entitlements" = "" -# The notarization settings below are for Godot's built-in notarization. -# If you use the notarize.sh script, you can leave these blank. -"notarize/api_key" = "" -"notarize/api_key_id" = "" -"notarize/api_issuer" = "" +custom_template/debug="" +custom_template/release="" +application/name="AvatarStream" +application/info="An application for streaming avatar animations." +application/icon="res://icon.svg" +application/identifier="com.example.avatarstream" +application/signature="" +application/short_version="1.0" +application/version="1.0" +application/copyright="" +application/category_type=1 +display/high_res=true +privacy/camera_usage_description="This app uses the camera to track your movements and animate the avatar." +privacy/microphone_usage_description="" +codesign/identity="" +codesign/timestamp=true +codesign/hardened_runtime=true +codesign/entitlements="" +codesign/custom_options=PackedStringArray() +notarization/apple_id_name="" +notarization/apple_id_password="" +notarization/team_id="" + +[preset.1] + +name="Windows Desktop" +platform="Windows Desktop" +runnable=true +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="build/windows/AvatarStream.exe" +encryption_include_filters="" +encryption_exclude_filters="" +encrypt_pck=false +encrypt_directory=false +script_export_mode=1 + +[preset.1.options] + +custom_template/debug="" +custom_template/release="" +binary_format/embed_pck=true +texture_format/bptc=true +texture_format/s3tc=true +texture_format/etc=false +texture_format/etc2=false +texture_format/no_bptc_fallbacks=true +codesign/enable=false +codesign/timestamp=true +codesign/identity_type=0 +codesign/identity="" +codesign/password="" +application/modify_resources=true +application/icon="res://icon.svg" +application/console_wrapper_icon="res://icon.svg" +application/icon_interpolation=4 +application/file_version="" +application/product_version="" +application/company_name="" +application/product_name="" +application/file_description="" +application/copyright="" +application/trademarks="" +ssh_remote_deploy/enabled=false +ssh_remote_deploy/host="user@host_ip" +ssh_remote_deploy/port="22" +ssh_remote_deploy/extra_args_ssh="" +ssh_remote_deploy/extra_args_scp="" +ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}' +$action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}' +$trigger = New-ScheduledTaskTrigger -Once -At 00:00 +$settings = New-ScheduledTaskSettingsSet +$task = Register-ScheduledTask -Action $action -Trigger $trigger -Settings $settings +Start-ScheduledTask -TaskName $task.TaskName +Wait-Process -Name {exe_name} -ErrorAction SilentlyContinue +Unregister-ScheduledTask -TaskName $task.TaskName -Confirm:$false +Remove-Item -Recurse -Force '{temp_dir}'" +ssh_remote_deploy/cleanup_script="Remove-Item -Recurse -Force '{temp_dir}'" + +[preset.2] + +name="Linux/X11" +platform="Linux/X11" +runnable=true +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="build/linux/AvatarStream.x86_64" +encryption_include_filters="" +encryption_exclude_filters="" +encrypt_pck=false +encrypt_directory=false +script_export_mode=1 + +[preset.2.options] + +custom_template/debug="" +custom_template/release="" +binary_format/embed_pck=true +texture_format/bptc=true +texture_format/s3tc=true +texture_format/etc=false +texture_format/etc2=false +texture_format/no_bptc_fallbacks=true +ssh_remote_deploy/enabled=false +ssh_remote_deploy/host="user@host_ip" +ssh_remote_deploy/port="22" +ssh_remote_deploy/extra_args_ssh="" +ssh_remote_deploy/extra_args_scp="" +ssh_remote_deploy/run_script="#!/usr/bin/env bash +export DISPLAY=:0 +unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\" +\"{temp_dir}/{exe_name}\" {cmd_args} +rm -rf \"{temp_dir}\"" +ssh_remote_deploy/cleanup_script="rm -rf \"{temp_dir}\"" diff --git a/game/AvatarStream/scripts/python/requirements.txt b/game/AvatarStream/scripts/python/requirements.txt index 3fbe35c..106f8d9 100644 --- a/game/AvatarStream/scripts/python/requirements.txt +++ b/game/AvatarStream/scripts/python/requirements.txt @@ -6,6 +6,7 @@ mediapipe pyvirtualcam pytest pytest-mock +pyinstaller # The following libraries are needed for the full implementation. # The exact package names and versions might vary. diff --git a/game/AvatarStream/scripts/python/tests/test_protocol.py b/game/AvatarStream/scripts/python/tests/test_protocol.py index 9dd1982..247277b 100644 --- a/game/AvatarStream/scripts/python/tests/test_protocol.py +++ b/game/AvatarStream/scripts/python/tests/test_protocol.py @@ -3,6 +3,7 @@ import struct import threading import time +import os import numpy as np # Mock implementation of the binary protocol diff --git a/run.py b/run.py index 651ed63..9e17ac5 100644 --- a/run.py +++ b/run.py @@ -11,6 +11,10 @@ def get_os(): return platform.system() def check_dependencies(): + # Skip dependency check if frozen (dependencies are bundled) + if getattr(sys, 'frozen', False): + return True + print("Checking dependencies...") try: import cv2 @@ -30,24 +34,6 @@ def check_dependencies(): print(f"Failed to install dependencies. Please manually run: pip install -r {requirements_path}") return False -def run_python_tracker(python_script_path): - print(f"Starting Python tracker: {python_script_path}") - try: - subprocess.run([sys.executable, python_script_path], check=True) - except subprocess.CalledProcessError as e: - print(f"Python tracker exited with error: {e}") - except KeyboardInterrupt: - pass - -def run_godot(godot_path, project_path): - print(f"Starting Godot: {godot_path} --path {project_path}") - try: - subprocess.run([godot_path, "--path", project_path], check=True) - except subprocess.CalledProcessError as e: - print(f"Godot exited with error: {e}") - except KeyboardInterrupt: - pass - def main(): parser = argparse.ArgumentParser(description="Unified Launcher for AvatarStream") parser.add_argument("--godot-path", type=str, help="Path to Godot executable", default="godot") @@ -56,35 +42,70 @@ def main(): os_name = get_os() print(f"Detected OS: {os_name}") + is_frozen = getattr(sys, 'frozen', False) + if not check_dependencies(): sys.exit(1) - python_script = os.path.join("game", "AvatarStream", "scripts", "python", "holistic_tracker.py") - project_path = os.path.join("game", "AvatarStream") + if is_frozen: + # Running in frozen mode (compiled executable) + base_dir = os.path.dirname(sys.executable) - if not os.path.exists(python_script): - print(f"Error: Python script not found at {python_script}") - sys.exit(1) + if os_name == "Windows": + tracker_cmd = [os.path.join(base_dir, "holistic_tracker.exe")] + godot_executable = os.path.join(base_dir, "AvatarStream.exe") + godot_cmd = [godot_executable] + elif os_name == "Darwin": # macOS + # In macOS app bundle structure might be different, but assuming side-by-side for now + # or inside Contents/MacOS + tracker_cmd = [os.path.join(base_dir, "holistic_tracker")] + # If we are inside an App bundle, finding the Godot app might be different + # But for simplicity, let's assume flat structure or adjust during build + godot_executable = os.path.join(base_dir, "AvatarStream.app", "Contents", "MacOS", "AvatarStream") + if not os.path.exists(godot_executable): + # Fallback for side-by-side non-app + godot_executable = os.path.join(base_dir, "AvatarStream") + godot_cmd = [godot_executable] + else: # Linux + tracker_cmd = [os.path.join(base_dir, "holistic_tracker")] + godot_executable = os.path.join(base_dir, "AvatarStream.x86_64") + godot_cmd = [godot_executable] - # Launch Python tracker in a separate thread/process - # We use a thread for simplicity here, but in a real scenario, separate processes are better managed. - # However, since we want to kill both, let's use subprocess.Popen and manage them. + else: + # Running in development mode + python_script = os.path.join("game", "AvatarStream", "scripts", "python", "holistic_tracker.py") + project_path = os.path.join("game", "AvatarStream") - tracker_process = subprocess.Popen([sys.executable, python_script]) + if not os.path.exists(python_script): + print(f"Error: Python script not found at {python_script}") + sys.exit(1) - # Launch Godot - # We wait for Godot to exit, then we kill the tracker. - godot_process = None + tracker_cmd = [sys.executable, python_script] + godot_cmd = [args.godot_path, "--path", project_path] + # Launch Python tracker + print(f"Starting Python tracker: {tracker_cmd}") + try: + tracker_process = subprocess.Popen(tracker_cmd) + except FileNotFoundError: + print(f"Error: Tracker executable not found: {tracker_cmd}") + sys.exit(1) + + # Launch Godot # Set environment variable to tell Godot it's launched by the runner env = os.environ.copy() env["AVATARSTREAM_LAUNCHED_BY_RUNNER"] = "1" + print(f"Starting Godot: {godot_cmd}") + godot_process = None try: - godot_process = subprocess.Popen([args.godot_path, "--path", project_path], env=env) + godot_process = subprocess.Popen(godot_cmd, env=env) godot_process.wait() except FileNotFoundError: - print(f"Error: Godot executable not found at '{args.godot_path}'. Please provide the correct path using --godot-path.") + if is_frozen: + print(f"Error: Godot executable not found at '{godot_cmd[0]}'.") + else: + print(f"Error: Godot executable not found at '{args.godot_path}'. Please provide the correct path using --godot-path.") except KeyboardInterrupt: print("\nExiting...") finally: