diff --git a/.github/workflows/python-demos.yml b/.github/workflows/python-demos.yml index c19ef94..a2e58a9 100644 --- a/.github/workflows/python-demos.yml +++ b/.github/workflows/python-demos.yml @@ -41,6 +41,12 @@ jobs: - name: Pre-build dependencies run: python -m pip install --upgrade pip + # TODO: remove after release + - name: Build dependencies + run: | + pip install -U pip setuptools + pip install wheel && cd ../../binding/python && python3 setup.py sdist bdist_wheel && pip install dist/pveagle-1.0.0-py3-none-any.whl + - name: Install dependencies run: pip install -r requirements.txt @@ -63,7 +69,7 @@ jobs: strategy: matrix: - machine: [rpi3-32, rpi3-64, rpi4-32, rpi4-64, jetson] + machine: [rpi3-32, rpi3-64, rpi4-32, rpi4-64, rpi5-64, jetson] steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/python-perf.yml b/.github/workflows/python-perf.yml index cdbe63a..723c438 100644 --- a/.github/workflows/python-perf.yml +++ b/.github/workflows/python-perf.yml @@ -53,8 +53,8 @@ jobs: profiler_performance_threshold_sec: 0.5 recognizer_performance_threshold_sec: 0.6 - os: macos-latest - profiler_performance_threshold_sec: 0.6 - recognizer_performance_threshold_sec: 0.8 + profiler_performance_threshold_sec: 0.9 + recognizer_performance_threshold_sec: 0.9 steps: - uses: actions/checkout@v3 @@ -87,10 +87,10 @@ jobs: machine: [rpi3-32, rpi3-64, rpi4-32, rpi4-64, jetson] include: - machine: rpi3-32 - profiler_performance_threshold_sec: 2.3 + profiler_performance_threshold_sec: 2.5 recognizer_performance_threshold_sec: 2.8 - machine: rpi3-64 - profiler_performance_threshold_sec: 2.3 + profiler_performance_threshold_sec: 2.5 recognizer_performance_threshold_sec: 2.8 - machine: rpi4-32 profiler_performance_threshold_sec: 1.5 @@ -98,6 +98,9 @@ jobs: - machine: rpi4-64 profiler_performance_threshold_sec: 2.0 recognizer_performance_threshold_sec: 2.5 + - machine: rpi5-64 + profiler_performance_threshold_sec: 0.5 + recognizer_performance_threshold_sec: 0.6 - machine: jetson profiler_performance_threshold_sec: 1.2 recognizer_performance_threshold_sec: 1.5 @@ -112,6 +115,12 @@ jobs: - name: Pre-build dependencies run: python3 -m pip install --upgrade pip + # TODO: remove after release + - name: Build dependencies + run: | + pip install -U pip setuptools + pip install wheel && cd ../../binding/python && python3 setup.py sdist bdist_wheel && pip install dist/pveagle-1.0.0-py3-none-any.whl + - name: Install dependencies run: pip3 install -r requirements.txt diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index abdb815..5665f01 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -64,7 +64,7 @@ jobs: strategy: matrix: - machine: [rpi3-32, rpi3-64, rpi4-32, rpi4-64, jetson] + machine: [rpi3-32, rpi3-64, rpi4-32, rpi4-64, rpi5-64, jetson] steps: - uses: actions/checkout@v3 diff --git a/README.md b/README.md index ae4db8f..53a48d0 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Eagle is an on-device speaker recognition engine. Eagle is: - Linux (x86_64), macOS (x86_64, arm64), Windows (x86_64) - Android and iOS - Chrome, Safari, Firefox, and Edge - - Raspberry Pi (4, 3) and NVIDIA Jetson Nano + - Raspberry Pi (5, 4, 3) and NVIDIA Jetson Nano ## Table of Contents @@ -616,6 +616,7 @@ while (true) { - Enhanced engine accuracy - Improved the enrollment process + - Added Raspberry Pi 5 support - Various bug fixes and improvements ### v0.2.0 - November 24th, 2023 diff --git a/binding/python/README.md b/binding/python/README.md index de7e3b4..4f1388b 100644 --- a/binding/python/README.md +++ b/binding/python/README.md @@ -12,12 +12,12 @@ Eagle is an on-device speaker recognition engine. Eagle is: - Linux (x86_64), macOS (x86_64, arm64), Windows (x86_64) - Android and iOS - Chrome, Safari, Firefox, and Edge - - Raspberry Pi (4, 3) and NVIDIA Jetson Nano + - Raspberry Pi (5, 4, 3) and NVIDIA Jetson Nano ## Compatibility -- Python 3.5 or higher -- Runs on Linux (x86_64), macOS (x86_64, arm64), Windows (x86_64), Raspberry Pi (4, 3), and NVIDIA Jetson Nano. +- Python 3.7 or higher +- Runs on Linux (x86_64), macOS (x86_64, arm64), Windows (x86_64), Raspberry Pi (5, 4, 3), and NVIDIA Jetson Nano. ## Installation diff --git a/binding/python/_eagle.py b/binding/python/_eagle.py index c0612a5..84bf176 100644 --- a/binding/python/_eagle.py +++ b/binding/python/_eagle.py @@ -16,7 +16,7 @@ class EagleError(Exception): - def __init__(self, message: str = '', message_stack: Sequence[str] = None): + def __init__(self, message: str = "", message_stack: Sequence[str] = None): super().__init__(message) self._message = message @@ -25,9 +25,9 @@ def __init__(self, message: str = '', message_stack: Sequence[str] = None): def __str__(self): message = self._message if len(self._message_stack) > 0: - message += ':' + message += ":" for i in range(len(self._message_stack)): - message += '\n [%d] %s' % (i, self._message_stack[i]) + message += "\n [%d] %s" % (i, self._message_stack[i]) return message @property @@ -109,7 +109,7 @@ class PicovoiceStatuses(Enum): PicovoiceStatuses.ACTIVATION_ERROR: EagleActivationError, PicovoiceStatuses.ACTIVATION_LIMIT_REACHED: EagleActivationLimitError, PicovoiceStatuses.ACTIVATION_THROTTLED: EagleActivationThrottledError, - PicovoiceStatuses.ACTIVATION_REFUSED: EagleActivationRefusedError + PicovoiceStatuses.ACTIVATION_REFUSED: EagleActivationRefusedError, } @@ -144,7 +144,7 @@ def to_bytes(self) -> bytes: return self._to_bytes(self.handle, self.size) @classmethod - def from_bytes(cls, profile: bytes) -> 'EagleProfile': + def from_bytes(cls, profile: bytes) -> "EagleProfile": """ Creates an instance of EagleProfile from a bytes object. @@ -207,10 +207,13 @@ def __init__(self, access_key: str, model_path: str, library_path: str) -> None: set_sdk_func.argtypes = [c_char_p] set_sdk_func.restype = None - set_sdk_func('python'.encode('utf-8')) + set_sdk_func("python".encode("utf-8")) self._get_error_stack_func = library.pv_get_error_stack - self._get_error_stack_func.argtypes = [POINTER(POINTER(c_char_p)), POINTER(c_int)] + self._get_error_stack_func.argtypes = [ + POINTER(POINTER(c_char_p)), + POINTER(c_int) + ] self._get_error_stack_func.restype = PicovoiceStatuses self._free_error_stack_func = library.pv_free_error_stack @@ -224,47 +227,51 @@ def __init__(self, access_key: str, model_path: str, library_path: str) -> None: init_func.argtypes = [ c_char_p, c_char_p, - POINTER(POINTER(self.CEagleProfiler))] + POINTER(POINTER(self.CEagleProfiler)), + ] init_func.restype = PicovoiceStatuses status = init_func( - access_key.encode('utf-8'), - model_path.encode('utf-8'), - byref(self._eagle_profiler)) + access_key.encode("utf-8"), + model_path.encode("utf-8"), + byref(self._eagle_profiler), + ) if status is not PicovoiceStatuses.SUCCESS: raise _PICOVOICE_STATUS_TO_EXCEPTION[status]( - message='Profile initialization failed', - message_stack=self._get_error_stack()) + message="Profile initialization failed", + message_stack=self._get_error_stack(), + ) speaker_profile_size_func = library.pv_eagle_profiler_export_size speaker_profile_size_func.argtypes = [ POINTER(self.CEagleProfiler), - POINTER(c_int32)] + POINTER(c_int32), + ] speaker_profile_size_func.restype = PicovoiceStatuses profile_size = c_int32() status = speaker_profile_size_func(self._eagle_profiler, byref(profile_size)) if status is not PicovoiceStatuses.SUCCESS: raise _PICOVOICE_STATUS_TO_EXCEPTION[status]( - message='Failed to get profile size', - message_stack=self._get_error_stack()) + message="Failed to get profile size", + message_stack=self._get_error_stack(), + ) self._profile_size = profile_size.value - enroll_min_audio_length_sample_func = \ - library.pv_eagle_profiler_enroll_min_audio_length_samples + enroll_min_audio_length_sample_func = library.pv_eagle_profiler_enroll_min_audio_length_samples enroll_min_audio_length_sample_func.argtypes = [ POINTER(self.CEagleProfiler), - POINTER(c_int32)] + POINTER(c_int32), + ] enroll_min_audio_length_sample_func.restype = PicovoiceStatuses min_enroll_samples = c_int32() - status = enroll_min_audio_length_sample_func( - self._eagle_profiler, - byref(min_enroll_samples)) + status = enroll_min_audio_length_sample_func(self._eagle_profiler, byref(min_enroll_samples)) if status is not PicovoiceStatuses.SUCCESS: raise _PICOVOICE_STATUS_TO_EXCEPTION[status]( - message='Failed to get min audio length sample', - message_stack=self._get_error_stack()) + message="Failed to get min audio length sample", + message_stack=self._get_error_stack(), + ) self._min_enroll_samples = min_enroll_samples.value self._delete_func = library.pv_eagle_profiler_delete @@ -277,7 +284,8 @@ def __init__(self, access_key: str, model_path: str, library_path: str) -> None: POINTER(c_int16), c_int32, POINTER(c_int), - POINTER(c_float)] + POINTER(c_float), + ] self._enroll_func.restype = PicovoiceStatuses self._reset_func = library.pv_eagle_profiler_reset @@ -287,7 +295,8 @@ def __init__(self, access_key: str, model_path: str, library_path: str) -> None: self._export_func = library.pv_eagle_profiler_export self._export_func.argtypes = [ POINTER(self.CEagleProfiler), - c_void_p] + c_void_p, + ] self._export_func.restype = PicovoiceStatuses self._sample_rate = library.pv_sample_rate() @@ -297,7 +306,7 @@ def __init__(self, access_key: str, model_path: str, library_path: str) -> None: version_func = library.pv_eagle_version version_func.argtypes = [] version_func.restype = c_char_p - self._version = version_func().decode('utf-8') + self._version = version_func().decode("utf-8") def enroll(self, pcm: Sequence[int]) -> Tuple[float, EagleProfilerEnrollFeedback]: """ @@ -339,8 +348,9 @@ def enroll(self, pcm: Sequence[int]) -> Tuple[float, EagleProfilerEnrollFeedback feedback = EagleProfilerEnrollFeedback(feedback_code.value) if status is not PicovoiceStatuses.SUCCESS: raise _PICOVOICE_STATUS_TO_EXCEPTION[status]( - message='Enrollment failed', - message_stack=self._get_error_stack()) + message="Enrollment failed", + message_stack=self._get_error_stack(), + ) return percentage.value, feedback @@ -355,12 +365,13 @@ def export(self) -> EagleProfile: profile = (c_byte * self._profile_size)() status = self._export_func( self._eagle_profiler, - byref(profile) + byref(profile), ) if status is not PicovoiceStatuses.SUCCESS: raise _PICOVOICE_STATUS_TO_EXCEPTION[status]( - message='Export failed', - message_stack=self._get_error_stack()) + message="Export failed", + message_stack=self._get_error_stack(), + ) return EagleProfile(cast(profile, c_void_p), self._profile_size) @@ -373,8 +384,9 @@ def reset(self) -> None: status = self._reset_func(self._eagle_profiler) if status is not PicovoiceStatuses.SUCCESS: raise _PICOVOICE_STATUS_TO_EXCEPTION[status]( - message='Profile reset failed', - message_stack=self._get_error_stack()) + message="Profile reset failed", + message_stack=self._get_error_stack(), + ) def delete(self) -> None: """ @@ -412,11 +424,11 @@ def _get_error_stack(self) -> Sequence[str]: message_stack_depth = c_int() status = self._get_error_stack_func(byref(message_stack_ref), byref(message_stack_depth)) if status is not PicovoiceStatuses.SUCCESS: - raise _PICOVOICE_STATUS_TO_EXCEPTION[status](message='Unable to get Eagle error state') + raise _PICOVOICE_STATUS_TO_EXCEPTION[status](message="Unable to get Eagle error state") message_stack = list() for i in range(message_stack_depth.value): - message_stack.append(message_stack_ref[i].decode('utf-8')) + message_stack.append(message_stack_ref[i].decode("utf-8")) self._free_error_stack_func(message_stack_ref) @@ -465,10 +477,13 @@ def __init__( set_sdk_func.argtypes = [c_char_p] set_sdk_func.restype = None - set_sdk_func('python'.encode('utf-8')) + set_sdk_func("python".encode("utf-8")) self._get_error_stack_func = library.pv_get_error_stack - self._get_error_stack_func.argtypes = [POINTER(POINTER(c_char_p)), POINTER(c_int)] + self._get_error_stack_func.argtypes = [ + POINTER(POINTER(c_char_p)), + POINTER(c_int), + ] self._get_error_stack_func.restype = PicovoiceStatuses self._free_error_stack_func = library.pv_free_error_stack @@ -484,7 +499,8 @@ def __init__( c_char_p, c_int32, POINTER(c_void_p), - POINTER(POINTER(self.CEagle))] + POINTER(POINTER(self.CEagle)), + ] init_func.restype = PicovoiceStatuses profile_bytes = (c_void_p * len(speaker_profiles))() @@ -492,15 +508,17 @@ def __init__( profile_bytes[i] = profile.handle status = init_func( - access_key.encode('utf-8'), - model_path.encode('utf-8'), + access_key.encode("utf-8"), + model_path.encode("utf-8"), len(speaker_profiles), profile_bytes, - byref(self._eagle)) + byref(self._eagle), + ) if status is not PicovoiceStatuses.SUCCESS: raise _PICOVOICE_STATUS_TO_EXCEPTION[status]( - message='Initialization failed', - message_stack=self._get_error_stack()) + message="Initialization failed", + message_stack=self._get_error_stack(), + ) self._delete_func = library.pv_eagle_delete self._delete_func.argtypes = [POINTER(self.CEagle)] @@ -510,7 +528,8 @@ def __init__( self._process_func.argtypes = [ POINTER(self.CEagle), POINTER(c_int16), - POINTER(c_float)] + POINTER(c_float), + ] self._process_func.restype = PicovoiceStatuses self._scores = (c_float * len(speaker_profiles))() @@ -526,7 +545,7 @@ def __init__( version_func = library.pv_eagle_version version_func.argtypes = [] version_func.restype = c_char_p - self._version = version_func().decode('utf-8') + self._version = version_func().decode("utf-8") def process(self, pcm: Sequence[int]) -> Sequence[float]: """ @@ -541,7 +560,8 @@ def process(self, pcm: Sequence[int]) -> Sequence[float]: if len(pcm) != self.frame_length: raise EagleInvalidArgumentError( - "Length of input frame %d does not match required frame length %d" % (len(pcm), self.frame_length)) + "Length of input frame %d does not match required frame length %d" % (len(pcm), self.frame_length) + ) frame_type = c_int16 * self.frame_length pcm = frame_type(*pcm) @@ -549,8 +569,9 @@ def process(self, pcm: Sequence[int]) -> Sequence[float]: status = self._process_func(self._eagle, pcm, self._scores) if status is not PicovoiceStatuses.SUCCESS: raise _PICOVOICE_STATUS_TO_EXCEPTION[status]( - message='Process failed', - message_stack=self._get_error_stack()) + message="Process failed", + message_stack=self._get_error_stack(), + ) # noinspection PyTypeChecker return [float(score) for score in self._scores] @@ -565,8 +586,9 @@ def reset(self) -> None: status = self._reset_func(self._eagle) if status is not PicovoiceStatuses.SUCCESS: raise _PICOVOICE_STATUS_TO_EXCEPTION[status]( - message='Reset failed', - message_stack=self._get_error_stack()) + message="Reset failed", + message_stack=self._get_error_stack(), + ) def delete(self) -> None: """ @@ -604,11 +626,11 @@ def _get_error_stack(self) -> Sequence[str]: message_stack_depth = c_int() status = self._get_error_stack_func(byref(message_stack_ref), byref(message_stack_depth)) if status is not PicovoiceStatuses.SUCCESS: - raise _PICOVOICE_STATUS_TO_EXCEPTION[status](message='Unable to get Eagle error state') + raise _PICOVOICE_STATUS_TO_EXCEPTION[status](message="Unable to get Eagle error state") message_stack = list() for i in range(message_stack_depth.value): - message_stack.append(message_stack_ref[i].decode('utf-8')) + message_stack.append(message_stack_ref[i].decode("utf-8")) self._free_error_stack_func(message_stack_ref) @@ -616,20 +638,20 @@ def _get_error_stack(self) -> Sequence[str]: __all__ = [ - 'Eagle', - 'EagleProfile', - 'EagleProfiler', - 'EagleProfilerEnrollFeedback', - 'EagleActivationError', - 'EagleActivationLimitError', - 'EagleActivationRefusedError', - 'EagleActivationThrottledError', - 'EagleError', - 'EagleInvalidArgumentError', - 'EagleInvalidStateError', - 'EagleIOError', - 'EagleKeyError', - 'EagleMemoryError', - 'EagleRuntimeError', - 'EagleStopIterationError', + "Eagle", + "EagleProfile", + "EagleProfiler", + "EagleProfilerEnrollFeedback", + "EagleActivationError", + "EagleActivationLimitError", + "EagleActivationRefusedError", + "EagleActivationThrottledError", + "EagleError", + "EagleInvalidArgumentError", + "EagleInvalidStateError", + "EagleIOError", + "EagleKeyError", + "EagleMemoryError", + "EagleRuntimeError", + "EagleStopIterationError", ] diff --git a/binding/python/_factory.py b/binding/python/_factory.py index b37bb2e..6b60bab 100644 --- a/binding/python/_factory.py +++ b/binding/python/_factory.py @@ -11,11 +11,7 @@ from typing import Optional, Sequence, Union -from ._eagle import ( - Eagle, - EagleProfile, - EagleProfiler -) +from ._eagle import Eagle, EagleProfile, EagleProfiler from ._util import default_library_path, default_model_path @@ -77,10 +73,11 @@ def create_profiler( return EagleProfiler( access_key=access_key, model_path=model_path, - library_path=library_path) + library_path=library_path, + ) __all__ = [ - 'create_recognizer', - 'create_profiler' + "create_recognizer", + "create_profiler" ] diff --git a/binding/python/_util.py b/binding/python/_util.py index aac7636..ca0b1c6 100644 --- a/binding/python/_util.py +++ b/binding/python/_util.py @@ -15,65 +15,73 @@ def _is_64bit(): - return '64bit' in platform.architecture()[0] + return "64bit" in platform.architecture()[0] def _linux_machine() -> str: machine = platform.machine() - if machine == 'x86_64': + if machine == "x86_64": return machine - elif machine in ['aarch64', 'armv7l']: - arch_info = ('-' + machine) if _is_64bit() else '' + elif machine in ["aarch64", "armv7l"]: + arch_info = ("-" + machine) if _is_64bit() else "" else: raise NotImplementedError("Unsupported CPU architecture: `%s`" % machine) - cpu_info = '' + cpu_info = "" try: - cpu_info = subprocess.check_output(['cat', '/proc/cpuinfo']).decode('utf-8') - cpu_part_list = [x for x in cpu_info.split('\n') if 'CPU part' in x] - cpu_part = cpu_part_list[0].split(' ')[-1].lower() + cpu_info = subprocess.check_output(["cat", "/proc/cpuinfo"]).decode("utf-8") + cpu_part_list = [x for x in cpu_info.split("\n") if "CPU part" in x] + cpu_part = cpu_part_list[0].split(" ")[-1].lower() except Exception as e: raise RuntimeError("Failed to identify the CPU with `%s`\nCPU info: `%s`" % (e, cpu_info)) - if '0xd03' == cpu_part: - return 'cortex-a53' + arch_info - elif '0xd07' == cpu_part: - return 'cortex-a57' + arch_info - elif '0xd08' == cpu_part: - return 'cortex-a72' + arch_info + if "0xd03" == cpu_part: + return "cortex-a53" + arch_info + elif "0xd07" == cpu_part: + return "cortex-a57" + arch_info + elif "0xd08" == cpu_part: + return "cortex-a72" + arch_info + elif "0xd0b" == cpu_part: + return "cortex-a76" + arch_info else: raise NotImplementedError("Unsupported CPU: `%s`." % cpu_part) -_RASPBERRY_PI_MACHINES = {'cortex-a53', 'cortex-a72', 'cortex-a53-aarch64', 'cortex-a72-aarch64'} -_JETSON_MACHINES = {'cortex-a57-aarch64'} +_RASPBERRY_PI_MACHINES = { + "cortex-a53", + "cortex-a72", + "cortex-a53-aarch64", + "cortex-a72-aarch64", + "cortex-a76-aarch64"} +_JETSON_MACHINES = {"cortex-a57-aarch64"} -def default_library_path(relative: str = '') -> str: - if platform.system() == 'Darwin': - if platform.machine() == 'x86_64': - return os.path.join(os.path.dirname(__file__), relative, 'lib/mac/x86_64/libpv_eagle.dylib') +def default_library_path(relative: str = "") -> str: + if platform.system() == "Darwin": + if platform.machine() == "x86_64": + return os.path.join(os.path.dirname(__file__), relative, "lib/mac/x86_64/libpv_eagle.dylib") elif platform.machine() == "arm64": - return os.path.join(os.path.dirname(__file__), relative, 'lib/mac/arm64/libpv_eagle.dylib') - elif platform.system() == 'Linux': + return os.path.join(os.path.dirname(__file__), relative, "lib/mac/arm64/libpv_eagle.dylib") + elif platform.system() == "Linux": linux_machine = _linux_machine() - if linux_machine == 'x86_64': - return os.path.join(os.path.dirname(__file__), relative, 'lib/linux/x86_64/libpv_eagle.so') + if linux_machine == "x86_64": + return os.path.join(os.path.dirname(__file__), relative, "lib/linux/x86_64/libpv_eagle.so") elif linux_machine in _JETSON_MACHINES: - return os.path.join(os.path.dirname(__file__), relative, 'lib/jetson/%s/libpv_eagle.so' % linux_machine) + return os.path.join(os.path.dirname(__file__), relative, "lib/jetson/%s/libpv_eagle.so" % linux_machine) elif linux_machine in _RASPBERRY_PI_MACHINES: return os.path.join( - os.path.dirname(__file__), - relative, - 'lib/raspberry-pi/%s/libpv_eagle.so' % linux_machine) - elif platform.system() == 'Windows': - return os.path.join(os.path.dirname(__file__), relative, 'lib', 'windows', 'amd64', 'libpv_eagle.dll') + os.path.dirname(__file__), relative, "lib/raspberry-pi/%s/libpv_eagle.so" % linux_machine + ) + elif platform.system() == "Windows": + return os.path.join(os.path.dirname(__file__), relative, "lib", "windows", "amd64", "libpv_eagle.dll") - raise NotImplementedError('Unsupported platform.') + raise NotImplementedError("Unsupported platform.") -def default_model_path(relative: str = '') -> str: - return os.path.join(os.path.dirname(__file__), relative, 'lib', 'common', 'eagle_params.pv') +def default_model_path(relative: str = "") -> str: + return os.path.join(os.path.dirname(__file__), relative, "lib", "common", "eagle_params.pv") -__all__ = ['default_library_path', 'default_model_path'] +__all__ = [ + "default_library_path", + "default_model_path"] diff --git a/binding/python/setup.py b/binding/python/setup.py index d79081b..2f53ea8 100644 --- a/binding/python/setup.py +++ b/binding/python/setup.py @@ -14,12 +14,12 @@ import setuptools -INCLUDE_FILES = ('../../LICENSE', '__init__.py', '_factory.py', '_eagle.py', '_util.py') -INCLUDE_LIBS = ('common', 'jetson', 'linux', 'mac', 'raspberry-pi', 'windows') +INCLUDE_FILES = ("../../LICENSE", "__init__.py", "_factory.py", "_eagle.py", "_util.py") +INCLUDE_LIBS = ("common", "jetson", "linux", "mac", "raspberry-pi", "windows") -os.system('git clean -dfx') +os.system("git clean -dfx") -package_folder = os.path.join(os.path.dirname(__file__), 'pveagle') +package_folder = os.path.join(os.path.dirname(__file__), "pveagle") os.mkdir(package_folder) manifest_in = "" @@ -27,22 +27,22 @@ shutil.copy(os.path.join(os.path.dirname(__file__), rel_path), package_folder) manifest_in += "include pveagle/%s\n" % os.path.basename(rel_path) -os.mkdir(os.path.join(package_folder, 'lib')) +os.mkdir(os.path.join(package_folder, "lib")) for platform in INCLUDE_LIBS: shutil.copytree( - os.path.join(os.path.dirname(__file__), '../../lib', platform), - os.path.join(package_folder, 'lib', platform)) + os.path.join(os.path.dirname(__file__), "../../lib", platform), os.path.join(package_folder, "lib", platform) + ) manifest_in += "recursive-include pveagle/lib *\n" -with open(os.path.join(os.path.dirname(__file__), 'MANIFEST.in'), 'w') as f: +with open(os.path.join(os.path.dirname(__file__), "MANIFEST.in"), "w") as f: f.write(manifest_in) -with open(os.path.join(os.path.dirname(__file__), 'README.md'), 'r') as f: +with open(os.path.join(os.path.dirname(__file__), "README.md"), "r") as f: long_description = f.read() setuptools.setup( name="pveagle", - version="0.2.0", + version="1.0.0", author="Picovoice", author_email="hello@picovoice.ai", description="Eagle Speaker Recognition Engine", @@ -52,13 +52,13 @@ packages=["pveagle"], include_package_data=True, classifiers=[ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Topic :: Multimedia :: Sound/Audio :: Speech" + "Topic :: Multimedia :: Sound/Audio :: Speech", ], - python_requires='>=3.5', + python_requires=">=3.7", keywords="Speaker Recognition, Speaker Identification, Voice Recognition, Voice Identification", ) diff --git a/binding/python/test_eagle.py b/binding/python/test_eagle.py index 2664a28..b24d768 100644 --- a/binding/python/test_eagle.py +++ b/binding/python/test_eagle.py @@ -17,37 +17,34 @@ import wave from typing import Sequence -from _eagle import ( - Eagle, - EagleError, - EagleProfiler, - EagleProfilerEnrollFeedback -) +from _eagle import Eagle, EagleError, EagleProfiler, EagleProfilerEnrollFeedback from _util import default_library_path, default_model_path class EagleTestCase(unittest.TestCase): ENROLL_PATHS = [ - os.path.join(os.path.dirname(__file__), '../../resources/audio_samples/speaker_1_utt_1.wav'), - os.path.join(os.path.dirname(__file__), '../../resources/audio_samples/speaker_1_utt_2.wav')] - TEST_PATH = os.path.join(os.path.dirname(__file__), '../../resources/audio_samples/speaker_1_test_utt.wav') - IMPOSTER_PATH = os.path.join(os.path.dirname(__file__), '../../resources/audio_samples/speaker_2_test_utt.wav') + os.path.join(os.path.dirname(__file__), "../../resources/audio_samples/speaker_1_utt_1.wav"), + os.path.join(os.path.dirname(__file__), "../../resources/audio_samples/speaker_1_utt_2.wav"), + ] + TEST_PATH = os.path.join(os.path.dirname(__file__), "../../resources/audio_samples/speaker_1_test_utt.wav") + IMPOSTER_PATH = os.path.join(os.path.dirname(__file__), "../../resources/audio_samples/speaker_2_test_utt.wav") access_key: str eagle: Eagle eagle_profiler: EagleProfiler @staticmethod def load_wav_resource(path: str) -> Sequence[int]: - with wave.open(path, 'rb') as f: + with wave.open(path, "rb") as f: buffer = f.readframes(f.getnframes()) - return struct.unpack('%dh' % f.getnframes(), buffer) + return struct.unpack("%dh" % f.getnframes(), buffer) @classmethod def setUpClass(cls) -> None: cls.eagle_profiler = EagleProfiler( access_key=cls.access_key, - model_path=default_model_path('../..'), - library_path=default_library_path('../..')) + model_path=default_model_path("../.."), + library_path=default_library_path("../.."), + ) for path in cls.ENROLL_PATHS: pcm = cls.load_wav_resource(path) @@ -57,9 +54,10 @@ def setUpClass(cls) -> None: cls.eagle = Eagle( access_key=cls.access_key, - model_path=default_model_path('../..'), - library_path=default_library_path('../..'), - speaker_profiles=[profile]) + model_path=default_model_path("../.."), + library_path=default_library_path("../.."), + speaker_profiles=[profile], + ) @classmethod def tearDownClass(cls) -> None: @@ -77,22 +75,12 @@ def test_eagle_enrollment(self) -> None: profile = self.eagle_profiler.export() self.assertGreater(profile.size, 0) - def test_eagle_enrollment_unknown_speaker(self) -> None: - for path in self.ENROLL_PATHS: - pcm = self.load_wav_resource(path) - percentage, error = self.eagle_profiler.enroll(pcm) - self.assertEqual(error, EagleProfilerEnrollFeedback.AUDIO_OK) - - pcm = self.load_wav_resource(self.IMPOSTER_PATH) - percentage, error = self.eagle_profiler.enroll(pcm) - self.assertEqual(error, EagleProfilerEnrollFeedback.UNKNOWN_SPEAKER) - def test_eagle_process(self) -> None: pcm = self.load_wav_resource(self.TEST_PATH) num_frames = len(pcm) // self.eagle.frame_length scores = [] for i in range(num_frames): - score = self.eagle.process(pcm=pcm[i * self.eagle.frame_length:(i + 1) * self.eagle.frame_length]) + score = self.eagle.process(pcm=pcm[i * self.eagle.frame_length: (i + 1) * self.eagle.frame_length]) scores.append(score[0]) self.assertGreater(max(scores), 0.5) @@ -103,7 +91,7 @@ def test_eagle_process_imposter(self) -> None: num_frames = len(pcm) // self.eagle.frame_length scores = [] for i in range(num_frames): - score = self.eagle.process(pcm=pcm[i * self.eagle.frame_length:(i + 1) * self.eagle.frame_length]) + score = self.eagle.process(pcm=pcm[i * self.eagle.frame_length: (i + 1) * self.eagle.frame_length]) scores.append(score[0]) self.assertLess(max(scores), 0.5) @@ -122,16 +110,17 @@ def test_frame_length(self) -> None: self.assertGreater(self.eagle.frame_length, 0) def test_message_stack(self): - relative_path = '../..' + relative_path = "../.." profile = self.eagle_profiler.export() error = None try: eagle = Eagle( - access_key='invalid', + access_key="invalid", model_path=default_model_path(relative_path), library_path=default_library_path(relative_path), - speaker_profiles=[profile]) + speaker_profiles=[profile], + ) self.assertIsNone(eagle) except EagleError as e: error = e.message_stack @@ -141,22 +130,24 @@ def test_message_stack(self): try: eagle = Eagle( - access_key='invalid', + access_key="invalid", model_path=default_model_path(relative_path), library_path=default_library_path(relative_path), - speaker_profiles=[profile]) + speaker_profiles=[profile], + ) self.assertIsNone(eagle) except EagleError as e: self.assertEqual(len(error), len(e.message_stack)) self.assertListEqual(list(error), list(e.message_stack)) def test_enroll_export_message_stack(self): - relative_path = '../..' + relative_path = "../.." profiler = EagleProfiler( access_key=self.access_key, model_path=default_model_path(relative_path), - library_path=default_library_path(relative_path)) + library_path=default_library_path(relative_path), + ) test_pcm = [0] * 512 address = profiler._eagle_profiler @@ -179,14 +170,15 @@ def test_enroll_export_message_stack(self): profiler._eagle_profiler = address def test_process_message_stack(self): - relative_path = '../..' + relative_path = "../.." profile = self.eagle_profiler.export() eagle = Eagle( access_key=self.access_key, model_path=default_model_path(relative_path), library_path=default_library_path(relative_path), - speaker_profiles=[profile]) + speaker_profiles=[profile], + ) test_pcm = [0] * eagle.frame_length address = eagle._eagle @@ -202,9 +194,9 @@ def test_process_message_stack(self): eagle._eagle = address -if __name__ == '__main__': +if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument('--access-key', required=True) + parser.add_argument("--access-key", required=True) args = parser.parse_args() EagleTestCase.access_key = args.access_key diff --git a/binding/python/test_eagle_perf.py b/binding/python/test_eagle_perf.py index 3e91c51..f31be69 100644 --- a/binding/python/test_eagle_perf.py +++ b/binding/python/test_eagle_perf.py @@ -18,17 +18,16 @@ from time import perf_counter from typing import Sequence -from _eagle import ( - Eagle, - EagleProfiler) +from _eagle import Eagle, EagleProfiler from _util import default_library_path, default_model_path class EaglePerformanceTestCase(unittest.TestCase): ENROLL_PATHS = [ - os.path.join(os.path.dirname(__file__), '../../resources/audio_samples/speaker_1_utt_1.wav'), - os.path.join(os.path.dirname(__file__), '../../resources/audio_samples/speaker_1_utt_2.wav')] - TEST_PATH = os.path.join(os.path.dirname(__file__), '../../resources/audio_samples/speaker_1_test_utt.wav') + os.path.join(os.path.dirname(__file__), "../../resources/audio_samples/speaker_1_utt_1.wav"), + os.path.join(os.path.dirname(__file__), "../../resources/audio_samples/speaker_1_utt_2.wav"), + ] + TEST_PATH = os.path.join(os.path.dirname(__file__), "../../resources/audio_samples/speaker_1_test_utt.wav") access_key: str num_test_iterations: int @@ -37,15 +36,16 @@ class EaglePerformanceTestCase(unittest.TestCase): @staticmethod def load_wav_resource(path: str) -> Sequence[int]: - with wave.open(path, 'rb') as f: + with wave.open(path, "rb") as f: buffer = f.readframes(f.getnframes()) - return struct.unpack('%dh' % f.getnframes(), buffer) + return struct.unpack("%dh" % f.getnframes(), buffer) def test_performance_profiler(self) -> None: eagle_profiler = EagleProfiler( access_key=self.access_key, - model_path=default_model_path('../..'), - library_path=default_library_path('../..')) + model_path=default_model_path("../.."), + library_path=default_library_path("../.."), + ) pcm = self.load_wav_resource(self.TEST_PATH) @@ -66,8 +66,9 @@ def test_performance_recognizer(self) -> None: # create profile eagle_profiler = EagleProfiler( access_key=self.access_key, - model_path=default_model_path('../..'), - library_path=default_library_path('../..')) + model_path=default_model_path("../.."), + library_path=default_library_path("../.."), + ) for path in self.ENROLL_PATHS: pcm = self.load_wav_resource(path) @@ -77,9 +78,10 @@ def test_performance_recognizer(self) -> None: eagle = Eagle( access_key=self.access_key, - model_path=default_model_path('../..'), - library_path=default_library_path('../..'), - speaker_profiles=[profile]) + model_path=default_model_path("../.."), + library_path=default_library_path("../.."), + speaker_profiles=[profile], + ) pcm = self.load_wav_resource(self.TEST_PATH) @@ -89,7 +91,7 @@ def test_performance_recognizer(self) -> None: for i in range(self.num_test_iterations + 1): for n in range(num_frames): start = perf_counter() - _ = eagle.process(pcm=pcm[n * eagle.frame_length:(n + 1) * eagle.frame_length]) + _ = eagle.process(pcm=pcm[n * eagle.frame_length: (n + 1) * eagle.frame_length]) perf_results.append(perf_counter() - start) eagle.delete() @@ -99,12 +101,12 @@ def test_performance_recognizer(self) -> None: self.assertLess(avg_perf, self.recognizer_performance_threshold_sec) -if __name__ == '__main__': +if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument('--access-key', required=True) - parser.add_argument('--num-test-iterations', type=int, required=True) - parser.add_argument('--recognizer-performance-threshold-sec', type=float, required=True) - parser.add_argument('--profiler-performance-threshold-sec', type=float, required=True) + parser.add_argument("--access-key", required=True) + parser.add_argument("--num-test-iterations", type=int, required=True) + parser.add_argument("--recognizer-performance-threshold-sec", type=float, required=True) + parser.add_argument("--profiler-performance-threshold-sec", type=float, required=True) args = parser.parse_args() EaglePerformanceTestCase.access_key = args.access_key diff --git a/demo/python/README.md b/demo/python/README.md index 42e7611..02ac9fc 100644 --- a/demo/python/README.md +++ b/demo/python/README.md @@ -11,12 +11,12 @@ Eagle is an on-device speaker recognition engine. Eagle is: - Linux (x86_64), macOS (x86_64, arm64), Windows (x86_64) - Android and iOS - Chrome, Safari, Firefox, and Edge - - Raspberry Pi (4, 3) and NVIDIA Jetson Nano + - Raspberry Pi (5, 4, 3) and NVIDIA Jetson Nano ## Compatibility -- Python 3.5+ -- Runs on Linux (x86_64), macOS (x86_64, arm64), Windows (x86_64), Raspberry Pi (4, 3), and NVIDIA Jetson Nano. +- Python 3.7+ +- Runs on Linux (x86_64), macOS (x86_64, arm64), Windows (x86_64), Raspberry Pi (5, 4, 3), and NVIDIA Jetson Nano. ## Installation diff --git a/demo/python/requirements.txt b/demo/python/requirements.txt index 4e06c9e..1c8fd44 100644 --- a/demo/python/requirements.txt +++ b/demo/python/requirements.txt @@ -1,2 +1,2 @@ -pveagle==0.2.0 +pveagle==1.0.0 pvrecorder==1.2.1 \ No newline at end of file diff --git a/demo/python/setup.py b/demo/python/setup.py index a961cfc..588442c 100644 --- a/demo/python/setup.py +++ b/demo/python/setup.py @@ -4,11 +4,11 @@ import setuptools -INCLUDE_FILES = ('../../LICENSE', 'eagle_demo_file.py', 'eagle_demo_mic.py') +INCLUDE_FILES = ("../../LICENSE", "eagle_demo_file.py", "eagle_demo_mic.py") -os.system('git clean -dfx') +os.system("git clean -dfx") -package_folder = os.path.join(os.path.dirname(__file__), 'pveagledemo') +package_folder = os.path.join(os.path.dirname(__file__), "pveagledemo") os.mkdir(package_folder) manifest_in = "" @@ -16,15 +16,15 @@ shutil.copy(os.path.join(os.path.dirname(__file__), rel_path), package_folder) manifest_in += "include pveagledemo/%s\n" % os.path.basename(rel_path) -with open(os.path.join(os.path.dirname(__file__), 'MANIFEST.in'), 'w') as f: +with open(os.path.join(os.path.dirname(__file__), "MANIFEST.in"), "w") as f: f.write(manifest_in) -with open(os.path.join(os.path.dirname(__file__), 'README.md'), 'r') as f: +with open(os.path.join(os.path.dirname(__file__), "README.md"), "r") as f: long_description = f.read() setuptools.setup( name="pveagledemo", - version="0.2.0", + version="1.0.0", author="Picovoice", author_email="hello@picovoice.ai", description="Eagle Speaker Recognition Engine demos", @@ -32,22 +32,22 @@ long_description_content_type="text/markdown", url="https://github.com/Picovoice/eagle", packages=["pveagledemo"], - install_requires=["pveagle==0.2.0", "pvrecorder==1.2.1"], + install_requires=["pveagle==1.0.0", "pvrecorder==1.2.1"], include_package_data=True, classifiers=[ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Topic :: Multimedia :: Sound/Audio :: Speech" + "Topic :: Multimedia :: Sound/Audio :: Speech", ], entry_points=dict( console_scripts=[ - 'eagle_demo_file=pveagledemo.eagle_demo_file:main', - 'eagle_demo_mic=pveagledemo.eagle_demo_mic:main', + "eagle_demo_file=pveagledemo.eagle_demo_file:main", + "eagle_demo_mic=pveagledemo.eagle_demo_mic:main", ], ), - python_requires='>=3.5', + python_requires=">=3.7", keywords="Speaker Recognition, Speaker Identification, Voice Recognition, Voice Identification", )