diff --git a/.github/workflows/python-demos.yml b/.github/workflows/python-demos.yml index af98c18..e8bbdd3 100644 --- a/.github/workflows/python-demos.yml +++ b/.github/workflows/python-demos.yml @@ -39,6 +39,12 @@ jobs: - name: Pre-build dependencies run: python -m pip install --upgrade pip + # ************** REMOVE AFTER RELEASE ******************** + - name: Build binding + run: | + pip install wheel && cd ../../binding/python && python setup.py sdist bdist_wheel && pip install dist/pvoctopus-2.0.0-py3-none-any.whl + # ******************************************************** + - name: Install dependencies run: pip install -r requirements.txt diff --git a/.github/workflows/python-perf.yml b/.github/workflows/python-perf.yml index 6ac0155..3887472 100644 --- a/.github/workflows/python-perf.yml +++ b/.github/workflows/python-perf.yml @@ -35,13 +35,13 @@ jobs: os: [ubuntu-latest, windows-latest, macos-latest] include: - os: ubuntu-latest - index_performance_threshold_sec: 1.4 + index_performance_threshold_sec: 2.2 search_performance_threshold_sec: 0.001 - os: windows-latest - index_performance_threshold_sec: 1.4 + index_performance_threshold_sec: 2.2 search_performance_threshold_sec: 0.001 - os: macos-latest - index_performance_threshold_sec: 2.0 + index_performance_threshold_sec: 2.8 search_performance_threshold_sec: 0.001 steps: diff --git a/binding/python/README.md b/binding/python/README.md index 646a1ed..c4e3a37 100644 --- a/binding/python/README.md +++ b/binding/python/README.md @@ -1,4 +1,6 @@ -# Octopus +# Octopus Binding for Python + +## Octopus Made in Vancouver, Canada by [Picovoice](https://picovoice.ai) @@ -30,8 +32,8 @@ Create an instance of the engine: ```python import pvoctopus -access_key = "" # AccessKey obtained from Picovoice Console (https://console.picovoice.ai/) -handle = pvoctopus.create(access_key=access_key) +access_key = "${ACCESS_KEY}" # AccessKey obtained from Picovoice Console (https://console.picovoice.ai/) +octopus = pvoctopus.create(access_key=access_key) ``` Octopus consists of two steps: Indexing and Searching. Indexing transforms audio data into a `Metadata` object that @@ -44,7 +46,7 @@ The engine accepts 16-bit linearly-encoded PCM and operates on single-channel au ```python audio_data = [...] -metadata = handle.index(audio_data) +metadata = octopus.index(audio_data) ``` Similarly, files can be indexed by passing in the absolute file path to the audio object. @@ -52,7 +54,7 @@ Supported file formats are mp3, flac, wav and opus: ```python audio_file_path = "/path/to/my/audiofile.wav" -metadata = handle.index_file(audio_file_path) +metadata = octopus.index_file(audio_file_path) ``` Once the `Metadata` object has been created, it can be used for searching: @@ -90,10 +92,10 @@ cached_metadata = pvoctopus.OctopusMetadata.from_bytes(metadata_bytes) matches = octopus.search(cached_metadata, ['avocado']) ``` -When done the handle resources have to be released explicitly: +When done the Octopus, resources have to be released explicitly: ```python -handle.delete() +octopus.delete() ``` ## Non-English Models diff --git a/binding/python/__init__.py b/binding/python/__init__.py index 6532a75..000756c 100644 --- a/binding/python/__init__.py +++ b/binding/python/__init__.py @@ -1,5 +1,5 @@ # -# Copyright 2021-2022 Picovoice Inc. +# Copyright 2021-2023 Picovoice Inc. # # You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" # file accompanying this source. @@ -9,32 +9,6 @@ # specific language governing permissions and limitations under the License. # -from typing import * - -from .octopus import * -from .util import * - -LIBRARY_PATH = pv_library_path('') - -MODEL_PATH = pv_model_path('', 'en') - - -def create(access_key: str, model_path: Optional[str] = None, library_path: Optional[str] = None) -> Octopus: - """ - Factory method for Octopus Speech-to-Index engine. - - :param access_key: AccessKey provided by Picovoice Console (https://console.picovoice.ai/) - :param model_path: Absolute path to the file containing model parameters. If not set it will be set to the default - location for English model. - :param library_path: Absolute path to Octopus' dynamic library. If not set it will be set to the default - location. - :return An instance of Octopus Speech-to-Index engine. - """ - - if model_path is None: - model_path = MODEL_PATH - - if library_path is None: - library_path = LIBRARY_PATH - - return Octopus(access_key=access_key, model_path=model_path, library_path=library_path) +from ._factory import * +from ._octopus import * +from ._util import * diff --git a/binding/python/_factory.py b/binding/python/_factory.py new file mode 100644 index 0000000..c000654 --- /dev/null +++ b/binding/python/_factory.py @@ -0,0 +1,42 @@ +# +# Copyright 2021-2023 Picovoice Inc. +# +# You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" +# file accompanying this source. +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# + +from typing import Optional + +from ._octopus import Octopus +from ._util import default_library_path, default_model_path + + +def create(access_key: str, model_path: Optional[str] = None, library_path: Optional[str] = None) -> Octopus: + """ + Factory method for Octopus Speech-to-Index engine. + + :param access_key: AccessKey provided by Picovoice Console (https://console.picovoice.ai/) + :param model_path: Absolute path to the file containing model parameters. If not set it will be set to the default + location for English model. + :param library_path: Absolute path to Octopus' dynamic library. If not set it will be set to the default + location. + :return An instance of Octopus Speech-to-Index engine. + """ + + if model_path is None: + model_path = default_model_path() + + if library_path is None: + library_path = default_library_path() + + return Octopus( + access_key=access_key, + model_path=model_path, + library_path=library_path) + + +__all__ = ['create'] diff --git a/binding/python/octopus.py b/binding/python/_octopus.py similarity index 64% rename from binding/python/octopus.py rename to binding/python/_octopus.py index 51f1ff2..ef3e380 100644 --- a/binding/python/octopus.py +++ b/binding/python/_octopus.py @@ -1,5 +1,5 @@ # -# Copyright 2021-2022 Picovoice Inc. +# Copyright 2021-2023 Picovoice Inc. # # You may not use this file except in compliance with the license. # A copy of the license is located in the "LICENSE" file accompanying this @@ -20,7 +20,27 @@ class OctopusError(Exception): - pass + def __init__(self, message: str = '', message_stack: Sequence[str] = None): + super().__init__(message) + + self._message = message + self._message_stack = list() if message_stack is None else message_stack + + def __str__(self): + message = self._message + if len(self._message_stack) > 0: + message += ':' + for i in range(len(self._message_stack)): + message += '\n [%d] %s' % (i, self._message_stack[i]) + return message + + @property + def message(self) -> str: + return self._message + + @property + def message_stack(self) -> Sequence[str]: + return self._message_stack class OctopusMemoryError(OctopusError): @@ -87,10 +107,6 @@ def size(self) -> int: def to_bytes(self) -> bytes: return self._to_bytes(self.handle, self.size) - @classmethod - def create_owned(cls, handle: c_void_p, size: int) -> 'OctopusMetadata': - return cls.from_bytes(cls._to_bytes(handle, size)) - @classmethod def from_bytes(cls, metadata_bytes: bytes) -> 'OctopusMetadata': byte_ptr = (c_byte * len(metadata_bytes)).from_buffer_copy(metadata_bytes) @@ -139,7 +155,11 @@ class PicovoiceStatuses(Enum): class COctopus(Structure): pass - def __init__(self, access_key: str, model_path: str, library_path: str) -> None: + def __init__( + self, + access_key: str, + model_path: str, + library_path: str) -> None: """ Constructor. @@ -148,43 +168,77 @@ def __init__(self, access_key: str, model_path: str, library_path: str) -> None: :param library_path: Absolute path to Octopus' dynamic library. """ + if not isinstance(access_key, str) or len(access_key) == 0: + raise OctopusInvalidArgumentError("`access_key` should be a non-empty string.") + if not os.path.exists(model_path): - raise IOError("Couldn't find model file at `%s`." % model_path) + raise OctopusIOError("Couldn't find model file at `%s`." % model_path) if not os.path.exists(library_path): - raise IOError("Couldn't find dynamic library at '%s'." % library_path) + raise OctopusIOError("Couldn't find dynamic library at '%s'." % library_path) library = cdll.LoadLibrary(library_path) + set_sdk_func = library.pv_set_sdk + set_sdk_func.argtypes = [c_char_p] + set_sdk_func.restype = None + + 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.restype = self.PicovoiceStatuses + + self._free_error_stack_func = library.pv_free_error_stack + self._free_error_stack_func.argtypes = [POINTER(c_char_p)] + self._free_error_stack_func.restype = None + init_func = library.pv_octopus_init init_func.argtypes = [c_char_p, c_char_p, POINTER(POINTER(self.COctopus))] init_func.restype = self.PicovoiceStatuses self._handle = POINTER(self.COctopus)() - status = init_func(access_key.encode('utf-8'), model_path.encode('utf-8'), byref(self._handle)) + status = init_func( + access_key.encode('utf-8'), + model_path.encode('utf-8'), + byref(self._handle)) if status is not self.PicovoiceStatuses.SUCCESS: - raise self._PICOVOICE_STATUS_TO_EXCEPTION[status]() + raise self._PICOVOICE_STATUS_TO_EXCEPTION[status]( + message='Initialization failed', + message_stack=self._get_error_stack()) self._delete_func = library.pv_octopus_delete self._delete_func.argtypes = [POINTER(self.COctopus)] self._delete_func.restype = None + self._index_size_func = library.pv_octopus_index_size + self._index_size_func.argtypes = [ + POINTER(self.COctopus), + c_int32, + POINTER(c_int32)] + self._index_size_func.restype = self.PicovoiceStatuses + self._index_func = library.pv_octopus_index self._index_func.argtypes = [ POINTER(self.COctopus), POINTER(c_short), c_int32, - POINTER(c_void_p), - POINTER(c_int32)] + c_void_p] self._index_func.restype = self.PicovoiceStatuses + self._index_file_size_func = library.pv_octopus_index_file_size + self._index_file_size_func.argtypes = [ + POINTER(self.COctopus), + c_char_p, + POINTER(c_int32)] + self._index_file_size_func.restype = self.PicovoiceStatuses + self._index_file_func = library.pv_octopus_index_file self._index_file_func.argtypes = [ POINTER(self.COctopus), c_char_p, - POINTER(c_void_p), - POINTER(c_int32)] + c_void_p] self._index_file_func.restype = self.PicovoiceStatuses self._search_func = library.pv_octopus_search @@ -197,6 +251,10 @@ def __init__(self, access_key: str, model_path: str, library_path: str) -> None: POINTER(c_int32)] self._search_func.restype = self.PicovoiceStatuses + self._matches_delete_func = library.pv_octopus_matches_delete + self._matches_delete_func.argtypes = [POINTER(self.CMatch)] + self._matches_delete_func.restype = None + version_func = library.pv_octopus_version version_func.argtypes = [] version_func.restype = c_char_p @@ -204,10 +262,6 @@ def __init__(self, access_key: str, model_path: str, library_path: str) -> None: self._sample_rate = library.pv_sample_rate() - self._pv_free = library.pv_free - self._pv_free.argtypes = [c_void_p] - self._pv_free.restype = None - def delete(self) -> None: """Releases resources acquired by Octopus.""" @@ -222,22 +276,29 @@ def index_audio_data(self, pcm: Sequence[int]) -> OctopusMetadata: :return metadata: An immutable metadata object. """ - c_metadata = c_void_p() metadata_size = c_int32() + status = self._index_size_func( + self._handle, + c_int32(len(pcm)), + byref(metadata_size)) + if status is not self.PicovoiceStatuses.SUCCESS: + raise self._PICOVOICE_STATUS_TO_EXCEPTION[status]( + message='Index size failed', + message_stack=self._get_error_stack()) + metadata_bytes = create_string_buffer(metadata_size.value) + metadata_bytes_ptr = cast(metadata_bytes, c_void_p) status = self._index_func( self._handle, (c_short * len(pcm))(*pcm), c_int32(len(pcm)), - byref(c_metadata), - byref(metadata_size)) + metadata_bytes_ptr) if status is not self.PicovoiceStatuses.SUCCESS: - raise self._PICOVOICE_STATUS_TO_EXCEPTION[status]() + raise self._PICOVOICE_STATUS_TO_EXCEPTION[status]( + message='Index failed', + message_stack=self._get_error_stack()) - metadata = OctopusMetadata.create_owned(c_metadata, metadata_size.value) - self._pv_free(c_metadata) - - return metadata + return OctopusMetadata(metadata_bytes_ptr, metadata_size.value) def index_audio_file(self, path: str) -> OctopusMetadata: """ @@ -248,23 +309,30 @@ def index_audio_file(self, path: str) -> OctopusMetadata: """ if not os.path.exists(path): - raise IOError("Couldn't find input file at `%s`." % path) + raise OctopusIOError("Couldn't find input file at `%s`." % path) - c_metadata = c_void_p() metadata_size = c_int32() - - status = self._index_file_func( + status = self._index_file_size_func( self._handle, path.encode('utf-8'), - byref(c_metadata), byref(metadata_size)) if status is not self.PicovoiceStatuses.SUCCESS: - raise self._PICOVOICE_STATUS_TO_EXCEPTION[status](status.name) + raise self._PICOVOICE_STATUS_TO_EXCEPTION[status]( + message='Index file size failed', + message_stack=self._get_error_stack()) - metadata = OctopusMetadata.create_owned(c_metadata, metadata_size.value) - self._pv_free(c_metadata) + metadata_bytes = create_string_buffer(metadata_size.value) + metadata_bytes_ptr = cast(metadata_bytes, c_void_p) + status = self._index_file_func( + self._handle, + path.encode('utf-8'), + metadata_bytes) + if status is not self.PicovoiceStatuses.SUCCESS: + raise self._PICOVOICE_STATUS_TO_EXCEPTION[status]( + message='Index file failed', + message_stack=self._get_error_stack()) - return metadata + return OctopusMetadata(metadata_bytes_ptr, metadata_size.value) Match = namedtuple('Match', ['start_sec', 'end_sec', 'probability']) @@ -301,7 +369,9 @@ def search(self, metadata: OctopusMetadata, phrases: Iterable[str]) -> Dict[str, byref(c_phrase_matches), byref(num_phrase_matches)) if status is not self.PicovoiceStatuses.SUCCESS: - raise self._PICOVOICE_STATUS_TO_EXCEPTION[status]() + raise self._PICOVOICE_STATUS_TO_EXCEPTION[status]( + message='Search failed', + message_stack=self._get_error_stack()) if num_phrase_matches.value > 0: phrase_matches = list() for i in range(num_phrase_matches.value): @@ -311,6 +381,7 @@ def search(self, metadata: OctopusMetadata, phrases: Iterable[str]) -> Dict[str, probability=c_phrase_matches[i].probability) phrase_matches.append(match) matches[phrase] = phrase_matches + self._matches_delete_func(c_phrase_matches) return matches @@ -326,6 +397,21 @@ def sample_rate(self) -> int: return self._sample_rate + def _get_error_stack(self) -> Sequence[str]: + message_stack_ref = POINTER(c_char_p)() + message_stack_depth = c_int() + status = self._get_error_stack_func(byref(message_stack_ref), byref(message_stack_depth)) + if status is not self.PicovoiceStatuses.SUCCESS: + raise self._PICOVOICE_STATUS_TO_EXCEPTION[status](message='Unable to get Porcupine error state') + + message_stack = list() + for i in range(message_stack_depth.value): + message_stack.append(message_stack_ref[i].decode('utf-8')) + + self._free_error_stack_func(message_stack_ref) + + return message_stack + __all__ = [ 'OctopusError', diff --git a/binding/python/util.py b/binding/python/_util.py similarity index 81% rename from binding/python/util.py rename to binding/python/_util.py index e8e7e41..b0c8d1f 100644 --- a/binding/python/util.py +++ b/binding/python/_util.py @@ -1,5 +1,5 @@ # -# Copyright 2021-2022 Picovoice Inc. +# Copyright 2021-2023 Picovoice Inc. # # You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" # file accompanying this source. @@ -9,14 +9,10 @@ # specific language governing permissions and limitations under the License. # -import logging import os import platform from typing import Tuple -log = logging.getLogger('OCT') -log.setLevel(logging.WARNING) - def _pv_platform() -> Tuple[str, str]: pv_system = platform.system() @@ -31,7 +27,7 @@ def _pv_platform() -> Tuple[str, str]: _PV_SYSTEM, _PV_MACHINE = _pv_platform() -def pv_library_path(relative_path: str) -> str: +def default_library_path(relative_path: str = '') -> str: if _PV_SYSTEM == 'Darwin': if _PV_MACHINE == 'x86_64': return os.path.join(os.path.dirname(__file__), relative_path, 'lib/mac/x86_64/libpv_octopus.dylib') @@ -46,14 +42,14 @@ def pv_library_path(relative_path: str) -> str: raise NotImplementedError('Unsupported platform.') -def pv_model_path(relative_path: str, language: str) -> str: +def default_model_path(relative_path: str = '') -> str: return os.path.join( os.path.dirname(__file__), relative_path, - 'lib/common/param/octopus_params%s.pv' % ('' if language == 'en' else ('_' + language))) + 'lib/common/param/octopus_params.pv') __all__ = [ - 'pv_library_path', - 'pv_model_path', + 'default_library_path', + 'default_model_path', ] diff --git a/binding/python/setup.py b/binding/python/setup.py index 4559066..b3abeed 100644 --- a/binding/python/setup.py +++ b/binding/python/setup.py @@ -3,51 +3,42 @@ import setuptools +INCLUDE_FILES = ('../../LICENSE', '__init__.py', '_factory.py', '_octopus.py', '_util.py') +INCLUDE_LIBS = ('linux', 'mac', 'windows') + os.system('git clean -dfx') package_folder = os.path.join(os.path.dirname(__file__), 'pvoctopus') os.mkdir(package_folder) +manifest_in = "" -shutil.copy(os.path.join(os.path.dirname(__file__), '../../LICENSE'), package_folder) - -shutil.copy(os.path.join(os.path.dirname(__file__), '__init__.py'), os.path.join(package_folder, '__init__.py')) -shutil.copy(os.path.join(os.path.dirname(__file__), 'octopus.py'), os.path.join(package_folder, 'octopus.py')) -shutil.copy(os.path.join(os.path.dirname(__file__), 'util.py'), os.path.join(package_folder, 'util.py')) +for rel_path in INCLUDE_FILES: + shutil.copy(os.path.join(os.path.dirname(__file__), rel_path), package_folder) + manifest_in += "include pvoctopus/%s\n" % os.path.basename(rel_path) -platforms = ('linux', 'mac', 'windows') +model_subdir = 'lib/common/param' +model_file = 'octopus_params.pv' +os.makedirs(os.path.join(package_folder, model_subdir)) +shutil.copy( + os.path.join(os.path.dirname(__file__), '../..', model_subdir, model_file), + os.path.join(package_folder, model_subdir, model_file)) +manifest_in += "include pvoctopus/%s/%s\n" % (model_subdir, model_file) -os.mkdir(os.path.join(package_folder, 'lib')) -for platform in platforms: +for platform in INCLUDE_LIBS: shutil.copytree( os.path.join(os.path.dirname(__file__), '../../lib', platform), os.path.join(package_folder, 'lib', platform)) - -os.makedirs(os.path.join(package_folder, 'lib/common/param')) -shutil.copy( - os.path.join(os.path.dirname(__file__), '../../lib/common/param/octopus_params.pv'), - os.path.join(package_folder, 'lib/common/param/octopus_params.pv')) - -MANIFEST_IN = """ -include pvoctopus/LICENSE -include pvoctopus/__init__.py -include pvoctopus/octopus.py -include pvoctopus/util.py -include pvoctopus/lib/common/param/octopus_params.pv -include pvoctopus/lib/linux/x86_64/libpv_octopus.so -include pvoctopus/lib/mac/x86_64/libpv_octopus.dylib -include pvoctopus/lib/mac/arm64/libpv_octopus.dylib -include pvoctopus/lib/windows/amd64/libpv_octopus.dll -""" + manifest_in += "recursive-include pvoctopus/lib/%s *\n" % platform with open(os.path.join(os.path.dirname(__file__), 'MANIFEST.in'), 'w') as f: - f.write(MANIFEST_IN.strip('\n ')) + f.write(manifest_in) with open(os.path.join(os.path.dirname(__file__), 'README.md'), 'r') as f: long_description = f.read() setuptools.setup( name="pvoctopus", - version="1.2.1", + version="2.0.0", author="Picovoice", author_email="hello@picovoice.ai", description="Octopus Speech-to-Index engine.", diff --git a/binding/python/test_octopus.py b/binding/python/test_octopus.py index 1ffd078..ffb2c3a 100644 --- a/binding/python/test_octopus.py +++ b/binding/python/test_octopus.py @@ -1,5 +1,5 @@ # -# Copyright 2021-2022 Picovoice Inc. +# Copyright 2021-2023 Picovoice Inc. # # You may not use this file except in compliance with the license. # A copy of the license is located in the "LICENSE" file accompanying this @@ -19,9 +19,9 @@ from parameterized import parameterized -from octopus import * +from _octopus import * +from _util import * from test_util import * -from util import * TEST_PARAMS = [ ["en", {"alexa": [(7.648, 8.352, 1)], "porcupine": [(5.728, 6.752, 1), (35.360, 36.416, 1)]}], @@ -29,7 +29,7 @@ ["es", {"manzana": [(5.184, 5.984, 1)]}], ["fr", {"perroquet": [(4.352, 5.184, 0.952)]}], ["it", {"porcospino": [(0.480, 1.728, 1)]}], - ["ja", {"りんご": [(0.960, 1.664, 1)]}], + ["ja", {"りんご": [(0.990, 1.634, 1)]}], ["ko", {"아이스크림": [(6.592, 7.520, 0.961)]}], ["pt", {"porco espinho": [(0.480, 1.792, 1)]}], ] @@ -39,18 +39,13 @@ class OctopusTestCase(unittest.TestCase): @classmethod def setUpClass(cls): cls._access_key = sys.argv[1] + cls._relative = '../..' - def _create_octopus(self, language: str) -> Octopus: + def _create_octopus(self, language: str = 'en') -> Octopus: return Octopus( access_key=self._access_key, - library_path=pv_library_path('../..'), - model_path=pv_model_path('../..', language)) - - @staticmethod - def _audio_path(language: str) -> str: - return os.path.join( - os.path.dirname(__file__), - '../../res/audio/multiple_keywords%s.wav' % ('' if language == 'en' else ('_' + language))) + library_path=default_library_path(self._relative), + model_path=get_model_path_by_language(self._relative, language)) def _check_matches( self, phrase_matches: Dict[str, Sequence[Octopus.Match]], @@ -69,7 +64,8 @@ def test_index(self, language: str, phrase_occurrences: Dict[str, Sequence[Tuple try: octopus = self._create_octopus(language) - metadata = octopus.index_audio_data(read_wav_file(self._audio_path(language), octopus.sample_rate)) + audio_data = read_wav_file(get_audio_path_by_language(self._relative, language), octopus.sample_rate) + metadata = octopus.index_audio_data(audio_data) phrase_matches = octopus.search(metadata, list(phrase_occurrences.keys())) self._check_matches(phrase_matches, phrase_occurrences) finally: @@ -82,7 +78,7 @@ def _test_index_file(self, language: str, phrase_occurrences: Dict[str, Sequence try: octopus = self._create_octopus(language) - metadata = octopus.index_audio_file(self._audio_path(language)) + metadata = octopus.index_audio_file(get_audio_path_by_language(self._relative, language)) phrase_matches = octopus.search(metadata, list(phrase_occurrences.keys())) self._check_matches(phrase_matches, phrase_occurrences) finally: @@ -93,8 +89,8 @@ def _test_invalid(self, phrase: str) -> None: octopus = None try: - octopus = self._create_octopus('en') - metadata = octopus.index_audio_file(self._audio_path('en')) + octopus = self._create_octopus() + metadata = octopus.index_audio_file(get_audio_path_by_language(self._relative)) with self.assertRaises(OctopusInvalidArgumentError): octopus.search(metadata, [phrase]) finally: @@ -120,8 +116,8 @@ def test_index_with_spaces(self): octopus = None try: - octopus = self._create_octopus('en') - metadata = octopus.index_audio_file(self._audio_path('en')) + octopus = self._create_octopus() + metadata = octopus.index_audio_file(get_audio_path_by_language(self._relative)) search_term = ' americano avocado ' normalized_search_term = 'americano avocado' matches = octopus.search(metadata, [search_term]) @@ -143,7 +139,7 @@ def test_to_from_bytes(self, language: str, phrase_occurrences: Dict[str, Sequen try: octopus = self._create_octopus(language) - original_metadata = octopus.index_audio_file(self._audio_path(language)) + original_metadata = octopus.index_audio_file(get_audio_path_by_language(self._relative, language)) metadata_bytes = original_metadata.to_bytes() metadata = OctopusMetadata.from_bytes(metadata_bytes) @@ -164,7 +160,7 @@ def test_to_from_bytes_file( try: octopus = self._create_octopus(language) - original_metadata = octopus.index_audio_file(self._audio_path(language)) + original_metadata = octopus.index_audio_file(get_audio_path_by_language(self._relative, language)) with open(cache_path, 'wb') as f: f.write(original_metadata.to_bytes()) @@ -183,12 +179,63 @@ def test_version(self): octopus = None try: - octopus = self._create_octopus('en') + octopus = self._create_octopus() self.assertIsInstance(octopus.version, str) finally: if octopus is not None: octopus.delete() + def test_message_stack(self): + error = None + try: + o = Octopus( + access_key='invalid', + library_path=default_library_path(self._relative), + model_path=get_model_path_by_language(self._relative)) + self.assertIsNone(o) + except OctopusError as e: + error = e.message_stack + + self.assertIsNotNone(error) + self.assertGreater(len(error), 0) + + try: + o = Octopus( + access_key='invalid', + library_path=default_library_path(self._relative), + model_path=get_model_path_by_language(self._relative)) + self.assertIsNone(o) + except OctopusError as e: + self.assertEqual(len(error), len(e.message_stack)) + self.assertListEqual(list(error), list(e.message_stack)) + + def test_index_search_message_stack(self): + o = Octopus( + access_key=self._access_key, + library_path=default_library_path(self._relative), + model_path=get_model_path_by_language(self._relative)) + test_pcm = [0] * 512 + test_metadata = o.index_audio_file(get_audio_path_by_language(self._relative, "en")) + + address = o._handle + o._handle = None + + try: + res = o.index_audio_data(test_pcm) + self.assertIsNone(res) + except OctopusError as e: + self.assertGreater(len(e.message_stack), 0) + self.assertLess(len(e.message_stack), 8) + + try: + res = o.search(test_metadata, ["test"]) + self.assertIsNone(res) + except OctopusError as e: + self.assertGreater(len(e.message_stack), 0) + self.assertLess(len(e.message_stack), 8) + + o._handle = address + if __name__ == '__main__': if len(sys.argv) != 2: diff --git a/binding/python/test_octopus_perf.py b/binding/python/test_octopus_perf.py index 36cb006..0d3f312 100644 --- a/binding/python/test_octopus_perf.py +++ b/binding/python/test_octopus_perf.py @@ -1,5 +1,5 @@ # -# Copyright 2022 Picovoice Inc. +# Copyright 2022-2023 Picovoice Inc. # # You may not use this file except in compliance with the license. # A copy of the license is located in the "LICENSE" file accompanying this @@ -12,14 +12,13 @@ # limitations under the License. # -import os import sys import time import unittest -from octopus import * +from _octopus import * +from _util import * from test_util import * -from util import * class OctopusTestCase(unittest.TestCase): @@ -28,17 +27,19 @@ class OctopusTestCase(unittest.TestCase): @classmethod def setUpClass(cls): access_key = sys.argv[1] + relative = '../..' + cls.num_test_iterations = int(sys.argv[2]) cls.index_performance_threshold_sec = float(sys.argv[3]) cls.search_performance_threshold_sec = float(sys.argv[4]) cls.octopus = Octopus( access_key=access_key, - library_path=pv_library_path('../..'), - model_path=pv_model_path('../..', 'en')) + library_path=default_library_path(relative), + model_path=get_model_path_by_language(relative)) cls.audio = read_wav_file( - os.path.join(os.path.dirname(__file__), '../../res/audio/multiple_keywords.wav'), + get_audio_path_by_language(relative), cls.octopus.sample_rate) @classmethod diff --git a/binding/python/test_util.py b/binding/python/test_util.py index a17d2ed..ead81e2 100644 --- a/binding/python/test_util.py +++ b/binding/python/test_util.py @@ -1,5 +1,5 @@ # -# Copyright 2022 Picovoice Inc. +# Copyright 2022-2023 Picovoice Inc. # # You may not use this file except in compliance with the license. A copy of the license is located in the "LICENSE" # file accompanying this source. @@ -9,6 +9,7 @@ # specific language governing permissions and limitations under the License. # +import os import struct import wave from typing import Sequence @@ -34,6 +35,24 @@ def read_wav_file(file_name: str, sample_rate: int) -> Sequence[int]: return frames[::channels] +def get_audio_path_by_language(relative: str, language: str = 'en') -> str: + audio_file_path = 'res/audio/multiple_keywords%s.wav' % ('' if language == 'en' else ('_' + language)) + return os.path.join( + os.path.dirname(__file__), + relative, + audio_file_path) + + +def get_model_path_by_language(relative: str, language: str = 'en'): + model_file_path = 'lib/common/param/octopus_params%s.pv' % ('' if language == 'en' else ('_%s' % language)) + return os.path.join( + os.path.dirname(__file__), + relative, + model_file_path) + + __all__ = [ + 'get_audio_path_by_language', + 'get_model_path_by_language', 'read_wav_file' ] diff --git a/demo/python/octopus_demo.py b/demo/python/octopus_demo.py index 800578a..96b16f6 100644 --- a/demo/python/octopus_demo.py +++ b/demo/python/octopus_demo.py @@ -49,25 +49,20 @@ def stop(self): def main(): parser = argparse.ArgumentParser() - parser.add_argument('--audio_paths', nargs='+', help='Absolute paths to input audio files', required=True) - - parser.add_argument('--library_path', help='Absolute path to dynamic library', default=pvoctopus.LIBRARY_PATH) - - parser.add_argument( - '--model_path', - help='Absolute path to the file containing model parameters', - default=pvoctopus.MODEL_PATH) - parser.add_argument( '--access_key', help='AccessKey provided by Picovoice Console (https://console.picovoice.ai/)', required=True) + parser.add_argument('--audio_paths', nargs='+', help='Absolute paths to input audio files', required=True) + + parser.add_argument('--library_path', help='Absolute path to dynamic library') + + parser.add_argument('--model_path', help='Absolute path to the file containing model parameters') + parser.add_argument( '--search_phrase', - help='Phrase to search in the provided audio paths', - default=None - ) + help='Phrase to search in the provided audio paths') args = parser.parse_args() @@ -77,7 +72,7 @@ def main(): library_path=args.library_path, model_path=args.model_path) print("Octopus version: %s" % octopus.version) - except (MemoryError, ValueError, RuntimeError, PermissionError) as e: + except pvoctopus.OctopusError as e: print(e) sys.exit(1) @@ -88,7 +83,7 @@ def main(): try: print("\rindexing '%s'" % os.path.basename(audio_file)) metadata_list.append(octopus.index_audio_file(os.path.abspath(audio_file))) - except (MemoryError, ValueError, RuntimeError, PermissionError, IOError) as e: + except pvoctopus.OctopusError as e: print("Failed to process '%s' with '%s'" % (os.path.basename(audio_file), e)) octopus.delete() sys.exit(1) @@ -104,7 +99,7 @@ def main(): for i, metadata in enumerate(metadata_list): try: matches = octopus.search(metadata, [str(search_phrase)]) - except pvoctopus.OctopusInvalidArgumentError as e: + except pvoctopus.OctopusError as e: print(e) continue if len(matches) != 0: diff --git a/demo/python/requirements.txt b/demo/python/requirements.txt index d16245e..c035c47 100644 --- a/demo/python/requirements.txt +++ b/demo/python/requirements.txt @@ -1,2 +1,2 @@ -pvoctopus==1.2.0 +pvoctopus==2.0.0 tabulate diff --git a/demo/python/setup.py b/demo/python/setup.py index f6c8ba6..25c286f 100644 --- a/demo/python/setup.py +++ b/demo/python/setup.py @@ -23,7 +23,7 @@ setuptools.setup( name="pvoctopusdemo", - version="1.2.1", + version="2.0.0", author="Picovoice", author_email="hello@picovoice.ai", description="Octopus Speech-to-Index engine demo.", @@ -31,7 +31,7 @@ long_description_content_type="text/markdown", url="https://github.com/Picovoice/octopus", packages=["pvoctopusdemo"], - install_requires=["pvoctopus==1.2.1", "tabulate"], + install_requires=["pvoctopus==2.0.0", "tabulate"], include_package_data=True, classifiers=[ "Development Status :: 5 - Production/Stable",