diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..0c3fb1a --- /dev/null +++ b/.coveragerc @@ -0,0 +1,6 @@ +[run] +omit= + tests/* + conftest.py +source = + EyeTrackApp diff --git a/EyeTrackApp/__init__.py b/EyeTrackApp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/EyeTrackApp/config.py b/EyeTrackApp/config.py index f98cba6..e8a2c31 100644 --- a/EyeTrackApp/config.py +++ b/EyeTrackApp/config.py @@ -1,12 +1,10 @@ import json import os.path import shutil -from eye import EyeId +from EyeTrackApp.eye import EyeId from pydantic import BaseModel from typing import Union, List -import psutil, os -import sys - +import os CONFIG_FILE_NAME: str = "eyetrack_settings.json" @@ -30,7 +28,6 @@ class EyeTrackCameraConfig(BaseModel): calibration_points: List[List[Union[float, None]]] = [] - class EyeTrackSettingsConfig(BaseModel): gui_flip_x_axis_left: bool = False gui_flip_x_axis_right: bool = False diff --git a/EyeTrackApp/osc.py b/EyeTrackApp/osc.py index 3bb3c31..02be6da 100644 --- a/EyeTrackApp/osc.py +++ b/EyeTrackApp/osc.py @@ -1,13 +1,12 @@ from pythonosc import udp_client from pythonosc import osc_server from pythonosc import dispatcher -from config import EyeTrackConfig -from utils.misc_utils import PlaySound, SND_FILENAME, SND_ASYNC +from EyeTrackApp.config import EyeTrackConfig +from EyeTrackApp.utils.misc_utils import PlaySound, SND_FILENAME, SND_ASYNC +from enum import IntEnum import queue import threading -from enum import IntEnum import time -import numpy as np class EyeId(IntEnum): @@ -30,13 +29,8 @@ falloff = False def output_osc(eye_x, eye_y, eye_blink, last_blink, pupil_dilation, avg_velocity, self): - # print(pupil_dilation) global se, falloff - # self.config.gui_osc_vrcft_v2 - # self.config.gui_osc_vrcft_v1 - # self.config.gui_vrc_native - if self.config.gui_osc_vrcft_v1: if self.main_config.eye_display_id in [ @@ -48,9 +42,6 @@ def output_osc(eye_x, eye_y, eye_blink, last_blink, pupil_dilation, avg_velocity self.client.send_message(self.config.osc_left_eye_x_address, eye_x) self.client.send_message(self.config.osc_right_eye_x_address, eye_x) self.client.send_message(self.config.osc_eyes_y_address, eye_y) - - self.config.osc_left_eye_close_address - self.client.send_message( self.config.osc_right_eye_close_address, eyelid_transformer(self, eye_blink), @@ -68,9 +59,7 @@ def output_osc(eye_x, eye_y, eye_blink, last_blink, pupil_dilation, avg_velocity self.l_eye_velocity = avg_velocity if self.l_eye_blink == 0.0: - if ( - last_blink > 0.15 - ): # when binary blink is on, blinks may be too fast for OSC so we repeat them. + if last_blink > 0.15: # when binary blink is on, blinks may be too fast for OSC so we repeat them. for i in range(4): self.client.send_message( self.config.osc_left_eye_close_address, @@ -94,9 +83,7 @@ def output_osc(eye_x, eye_y, eye_blink, last_blink, pupil_dilation, avg_velocity self.l_eye_velocity = avg_velocity if self.r_eye_blink == 0.0: - if ( - last_blink > 0.15 - ): # when binary blink is on, blinks may be too fast for OSC so we repeat them. + if last_blink > 0.15: # when binary blink is on, blinks may be too fast for OSC so we repeat them. # print("REPEATING R BLINK") for i in range(4): self.client.send_message( @@ -105,9 +92,7 @@ def output_osc(eye_x, eye_y, eye_blink, last_blink, pupil_dilation, avg_velocity ) last_blink = time.time() - last_blink if self.config.gui_outer_side_falloff: - if ( - self.l_eye_blink == 0.0 - ): # if both eyes closed and DEF is enables, blink + if self.l_eye_blink == 0.0: # if both eyes closed and DEF is enables, blink self.client.send_message( self.config.osc_left_eye_close_address, eyelid_transformer(self, self.r_eye_blink), @@ -127,11 +112,7 @@ def output_osc(eye_x, eye_y, eye_blink, last_blink, pupil_dilation, avg_velocity eyelid_transformer(self, self.r_eye_blink), ) - if ( - self.main_config.eye_display_id in [EyeId.BOTH] - and self.right_y != 621 - and self.left_y != 621 - ): + if self.main_config.eye_display_id in [EyeId.BOTH] and self.right_y != 621 and self.left_y != 621: y = (self.right_y + self.left_y) / 2 self.client.send_message(self.config.osc_eyes_y_address, y) @@ -144,10 +125,7 @@ def output_osc(eye_x, eye_y, eye_blink, last_blink, pupil_dilation, avg_velocity se = True self.client.send_message("/avatar/parameters/v2/EyeX", eye_x) - self.client.send_message("/avatar/parameters/v2/EyeLeftX", eye_x) - self.client.send_message("/avatar/parameters/v2/EyeRightX", eye_x) - self.client.send_message("/avatar/parameters/v2/EyeLeftY", eye_y) - self.client.send_message("/avatar/parameters/v2/EyeRightY", eye_y) + self.client.send_message("/avatar/parameters/v2/EyeY", eye_y) self.client.send_message( "/avatar/parameters/v2/EyeLid", eyelid_transformer(self, eye_blink), @@ -161,9 +139,7 @@ def output_osc(eye_x, eye_y, eye_blink, last_blink, pupil_dilation, avg_velocity self.r_eye_velocity = avg_velocity if self.l_eye_blink == 0.0: - if ( - last_blink > 0.15 - ): # when binary blink is on, blinks may be too fast for OSC so we repeat them. + if last_blink > 0.15: # when binary blink is on, blinks may be too fast for OSC so we repeat them. for i in range(4): self.client.send_message( "/avatar/parameters/v2/EyeLidLeft", @@ -171,9 +147,7 @@ def output_osc(eye_x, eye_y, eye_blink, last_blink, pupil_dilation, avg_velocity ) last_blink = time.time() - last_blink if self.config.gui_outer_side_falloff: - if ( - self.r_eye_blink == 0.0 - ): # if both eyes closed and DEF is enables, blink + if self.r_eye_blink == 0.0: # if both eyes closed and DEF is enables, blink self.client.send_message( "/avatar/parameters/v2/EyeLidLeft", eyelid_transformer(self, self.l_eye_blink), @@ -188,12 +162,10 @@ def output_osc(eye_x, eye_y, eye_blink, last_blink, pupil_dilation, avg_velocity self.left_y = eye_y if self.left_y != 621: - self.client.send_message( - "/avatar/parameters/FT/v2/EyeLeftY", self.left_y - ) + self.client.send_message("/avatar/parameters/v2/EyeLeftY", self.left_y) self.client.send_message( - "/avatar/parameters/FT/v2/EyeLidLeft", + "/avatar/parameters/v2/EyeLidLeft", eyelid_transformer(self, self.l_eye_blink), ) @@ -203,10 +175,7 @@ def output_osc(eye_x, eye_y, eye_blink, last_blink, pupil_dilation, avg_velocity self.r_eye_velocity = avg_velocity if self.r_eye_blink == 0.0: - if ( - last_blink > 0.15 - ): # when binary blink is on, blinks may be too fast for OSC so we repeat them. - # print("REPEATING R BLINK") + if last_blink > 0.15: # when binary blink is on, blinks may be too fast for OSC so we repeat them. for i in range(4): self.client.send_message( "/avatar/parameters/v2/EyeLidRight", @@ -214,9 +183,7 @@ def output_osc(eye_x, eye_y, eye_blink, last_blink, pupil_dilation, avg_velocity ) last_blink = time.time() - last_blink if self.config.gui_outer_side_falloff: - if ( - self.l_eye_blink == 0.0 - ): # if both eyes closed and DEF is enables, blink + if self.l_eye_blink == 0.0: # if both eyes closed and DEF is enables, blink self.client.send_message( "/avatar/parameters/v2/EyeLidLeft", eyelid_transformer(self, self.r_eye_blink), @@ -232,23 +199,13 @@ def output_osc(eye_x, eye_y, eye_blink, last_blink, pupil_dilation, avg_velocity self.right_y = eye_y if self.right_y != 621: - self.client.send_message( - "/avatar/parameters/v2/EyeRightY", self.right_y - ) + self.client.send_message("/avatar/parameters/v2/EyeRightY", self.right_y) self.client.send_message( "/avatar/parameters/v2/EyeLidRight", eyelid_transformer(self, self.r_eye_blink), ) - if ( - self.main_config.eye_display_id in [EyeId.BOTH] - and self.right_y != 621 - and self.left_y != 621 - ): - y = (self.right_y + self.left_y) / 2 - self.client.send_message("/avatar/parameters/v2/EyeY", y) - if self.config.gui_vrc_native: # VRC NATIVE if self.main_config.eye_display_id in [ @@ -257,20 +214,14 @@ def output_osc(eye_x, eye_y, eye_blink, last_blink, pupil_dilation, avg_velocity ]: # we are in single eye mode se = True if eye_blink == 0.0: - if ( - last_blink > 0.2 - ): # when binary blink is on, blinks may be too fast for OSC so we repeat them. + if last_blink > 0.2: # when binary blink is on, blinks may be too fast for OSC so we repeat them. for i in range(5): - self.client.send_message( - "/tracking/eye/EyesClosedAmount", float(1 - eye_blink) - ) + self.client.send_message("/tracking/eye/EyesClosedAmount", float(1 - eye_blink)) eye_blink += 0.02 # TODO finish tuning value last_blink = time.time() - last_blink else: - self.client.send_message( - "/tracking/eye/EyesClosedAmount", float(1 - eye_blink) - ) + self.client.send_message("/tracking/eye/EyesClosedAmount", float(1 - eye_blink)) self.client.send_message( "/tracking/eye/LeftRightVec", [float(eye_x), float(eye_y), 1.0, float(eye_x), float(eye_y), 1.0], @@ -290,21 +241,13 @@ def output_osc(eye_x, eye_y, eye_blink, last_blink, pupil_dilation, avg_velocity ) if self.l_eye_blink == 0.0: - if ( - last_blink > 0.2 - ): # when binary blink is on, blinks may be too fast for OSC so we repeat them. + if last_blink > 0.2: # when binary blink is on, blinks may be too fast for OSC so we repeat them. for i in range(5): - self.client.send_message( - "/tracking/eye/EyesClosedAmount", float(1 - eye_blink) - ) + self.client.send_message("/tracking/eye/EyesClosedAmount", float(1 - eye_blink)) last_blink = time.time() - last_blink if self.config.gui_outer_side_falloff: - if ( - self.r_eye_blink == 0.0 - ): # if both eyes closed and DEF is enables, blink - self.client.send_message( - "/tracking/eye/EyesClosedAmount", float(1 - eye_blink) - ) + if self.r_eye_blink == 0.0: # if both eyes closed and DEF is enables, blink + self.client.send_message("/tracking/eye/EyesClosedAmount", float(1 - eye_blink)) self.l_eye_x = self.r_eye_x elif self.eye_id in [EyeId.RIGHT] and not se: # Right eye, send data to right @@ -318,48 +261,26 @@ def output_osc(eye_x, eye_y, eye_blink, last_blink, pupil_dilation, avg_velocity ) if self.r_eye_blink == 0.0: - if ( - last_blink > 0.2 - ): # when binary blink is on, blinks may be too fast for OSC so we repeat them. + if last_blink > 0.2: # when binary blink is on, blinks may be too fast for OSC so we repeat them. for i in range(5): - self.client.send_message( - "/tracking/eye/EyesClosedAmount", float(1 - eye_blink) - ) + self.client.send_message("/tracking/eye/EyesClosedAmount", float(1 - eye_blink)) last_blink = time.time() - last_blink if self.config.gui_outer_side_falloff: - if ( - self.l_eye_blink == 0.0 - ): # if both eyes closed and DEF is enables, blink - self.client.send_message( - "/tracking/eye/EyesClosedAmount", float(0) - ) + if self.l_eye_blink == 0.0: # if both eyes closed and DEF is enables, blink + self.client.send_message("/tracking/eye/EyesClosedAmount", float(0)) self.r_eye_x = self.l_eye_x - if ( - self.main_config.eye_display_id in [EyeId.BOTH] - and self.r_eye_blink != 621 - and self.r_eye_blink != 621 - ): + if self.main_config.eye_display_id in [EyeId.BOTH] and self.r_eye_blink != 621 and self.r_eye_blink != 621: if self.r_eye_blink == 0.0 or self.l_eye_blink == 0.0: - if ( - last_blink > 0.2 - ): # when binary blink is on, blinks may be too fast for OSC so we repeat them. + if last_blink > 0.2: # when binary blink is on, blinks may be too fast for OSC so we repeat them. for i in range(5): - self.client.send_message( - "/tracking/eye/EyesClosedAmount", float(1) - ) + self.client.send_message("/tracking/eye/EyesClosedAmount", float(1)) last_blink = time.time() - last_blink eye_blink = (self.r_eye_blink + self.l_eye_blink) / 2 - self.client.send_message( - "/tracking/eye/EyesClosedAmount", float(1 - eye_blink) - ) + self.client.send_message("/tracking/eye/EyesClosedAmount", float(1 - eye_blink)) - if ( - self.main_config.eye_display_id in [EyeId.BOTH] - and self.right_y != 621 - and self.left_y != 621 - ): + if self.main_config.eye_display_id in [EyeId.BOTH] and self.right_y != 621 and self.left_y != 621: eye_y = (self.right_y + self.left_y) / 2 if not se: @@ -426,9 +347,7 @@ class VRChatOSC: class VRChatOSCReceiver: - def __init__( - self, cancellation_event: threading.Event, main_config: EyeTrackConfig, eyes: [] - ): + def __init__(self, cancellation_event: threading.Event, main_config: EyeTrackConfig, eyes: []): self.config = main_config.settings self.cancellation_event = cancellation_event self.dispatcher = dispatcher.Dispatcher() @@ -439,9 +358,7 @@ class VRChatOSCReceiver: self.dispatcher, ) except: - print( - f"\033[91m[ERROR] OSC Receive port: {self.config.gui_osc_receiver_port} occupied.\033[0m" - ) + print(f"\033[91m[ERROR] OSC Receive port: {self.config.gui_osc_receiver_port} occupied.\033[0m") def shutdown(self): print("\033[94m[INFO] Exiting OSC Receiver\033[0m") @@ -467,24 +384,13 @@ class VRChatOSCReceiver: PlaySound("Audio/start.wav", SND_FILENAME | SND_ASYNC) def run(self): - # bind what function to run when specified OSC message is received try: - self.dispatcher.map( - self.config.gui_osc_recalibrate_address, self.recalibrate_eyes - ) - self.dispatcher.map( - self.config.gui_osc_recenter_address, self.recenter_eyes - ) + self.dispatcher.map(self.config.gui_osc_recalibrate_address, self.recalibrate_eyes) + self.dispatcher.map(self.config.gui_osc_recenter_address, self.recenter_eyes) # start the server - print( - "\033[92m[INFO] VRChatOSCReceiver serving on {}\033[0m".format( - self.server.server_address - ) - ) + print("\033[92m[INFO] VRChatOSCReceiver serving on {}\033[0m".format(self.server.server_address)) self.server.serve_forever() except: - print( - f"\033[91m[ERROR] OSC Receive port: {self.config.gui_osc_receiver_port} occupied.\033[0m" - ) + print(f"\033[91m[ERROR] OSC Receive port: {self.config.gui_osc_receiver_port} occupied.\033[0m") diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..2b78419 --- /dev/null +++ b/conftest.py @@ -0,0 +1,125 @@ +import pytest +from EyeTrackApp.config import ( + EyeTrackConfig, + EyeTrackCameraConfig, + EyeTrackSettingsConfig, +) + + +@pytest.fixture() +def eyetrack_settings_config(): + return EyeTrackSettingsConfig( + gui_flip_x_axis_left=False, + gui_flip_x_axis_right=False, + gui_flip_y_axis=False, + gui_RANSAC3D=False, + gui_HSF=False, + gui_BLOB=False, + gui_BLINK=False, + gui_HSRAC=False, + gui_AHSFRAC=False, + gui_AHSF=False, + gui_DADDY=False, + gui_LEAP=True, + gui_HSF_radius=15, + gui_HSF_radius_left=10, + gui_HSF_radius_right=10, + gui_min_cutoff="0.0004", + gui_speed_coefficient="0.9", + gui_osc_address="127.0.0.1", + gui_osc_port=8889, + gui_osc_receiver_port=9001, + gui_osc_recenter_address="/avatar/parameters/etvr_recenter", + gui_osc_recalibrate_address="/avatar/parameters/etvr_recalibrate", + gui_blob_maxsize=25.0, + gui_blob_minsize=10.0, + gui_recenter_eyes=False, + tracker_single_eye=2, + gui_threshold=65, + gui_AHSFRACP=1, + gui_AHSFP=2, + gui_HSRACP=3, + gui_HSFP=4, + gui_DADDYP=5, + gui_RANSAC3DP=6, + gui_BLOBP=7, + gui_LEAPP=8, + gui_IBO=True, + gui_skip_autoradius=False, + gui_thresh_add=11, + gui_update_check=False, + gui_ROSC=False, + gui_circular_crop_right=False, + gui_circular_crop_left=False, + ibo_filter_samples=400, + ibo_average_output_samples=0, + ibo_fully_close_eye_threshold=0.3, + calibration_samples=600, + osc_right_eye_close_address="/avatar/parameters/RightEyeLidExpandedSqueeze", + osc_left_eye_close_address="/avatar/parameters/LeftEyeLidExpandedSqueeze", + osc_left_eye_x_address="/avatar/parameters/LeftEyeX", + osc_right_eye_x_address="/avatar/parameters/RightEyeX", + osc_eyes_y_address="/avatar/parameters/EyesY", + osc_invert_eye_close=False, + gui_RANSACBLINK=False, + gui_right_eye_dominant=False, + gui_left_eye_dominant=False, + gui_outer_side_falloff=False, + gui_eye_dominant_diff_thresh=0.3, + gui_legacy_ransac=False, + gui_legacy_ransac_thresh_right=80, + gui_legacy_ransac_thresh_left=80, + gui_LEAP_lid=False, + gui_osc_vrcft_v1=False, + gui_osc_vrcft_v2=False, + gui_vrc_native=False, + gui_pupil_dilation=True, + ) + + +@pytest.fixture() +def eyetrack_camera_config(): + return EyeTrackCameraConfig( + rotation_angle=250, + roi_window_x=67, + roi_window_y=27, + roi_window_w=96, + roi_window_h=117, + focal_length=30, + capture_source="http://192.168.0.31/", + calib_XMAX=122.5, + calib_XMIN=38.0, + calib_YMAX=118.0, + calib_YMIN=6.0, + calib_XOFF=40.0, + calib_YOFF=63.0, + calibration_points=[], + ) + + +@pytest.fixture() +def main_config(eyetrack_camera_config, eyetrack_settings_config): + return EyeTrackConfig( + right_eye=eyetrack_camera_config, + left_eye=eyetrack_camera_config, + settings=eyetrack_settings_config, + eye_display_id=0, + ) + + +@pytest.fixture() +def main_config_v1_params(main_config): + main_config.settings.gui_osc_vrcft_v1 = True + return main_config + + +@pytest.fixture() +def main_config_v2_params(main_config): + main_config.settings.gui_osc_vrcft_v2 = True + return main_config + + +@pytest.fixture() +def main_config_native_params(main_config): + main_config.settings.gui_vrc_native = True + return main_config diff --git a/poetry.lock b/poetry.lock index 44ed353..c15a2a7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -207,6 +207,70 @@ humanfriendly = ">=9.1" [package.extras] cron = ["capturer (>=2.4)"] +[[package]] +name = "coverage" +version = "7.4.1" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7"}, + {file = "coverage-7.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25"}, + {file = "coverage-7.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c"}, + {file = "coverage-7.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b"}, + {file = "coverage-7.4.1-cp310-cp310-win32.whl", hash = "sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016"}, + {file = "coverage-7.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018"}, + {file = "coverage-7.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295"}, + {file = "coverage-7.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd"}, + {file = "coverage-7.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1"}, + {file = "coverage-7.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6"}, + {file = "coverage-7.4.1-cp311-cp311-win32.whl", hash = "sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5"}, + {file = "coverage-7.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968"}, + {file = "coverage-7.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581"}, + {file = "coverage-7.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156"}, + {file = "coverage-7.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1"}, + {file = "coverage-7.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc"}, + {file = "coverage-7.4.1-cp312-cp312-win32.whl", hash = "sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74"}, + {file = "coverage-7.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448"}, + {file = "coverage-7.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218"}, + {file = "coverage-7.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06"}, + {file = "coverage-7.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60"}, + {file = "coverage-7.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad"}, + {file = "coverage-7.4.1-cp38-cp38-win32.whl", hash = "sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042"}, + {file = "coverage-7.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d"}, + {file = "coverage-7.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54"}, + {file = "coverage-7.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950"}, + {file = "coverage-7.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756"}, + {file = "coverage-7.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35"}, + {file = "coverage-7.4.1-cp39-cp39-win32.whl", hash = "sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c"}, + {file = "coverage-7.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a"}, + {file = "coverage-7.4.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166"}, + {file = "coverage-7.4.1.tar.gz", hash = "sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04"}, +] + +[package.extras] +toml = ["tomli"] + [[package]] name = "flake8" version = "5.0.4" @@ -291,6 +355,17 @@ pyav = ["av"] test = ["fsspec[github]", "pytest", "pytest-cov"] tifffile = ["tifffile"] +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + [[package]] name = "lazy-loader" version = "0.3" @@ -665,6 +740,21 @@ files = [ docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] +[[package]] +name = "pluggy" +version = "1.4.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + [[package]] name = "protobuf" version = "4.25.1" @@ -991,6 +1081,44 @@ files = [ {file = "PySimpleGUI-4.60.5.tar.gz", hash = "sha256:31014d1cc5eef1373d7e93564ff2604662645cc774a939b1f01aa253e7f9d78b"}, ] +[[package]] +name = "pytest" +version = "8.0.0" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.0.0-py3-none-any.whl", hash = "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6"}, + {file = "pytest-8.0.0.tar.gz", hash = "sha256:249b1b0864530ba251b7438274c4d251c58d868edaaec8762893ad4a0d71c36c"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.3.0,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + [[package]] name = "python-osc" version = "1.8.3" @@ -1251,4 +1379,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "~3.11.0" -content-hash = "01b325e85a4f1b92329d326db3871acd7051a0756b27b7dd230daf4ffa9c18bd" +content-hash = "51c416718d7c51b683ad74bee318c227ced271bc97307643b7c8072921cc4928" diff --git a/pyproject.toml b/pyproject.toml index d3c5a23..54bd77e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,8 @@ winotify = [ onnxruntime = "^1.13.1" colorama = "^0.4.6" taskipy = "^1.10.4" +pytest = "^8.0.0" +pytest-cov = "^4.1.0" [tool.poetry.group.dev.dependencies] black = "^22.10.0" pyinstaller = "^5.6.2" @@ -31,6 +33,16 @@ flake8 = "^5.0.4" [tool.taskipy.tasks] dev = "python eyetrackapp.py" +[tool.black] +line-length = 120 + [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" + +[tool.pytest.ini_options] +addopts = "-ra -q --cov=. --cov-report html --cov-report term" +pythonpath = "." +python_files = [ + "test_*.py" +] \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_osc.py b/tests/test_osc.py new file mode 100644 index 0000000..dc72d25 --- /dev/null +++ b/tests/test_osc.py @@ -0,0 +1,314 @@ +import dataclasses +import threading +from queue import Queue +from time import sleep +from unittest import mock + +import pytest + +from EyeTrackApp.osc import VRChatOSC + + +@dataclasses.dataclass +class EyeInfoMock: + x: int + y: int + blink: float + pupil_dilation: float + avg_velocity: float + + +class SimpleUDPClientMock: + def __init__(self, osc_address, port): + self.osc_address = osc_address + self.port = port + self.messages = [] + + def send_message(self, address, value): + self.messages.append((address, value)) + + +@pytest.mark.parametrize( + "eye_id,messages,expected_outcome", + [ + ( + 0, + [ + EyeInfoMock( + x=0, + y=0, + blink=1, + pupil_dilation=0, + avg_velocity=0, + ), + ], + [ + ("/avatar/parameters/v2/EyeX", 0), + ("/avatar/parameters/v2/EyeY", 0), + ("/avatar/parameters/v2/EyeLid", 1.0), + ], + ), + ( + 1, + [ + EyeInfoMock( + x=0, + y=0, + blink=1, + pupil_dilation=0, + avg_velocity=0, + ), + ], + [ + ("/avatar/parameters/v2/EyeX", 0), + ("/avatar/parameters/v2/EyeY", 0), + ("/avatar/parameters/v2/EyeLid", 1.0), + ], + ), + ], +) +def test_send_command_v2_params_single_eye(main_config_v2_params, eye_id, messages, expected_outcome): + with mock.patch("EyeTrackApp.osc.udp_client.SimpleUDPClient", SimpleUDPClientMock): + cancellation_event = threading.Event() + msg_queue = Queue() + client = VRChatOSC( + main_config=main_config_v2_params, + msg_queue=msg_queue, + cancellation_event=cancellation_event, + ) + + osc_thread = threading.Thread(target=client.run) + osc_thread.start() + + for message in messages: + sleep(0.01) + msg_queue.put((eye_id, message)) + + cancellation_event.set() + osc_thread.join() + + assert msg_queue.empty() + assert client.client.messages == expected_outcome + + +@pytest.mark.parametrize( + "eye_data,expected_outcome", + [ + ( + [ + ( + 0, + EyeInfoMock( + x=0, + y=0, + blink=1, + pupil_dilation=1, + avg_velocity=0, + ), + ), + ( + 1, + EyeInfoMock( + x=10, + y=5, + blink=0.5, + pupil_dilation=1, + avg_velocity=0, + ), + ), + ], + [ + ("/avatar/parameters/v2/EyeRightX", 0), + ("/avatar/parameters/v2/EyeRightY", 0), + ("/avatar/parameters/v2/EyeLidRight", 1.0), + ("/avatar/parameters/v2/EyeLeftX", 10), + ("/avatar/parameters/v2/EyeLeftY", 5), + ("/avatar/parameters/v2/EyeLidLeft", 0.5), + ], + ), + # binary blink + ( + [ + ( + 0, + EyeInfoMock( + x=0, + y=0, + blink=0, + pupil_dilation=1, + avg_velocity=0, + ), + ), + ( + 1, + EyeInfoMock( + x=10, + y=5, + blink=0, + pupil_dilation=1, + avg_velocity=0, + ), + ), + ], + [ + ("/avatar/parameters/v2/EyeLidRight", 0.0), + ("/avatar/parameters/v2/EyeLidRight", 0.0), + ("/avatar/parameters/v2/EyeLidRight", 0.0), + ("/avatar/parameters/v2/EyeLidRight", 0.0), + ("/avatar/parameters/v2/EyeRightX", 0), + ("/avatar/parameters/v2/EyeRightY", 0), + ("/avatar/parameters/v2/EyeLidRight", 0.0), + ("/avatar/parameters/v2/EyeLidLeft", 0.0), + ("/avatar/parameters/v2/EyeLidLeft", 0.0), + ("/avatar/parameters/v2/EyeLidLeft", 0.0), + ("/avatar/parameters/v2/EyeLidLeft", 0.0), + ("/avatar/parameters/v2/EyeLeftX", 0), + ("/avatar/parameters/v2/EyeLeftY", 5), + ("/avatar/parameters/v2/EyeLidLeft", 0.0), + ], + ), + ], +) +def test_send_command_v2_params_dual_eye(main_config_v2_params, eye_data, expected_outcome): + main_config_v2_params.eye_display_id = 2 + + with mock.patch("EyeTrackApp.osc.udp_client.SimpleUDPClient", SimpleUDPClientMock): + cancellation_event = threading.Event() + msg_queue = Queue() + client = VRChatOSC( + main_config=main_config_v2_params, + msg_queue=msg_queue, + cancellation_event=cancellation_event, + ) + + osc_thread = threading.Thread(target=client.run) + osc_thread.start() + + for eye_id, message in eye_data: + sleep(0.01) + msg_queue.put((eye_id, message)) + + cancellation_event.set() + osc_thread.join() + + assert msg_queue.empty() + assert client.client.messages == expected_outcome + + +@pytest.mark.parametrize( + "eye_data,expected_outcome", + [ + ( + [ + ( + 0, + EyeInfoMock( + x=0, + y=0, + blink=0, + pupil_dilation=1, + avg_velocity=0, + ), + ), + ( + 1, + EyeInfoMock( + x=10, + y=5, + blink=0, + pupil_dilation=1, + avg_velocity=0, + ), + ), + ], + [ + ("/avatar/parameters/v2/EyeLidRight", 0.0), + ("/avatar/parameters/v2/EyeLidRight", 0.0), + ("/avatar/parameters/v2/EyeLidRight", 0.0), + ("/avatar/parameters/v2/EyeLidRight", 0.0), + ("/avatar/parameters/v2/EyeRightX", 0), + ("/avatar/parameters/v2/EyeRightY", 0), + ("/avatar/parameters/v2/EyeLidRight", 0.0), + ("/avatar/parameters/v2/EyeLidLeft", 0.0), + ("/avatar/parameters/v2/EyeLidLeft", 0.0), + ("/avatar/parameters/v2/EyeLidLeft", 0.0), + ("/avatar/parameters/v2/EyeLidLeft", 0.0), + ("/avatar/parameters/v2/EyeLidLeft", 0.0), + ("/avatar/parameters/v2/EyeLidRight", 0.0), + ("/avatar/parameters/v2/EyeLeftX", 0), + ("/avatar/parameters/v2/EyeLeftY", 5), + ("/avatar/parameters/v2/EyeLidLeft", 0.0), + ], + ), + ( + [ + ( + 0, + EyeInfoMock( + x=0, + y=0, + blink=0, + pupil_dilation=1, + avg_velocity=0, + ), + ), + ( + 1, + EyeInfoMock( + x=10, + y=5, + blink=0, + pupil_dilation=1, + avg_velocity=0, + ), + ), + ], + [ + # binary blink + ("/avatar/parameters/v2/EyeLidRight", 0.0), + ("/avatar/parameters/v2/EyeLidRight", 0.0), + ("/avatar/parameters/v2/EyeLidRight", 0.0), + ("/avatar/parameters/v2/EyeLidRight", 0.0), + ("/avatar/parameters/v2/EyeRightX", 0), + ("/avatar/parameters/v2/EyeRightY", 0), + # side falloff + ("/avatar/parameters/v2/EyeLidRight", 0.0), + ("/avatar/parameters/v2/EyeLidLeft", 0.0), + # binary blink again + ("/avatar/parameters/v2/EyeLidLeft", 0.0), + ("/avatar/parameters/v2/EyeLidLeft", 0.0), + ("/avatar/parameters/v2/EyeLidLeft", 0.0), + ("/avatar/parameters/v2/EyeLidLeft", 0.0), + ("/avatar/parameters/v2/EyeLidRight", 0.0), + ("/avatar/parameters/v2/EyeLeftX", 0), + ("/avatar/parameters/v2/EyeLeftY", 5), + ("/avatar/parameters/v2/EyeLidLeft", 0.0), + ], + ), + ], +) +def test_send_command_v2_params_eye_outer_side_falloff(main_config_v2_params, eye_data, expected_outcome): + main_config_v2_params.eye_display_id = 2 + main_config_v2_params.settings.gui_outer_side_falloff = True + + with mock.patch("EyeTrackApp.osc.udp_client.SimpleUDPClient", SimpleUDPClientMock): + cancellation_event = threading.Event() + msg_queue = Queue() + client = VRChatOSC( + main_config=main_config_v2_params, + msg_queue=msg_queue, + cancellation_event=cancellation_event, + ) + + osc_thread = threading.Thread(target=client.run) + osc_thread.start() + + for eye_id, message in eye_data: + sleep(0.01) + msg_queue.put((eye_id, message)) + + cancellation_event.set() + osc_thread.join() + + assert msg_queue.empty() + assert client.client.messages == expected_outcome