diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..5b1982f --- /dev/null +++ b/.clang-format @@ -0,0 +1,88 @@ +# Generated from CLion C/C++ Code Style settings +BasedOnStyle: LLVM +Cpp11BracedListStyle: true +AccessModifierOffset: -4 +AlignConsecutiveMacros: true +AlignTrailingComments: false +AlignAfterOpenBracket: Align +AllowAllArgumentsOnNextLine: false +AllowAllConstructorInitializersOnNextLine: false +AllowAllParametersOfDeclarationOnNextLine: false +AlignArrayOfStructures: Left +AllowShortBlocksOnASingleLine: true +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: None +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: None +AllowShortLoopsOnASingleLine: false +AlwaysBreakAfterReturnType: None +AlwaysBreakTemplateDeclarations: Yes +BreakBeforeBraces: Custom +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterExternBlock: false + AfterControlStatement: Never + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterUnion: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: true +# BraceBreakingStyle: Attach +BreakBeforeBinaryOperators: None +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeColon +BreakInheritanceList: BeforeColon +ColumnLimit: 0 +CommentPragmas: '^[^ ]' +CompactNamespaces: false +ContinuationIndentWidth: 8 +IncludeBlocks: Preserve +IncludeCategories: + - Regex: '^"(llvm|llvm-c|clang|clang-c)/' + Priority: 2 + SortPriority: 0 + CaseSensitive: false + - Regex: '^(<|"(gtest|gmock|isl|json)/)' + Priority: 3 + SortPriority: 0 + CaseSensitive: false + - Regex: '.*' + Priority: 1 + SortPriority: 0 + CaseSensitive: false +IndentCaseLabels: true +IndentPPDirectives: None +IndentWidth: 4 +InsertTrailingCommas: Wrapped +KeepEmptyLinesAtTheStartOfBlocks: true +MaxEmptyLinesToKeep: 2 +NamespaceIndentation: All +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PointerAlignment: Right +ReflowComments: false +SpaceAfterCStyleCast: true +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: false +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 1 +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInContainerLiterals: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +TabWidth: 4 +UseTab: Never \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fc47611 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +*.py linguist-detectable=true +demo/** linguist-detectable=false +lib/** linguist-detectable=false +resources/** linguist-detectable=false diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..3e084a4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,20 @@ +--- +name: Bug report +about: Bugs in Falcon +title: 'Falcon Issue: ' +labels: bug +assignees: '' + +--- + +Make sure you have read the documentation, and have put forth a reasonable effort to find an existing answer. + +### Expected behaviour + + +### Actual behaviour + + +### Steps to reproduce the behaviour + +(Include enough details so that the issue can be reproduced independently.) diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md new file mode 100644 index 0000000..4405efe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.md @@ -0,0 +1,14 @@ +--- +name: Documentation +about: Issues around documentation of Falcon +title: Falcon Documentation Issue +labels: documentation +assignees: '' + +--- + +### What is the URL of the doc? + + + +### What's the nature of the issue? (e.g. steps do not work, typos/grammar/spelling, etc., out of date) diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..36d33b3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Falcon suggestion +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/c-codestyle.yml b/.github/workflows/c-codestyle.yml new file mode 100644 index 0000000..1f8e6f5 --- /dev/null +++ b/.github/workflows/c-codestyle.yml @@ -0,0 +1,32 @@ +name: C Codestyle + +on: + workflow_dispatch: + push: + branches: [ main ] + paths: + - '**/*.c' + - '.github/workflows/c-codestyle.yml' + pull_request: + branches: [ main, 'v[0-9]+.[0-9]+' ] + paths: + - '**/*.c' + - '.github/workflows/c-codestyle.yml' + +jobs: + check-c-codestyle: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install dependencies + run: sudo apt install clang-format + + - name: Check c codestyle + run: python3 resources/.lint/c/formatter.py -c -v \ No newline at end of file diff --git a/.github/workflows/c-demos.yml b/.github/workflows/c-demos.yml new file mode 100644 index 0000000..5bcb4c8 --- /dev/null +++ b/.github/workflows/c-demos.yml @@ -0,0 +1,119 @@ +name: C Demos + +on: + workflow_dispatch: + push: + branches: [ main ] + paths: + - '!demo/c/README.md' + - '.github/workflows/c-demos.yml' + - 'demo/c/**' + - 'lib/common/**' + - 'lib/jetson/**' + - 'lib/linux/**' + - 'lib/mac/**' + - 'lib/raspberry-pi/**' + - 'lib/windows/**' + - 'resources/audio_samples/*.wav' + pull_request: + branches: [ main, 'v[0-9]+.[0-9]+' ] + paths: + - '!demo/c/README.md' + - '.github/workflows/c-demos.yml' + - 'demo/c/**' + - 'lib/common/**' + - 'lib/jetson/**' + - 'lib/linux/**' + - 'lib/mac/**' + - 'lib/raspberry-pi/**' + - 'lib/windows/**' + - 'resources/audio_samples/*.wav' + +defaults: + run: + working-directory: demo/c + +jobs: + build-demo-github-hosted: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + # TODO: uncomment all commented lines +# os: [ubuntu-latest, windows-latest, macos-latest] + os: [ubuntu-latest] + include: + - os: ubuntu-latest + platform: linux + arch: x86_64 + make_file: "Unix Makefiles" +# - os: windows-latest +# platform: windows +# arch: amd64 +# make_file: "MinGW Makefiles" +# - os: macos-latest +# platform: mac +# arch: x86_64 +# make_file: "Unix Makefiles" + + steps: + - uses: actions/checkout@v3 + with: + submodules: recursive + + - name: Set up Python '3.10' + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Create build directory + run: cmake -G "${{ matrix.make_file }}" -B ./build + + - name: Build demo + run: cmake --build ./build --target falcon_demo + + - name: Install dependencies + run: pip install -r test/requirements.txt + + - name: Test + run: python test/test_falcon_c.py "${{secrets.PV_VALID_ACCESS_KEY}}" ${{ matrix.platform }} ${{ matrix.arch }} + +# build-demo-self-hosted: +# runs-on: ${{ matrix.machine }} +# +# strategy: +# matrix: +# machine: [rpi3-32, rpi3-64, rpi4-32, rpi4-64, jetson] +# include: +# - machine: rpi3-32 +# platform: raspberry-pi +# arch: cortex-a53 +# - machine: rpi3-64 +# platform: raspberry-pi +# arch: cortex-a53-aarch64 +# - machine: rpi4-32 +# platform: raspberry-pi +# arch: cortex-a72 +# - machine: rpi4-64 +# platform: raspberry-pi +# arch: cortex-a72-aarch64 +# - machine: jetson +# platform: jetson +# arch: cortex-a57-aarch64 +# +# steps: +# - uses: actions/checkout@v3 +# with: +# submodules: recursive +# +# - name: Create build directory +# run: cmake -B ./build +# +# - name: Build demo +# run: cmake --build ./build --target falcon_demo +# +# - name: Install dependencies +# run: pip install -r test/requirements.txt +# +# - name: Test +# run: python3 test/test_falcon_c.py ${{secrets.PV_VALID_ACCESS_KEY}} ${{ matrix.platform }} ${{ matrix.arch }} \ No newline at end of file diff --git a/.github/workflows/link-check.yml b/.github/workflows/link-check.yml new file mode 100644 index 0000000..ffb82e8 --- /dev/null +++ b/.github/workflows/link-check.yml @@ -0,0 +1,18 @@ +name: Check Markdown links + +on: + workflow_dispatch: + push: + branches: [main] + pull_request: + branches: [main, 'v[0-9]+.[0-9]+'] + +jobs: + markdown-link-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: gaurav-nelson/github-action-markdown-link-check@1.0.14 + with: + use-quiet-mode: 'yes' + use-verbose-mode: 'yes' diff --git a/.github/workflows/python-codestyle.yml b/.github/workflows/python-codestyle.yml new file mode 100644 index 0000000..8aa9848 --- /dev/null +++ b/.github/workflows/python-codestyle.yml @@ -0,0 +1,30 @@ +name: Python Codestyle + +on: + workflow_dispatch: + push: + branches: [main] + paths: + - '**/*.py' + pull_request: + branches: [main, 'v[0-9]+.[0-9]+'] + paths: + - '**/*.py' + +jobs: + check-python-codestyle: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Install dependencies + run: pip install flake8 pep8-naming + + - name: Check python codestyle + run: flake8 --ignore=F401,F403,F405 --max-line-length=120 . \ No newline at end of file diff --git a/.github/workflows/python-demos.yml b/.github/workflows/python-demos.yml new file mode 100644 index 0000000..95383a0 --- /dev/null +++ b/.github/workflows/python-demos.yml @@ -0,0 +1,64 @@ +name: Python Demos + +on: + workflow_dispatch: + push: + branches: [ master ] + paths: + - '.github/workflows/python-demos.yml' + - 'demo/python/**' + - '!demo/python/README.md' + pull_request: + branches: [ master, 'v[0-9]+.[0-9]+' ] + paths: + - '.github/workflows/python-demos.yml' + - 'demo/python/**' + - '!demo/python/README.md' + +defaults: + run: + working-directory: demo/python + +jobs: + build-github-hosted: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + # TODO: uncomment all commented lines +# os: [ubuntu-latest, windows-latest, macos-latest] + os: [ubuntu-latest] + python-version: ['3.7', '3.8', '3.9', '3.10'] + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Pre-build dependencies + run: python -m pip install --upgrade pip + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Test + run: python falcon_demo_file.py --access_key ${{secrets.PV_VALID_ACCESS_KEY}} --wav_paths ../../resources/audio_samples/test.wav + +# build-self-hosted: +# runs-on: ${{ matrix.machine }} +# +# strategy: +# matrix: +# machine: [rpi3-32, rpi3-64, rpi4-32, rpi4-64, jetson] +# +# steps: +# - uses: actions/checkout@v3 +# +# - name: Install dependencies +# run: pip3 install -r requirements.txt +# +# - name: Test +# run: python3 falcon_demo_file.py --access_key ${{secrets.PV_VALID_ACCESS_KEY}} --wav_paths ../../resources/audio_samples/test.wav \ No newline at end of file diff --git a/.github/workflows/python-perf.yml b/.github/workflows/python-perf.yml new file mode 100644 index 0000000..2d5bd13 --- /dev/null +++ b/.github/workflows/python-perf.yml @@ -0,0 +1,120 @@ +name: Python Performance + +on: + workflow_dispatch: + push: + branches: [ main ] + paths: + - '.github/workflows/python-perf.yml' + - 'binding/python/test_falcon_perf.py' + - 'lib/common/**' + - 'lib/linux/**' + - 'lib/jetson/**' + - 'lib/mac/**' + - 'lib/raspberry-pi/**' + - 'lib/windows/**' + pull_request: + branches: [ main, 'v[0-9]+.[0-9]+' ] + paths: + - '.github/workflows/python-perf.yml' + - 'binding/python/test_falcon_perf.py' + - 'lib/common/**' + - 'lib/linux/**' + - 'lib/jetson/**' + - 'lib/mac/**' + - 'lib/raspberry-pi/**' + - 'lib/windows/**' + +defaults: + run: + working-directory: binding/python + +jobs: + perf-github-hosted: + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + # TODO: uncomment all commented lines +# os: [ubuntu-latest, windows-latest, macos-latest] + os: [ubuntu-latest] + include: + - os: ubuntu-latest + performance_threshold_sec: 1.2 +# - os: windows-latest +# performance_threshold_sec: 0.0 +# - os: macos-latest +# performance_threshold_sec: 0.0 + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: '3.10' + + - name: Pre-build dependencies + run: python -m pip install --upgrade pip + + - name: Build dependencies + run: | + pip install wheel && python3 setup.py sdist bdist_wheel && pip install pip install -U dist/*.whl + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Test + run: > + python3 test_falcon_perf.py + --access-key ${{secrets.PV_VALID_ACCESS_KEY}} + --num-test-iterations 20 + --performance-threshold-sec ${{matrix.performance_threshold_sec}} + +# perf-self-hosted: +# runs-on: ${{ matrix.machine }} +# +# strategy: +# fail-fast: false +# matrix: +# machine: [rpi3-32, rpi3-64, rpi4-32, rpi4-64, jetson] +# include: +# - machine: rpi3-32 +# performance_threshold_sec: 0.0 +# - machine: rpi3-64 +# performance_threshold_sec: 0.0 +# - machine: rpi4-32 +# performance_threshold_sec: 0.0 +# - machine: rpi4-64 +# performance_threshold_sec: 0.0 +# - machine: jetson +# performance_threshold_sec: 0.0 +# +# steps: +# - uses: actions/checkout@v3 +# +# - name: Machine state before +# working-directory: resources/scripts +# run: bash machine-state.sh +# +# - name: Pre-build dependencies +# run: python3 -m pip install --upgrade pip +# +# - name: Build dependencies +# run: | +# pip install wheel && python3 setup.py sdist bdist_wheel && pip install pip install -U dist/*.whl +# +# - name: Install dependencies +# run: pip install -r requirements.txt +# +# - name: Test +# run: > +# python3 test_falcon_perf.py +# --access-key ${{secrets.PV_VALID_ACCESS_KEY}} +# --num-test-iterations 20 +# --performance_threshold_sec ${{matrix.performance_threshold_sec}} +# +# - name: Machine state after +# working-directory: resources/scripts +# run: bash machine-state.sh \ No newline at end of file diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 0000000..14f8073 --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,81 @@ +name: Python + +on: + workflow_dispatch: + push: + branches: [main] + paths: + - '.github/workflows/python.yml' + - 'binding/python/**' + - '!binding/python/README.md' + - 'lib/common/**' + - 'lib/jetson/**' + - 'lib/linux/**' + - 'lib/mac/**' + - 'lib/raspberry-pi/**' + - 'lib/windows/**' + - 'resources/audio_samples/*.wav' + pull_request: + branches: [main, 'v[0-9]+.[0-9]+'] + paths: + - '.github/workflows/python.yml' + - 'binding/python/**' + - '!binding/python/README.md' + - 'lib/common/**' + - 'lib/jetson/**' + - 'lib/linux/**' + - 'lib/mac/**' + - 'lib/raspberry-pi/**' + - 'lib/windows/**' + - 'resources/audio_samples/*.wav' + +defaults: + run: + working-directory: binding/python + +jobs: + build-github-hosted: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + # TODO: uncomment all commented lines +# os: [ ubuntu-latest, windows-latest, macos-latest ] + os: [ ubuntu-latest ] + python-version: ['3.7', '3.8', '3.9', '3.10'] + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Pre-build dependencies + run: python -m pip install --upgrade pip + + - name: Install dependencies + run: pip install -r requirements.txt + + - name: Test + run: python test_falcon.py ${{secrets.PV_VALID_ACCESS_KEY}} + +# build-self-hosted: +# runs-on: ${{ matrix.machine }} +# +# strategy: +# matrix: +# machine: [rpi3-32, rpi3-64, rpi4-32, rpi4-64, jetson] +# +# steps: +# - uses: actions/checkout@v3 +# +# - name: Pre-build dependencies +# run: python3 -m pip install --upgrade pip +# +# - name: Install dependencies +# run: pip install -r requirements.txt +# +# - name: Test +# run: python3 test_falcon.py --access-key ${{secrets.PV_VALID_ACCESS_KEY}} diff --git a/.github/workflows/spell-check.yml b/.github/workflows/spell-check.yml new file mode 100644 index 0000000..00970d7 --- /dev/null +++ b/.github/workflows/spell-check.yml @@ -0,0 +1,25 @@ +name: SpellCheck + +on: + workflow_dispatch: + push: + branches: [main] + pull_request: + branches: [main, 'v[0-9]+.[0-9]+'] + +jobs: + spellcheck: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: Install CSpell + run: npm install -g cspell + + - name: Run CSpell + run: cspell --config resources/.lint/spell-check/.cspell.json "**/*" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3d72576 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +.idea \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..6fecd5f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "demo/c/pvrecorder"] + path = demo/c/pvrecorder + url = ../../Picovoice/pvrecorder.git +[submodule "demo/c/dr_libs"] + path = demo/c/dr_libs + url = ../../mackron/dr_libs.git diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5d1a904 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 Picovoice Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + 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. diff --git a/README.md b/README.md index e69de29..d58d4f6 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,164 @@ +# Falcon + +[![GitHub release](https://img.shields.io/github/v/tag/Picovoice/falcon.svg)](https://github.com/Picovoice/falcon/releases) +[![GitHub](https://img.shields.io/github/license/Picovoice/falcon)](https://github.com/Picovoice/falcon/) + +[![PyPI](https://img.shields.io/pypi/v/pvfalcon)](https://pypi.org/project/pvfalcon/) + + +Made in Vancouver, Canada by [Picovoice](https://picovoice.ai) + + +[![Twitter URL](https://img.shields.io/twitter/url?label=%40AiPicovoice&style=social&url=https%3A%2F%2Ftwitter.com%2FAiPicovoice)](https://twitter.com/AiPicovoice) + +[![YouTube Channel Views](https://img.shields.io/youtube/channel/views/UCAdi9sTCXLosG1XeqDwLx7w?label=YouTube&style=social)](https://www.youtube.com/channel/UCAdi9sTCXLosG1XeqDwLx7w) + +Falcon is an on-device speaker diarization engine. Falcon is: + +- Private; All voice processing runs locally. +- Cross-Platform: + - Linux (x86_64), macOS (x86_64, arm64), Windows (x86_64) + - Raspberry Pi (4, 3) and NVIDIA Jetson Nano + + +## Table of Contents + +- [Falcon](#falcon) + - [Table of Contents](#table-of-contents) + - [AccessKey](#accesskey) + - [Demos](#demos) + - [Python Demos](#python-demos) + - [C Demos](#c-demos) + - [SDKs](#sdks) + - [Python](#python) + - [C](#c) + - [Releases](#releases) + - [FAQ](#faq) + +## AccessKey + +AccessKey is your authentication and authorization token for deploying Picovoice SDKs, including Falcon. Anyone who is +using Picovoice needs to have a valid AccessKey. You must keep your AccessKey secret. You would need internet +connectivity to validate your AccessKey with Picovoice license servers even though the speaker recognition is running +100% offline. + +AccessKey also verifies that your usage is within the limits of your account. Everyone who signs up for +[Picovoice Console](https://console.picovoice.ai/) receives the `Free Tier` usage rights described +[here](https://picovoice.ai/pricing/). If you wish to increase your limits, you can purchase a subscription plan. + +## Demos + +### Python Demos + +Install the demo package: + +```console +pip3 install pvfalcondemo +``` + +Run the following in the terminal: + +```console +falcon_demo_file --access_key ${ACCESS_KEY} --audio_paths ${AUDIO_PATH} +``` + +Replace `${ACCESS_KEY}` with yours obtained from Picovoice Console. + +For more information about Python demos go to [demo/python](./demo/python). + +### C Demos + +Build the demo: + +```console +cmake -S demo/c/ -B demo/c/build && cmake --build demo/c/build +``` + +Run the demo: + +```console +./demo/c/build/falcon_demo -a ${ACCESS_KEY} -l ${LIBRARY_PATH} -m ${MODEL_PATH} ${AUDIO_PATH} +``` + +## SDKs + +### Python + +Install the Python SDK: + +```console +pip3 install pvfalcon +``` + +Create an instance of the engine and perform speaker diarization on an audio file: + +```python +import pvfalcon + +falcon = pvfalcon.create(access_key='${ACCESS_KEY}') + +print(falcon.process_file('${AUDIO_PATH}')) +``` + +Replace `${ACCESS_KEY}` with yours obtained from [Picovoice Console](https://console.picovoice.ai/) and +`${AUDIO_PATH}` to path an audio file. + +Finally, when done be sure to explicitly release the resources: +```python +falcon.delete() +``` + +### C + +Create an instance of the engine and perform speaker diarization on an audio file: + +```c +#include +#include +#include + +#include "pv_falcon.h" + +pv_falcon_t *falcon = NULL; +pv_status_t status = pv_falcon_init("${ACCESS_KEY}", "${MODEL_PATH}", &falcon); +if (status != PV_STATUS_SUCCESS) { + // error handling logic +} + +int32_t num_segments = 0; +pv_segment_t *segments = NULL; +status = pv_falcon_process_file(falcon, "${AUDIO_PATH}", &num_segments, &segments); +if (status != PV_STATUS_SUCCESS) { + // error handling logic +} + +for (int32_t i = 0; i < num_segments; i++) { + pv_segment_t *segment = &segments[i]; + fprintf( + stdout, + "Speaker: %d -> Start: %5.2f, End: %5.2f\n", + segment->speaker_tag, + segment->start_sec, + segment->end_sec); +} + +pv_falcon_segments_delete(segments); +``` + +Replace `${ACCESS_KEY}` with yours obtained from Picovoice Console, `${MODEL_PATH}` to path to +[default model file](./lib/common/falcon_params.pv) (or your custom one), and `${AUDIO_PATH}` to path an audio file. + +Finally, when done be sure to release resources acquired: +```c +pv_falcon_delete(falcon); +``` + +## Releases + +### v1.0.0 - November xth, 2023 + +- Initial release. + +## FAQ + +You can find the FAQ [here](https://picovoice.ai/docs/faq/picovoice/). diff --git a/binding/python/.gitignore b/binding/python/.gitignore new file mode 100644 index 0000000..1c6c0bd --- /dev/null +++ b/binding/python/.gitignore @@ -0,0 +1,6 @@ +__pycache__ +build +dist +pvfalcon +pvfalcon.egg-info +MANIFEST.in diff --git a/binding/python/README.md b/binding/python/README.md new file mode 100644 index 0000000..8232695 --- /dev/null +++ b/binding/python/README.md @@ -0,0 +1,51 @@ +# Falcon Speaker Diarization Engine + +Made in Vancouver, Canada by [Picovoice](https://picovoice.ai) + +Falcon is an on-device speaker diarization engine. Falcon is: + +- Private; All voice processing runs locally. +- Cross-Platform: + - Linux (x86_64), macOS (x86_64, arm64), Windows (x86_64) + - Raspberry Pi (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. + +## Installation + +```console +pip3 install pvfalcon +``` + +## AccessKey + +Falcon requires a valid Picovoice `AccessKey` at initialization. `AccessKey` acts as your credentials when using Falcon SDKs. +You can get your `AccessKey` for free. Make sure to keep your `AccessKey` secret. +Signup or Login to [Picovoice Console](https://console.picovoice.ai/) to get your `AccessKey`. + +### Usage + +Create an instance of the engine and diarize an audio file: + +```python +import pvfalcon + +handle = pvfalcon.create(access_key='${ACCESS_KEY}') + +segments = handle.process_file('${AUDIO_PATH}') +for segment in segments: + print("{speaker tag=%d - start_sec=%.2f end_sec=%.2f}" + % (segment.speaker_tag, segment.start_sec, segment.end_sec)) +``` + +Replace `${ACCESS_KEY}` with yours obtained from [Picovoice Console](https://console.picovoice.ai/) and +`${AUDIO_PATH}` to the path an audio file. Finally, when done be sure to explicitly release the resources using +`handle.delete()`. + +## Demos + +[pvfalcondemo](https://pypi.org/project/pvfalcondemo/) provides command-line utilities for processing audio using +Falcon. diff --git a/binding/python/__init__.py b/binding/python/__init__.py new file mode 100644 index 0000000..3393bff --- /dev/null +++ b/binding/python/__init__.py @@ -0,0 +1,14 @@ +# +# Copyright 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 ._factory import * +from ._falcon import * +from ._util import * diff --git a/binding/python/_factory.py b/binding/python/_factory.py new file mode 100644 index 0000000..30b1b37 --- /dev/null +++ b/binding/python/_factory.py @@ -0,0 +1,40 @@ +# +# Copyright 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 * + +from ._falcon import Falcon +from ._util import default_library_path, default_model_path + + +def create(access_key: str, model_path: Optional[str] = None, library_path: Optional[str] = None) -> Falcon: + """ + Factory method for Falcon speaker diarization engine. + + :param access_key: AccessKey obtained from Picovoice Console (https://console.picovoice.ai/) + :param library_path: Absolute path to Falcon's dynamic library. If not set it will be set to the default location. + :param model_path: Absolute path to the file containing model parameters. If not set it will be set to the default + location. + :return: An instance of Falcon diarization engine. + """ + + if model_path is None: + model_path = default_model_path("") + + if library_path is None: + library_path = default_library_path("") + + return Falcon(access_key=access_key, model_path=model_path, library_path=library_path) + + +__all__ = [ + "create", +] diff --git a/binding/python/_falcon.py b/binding/python/_falcon.py new file mode 100644 index 0000000..851365d --- /dev/null +++ b/binding/python/_falcon.py @@ -0,0 +1,331 @@ +import os +import pathlib +from collections import namedtuple +from ctypes import * +from enum import Enum +from typing import * + + +class FalconError(Exception): + 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 FalconMemoryError(FalconError): + pass + + +class FalconIOError(FalconError): + pass + + +class FalconInvalidArgumentError(FalconError): + pass + + +class FalconStopIterationError(FalconError): + pass + + +class FalconKeyError(FalconError): + pass + + +class FalconInvalidStateError(FalconError): + pass + + +class FalconRuntimeError(FalconError): + pass + + +class FalconActivationError(FalconError): + pass + + +class FalconActivationLimitError(FalconError): + pass + + +class FalconActivationThrottledError(FalconError): + pass + + +class FalconActivationRefusedError(FalconError): + pass + + +class Falcon(object): + """ + Python binding for Falcon Speaker Diarization engine. + """ + + class PicovoiceStatuses(Enum): + SUCCESS = 0 + OUT_OF_MEMORY = 1 + IO_ERROR = 2 + INVALID_ARGUMENT = 3 + STOP_ITERATION = 4 + KEY_ERROR = 5 + INVALID_STATE = 6 + RUNTIME_ERROR = 7 + ACTIVATION_ERROR = 8 + ACTIVATION_LIMIT_REACHED = 9 + ACTIVATION_THROTTLED = 10 + ACTIVATION_REFUSED = 11 + + _PICOVOICE_STATUS_TO_EXCEPTION = { + PicovoiceStatuses.OUT_OF_MEMORY: FalconMemoryError, + PicovoiceStatuses.IO_ERROR: FalconIOError, + PicovoiceStatuses.INVALID_ARGUMENT: FalconInvalidArgumentError, + PicovoiceStatuses.STOP_ITERATION: FalconStopIterationError, + PicovoiceStatuses.KEY_ERROR: FalconKeyError, + PicovoiceStatuses.INVALID_STATE: FalconInvalidStateError, + PicovoiceStatuses.RUNTIME_ERROR: FalconRuntimeError, + PicovoiceStatuses.ACTIVATION_ERROR: FalconActivationError, + PicovoiceStatuses.ACTIVATION_LIMIT_REACHED: FalconActivationLimitError, + PicovoiceStatuses.ACTIVATION_THROTTLED: FalconActivationThrottledError, + PicovoiceStatuses.ACTIVATION_REFUSED: FalconActivationRefusedError, + } + + _VALID_EXTENSIONS = ( + "3gp", + "flac", + "m4a", + "mp3", + "mp4", + "ogg", + "opus", + "vorbis", + "wav", + "webm", + ) + + class CFalcon(Structure): + pass + + class CSegment(Structure): + """ + Represents a segment with its start, end, and associated speaker tag. + """ + _fields_ = [("start_sec", c_float), ("end_sec", c_float), ("speaker_tag", c_int32)] + + def __init__(self, access_key: str, model_path: str, library_path: str) -> None: + """ + Constructor. + + :param access_key: AccessKey obtained from Picovoice Console (https://console.picovoice.ai/) + :param model_path: Absolute path to the file containing model parameters. + :param library_path: Absolute path to Falcon's dynamic library. + """ + + if not isinstance(access_key, str) or len(access_key) == 0: + raise FalconInvalidArgumentError("`access_key` should be a non-empty string.") + + if not os.path.exists(model_path): + raise FalconIOError("Could not find model file at `%s`." % model_path) + + if not os.path.exists(library_path): + raise FalconIOError("Could not find Falcon's 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_falcon_init + init_func.argtypes = [c_char_p, c_char_p, POINTER(POINTER(self.CFalcon))] + init_func.restype = self.PicovoiceStatuses + + self._handle = POINTER(self.CFalcon)() + + status = init_func(access_key.encode(), model_path.encode(), byref(self._handle)) + if status is not self.PicovoiceStatuses.SUCCESS: + raise self._PICOVOICE_STATUS_TO_EXCEPTION[status]( + message="Initialization failed", message_stack=self._get_error_stack() + ) + + self._delete_func = library.pv_falcon_delete + self._delete_func.argtypes = [POINTER(self.CFalcon)] + self._delete_func.restype = None + + self._process_func = library.pv_falcon_process + self._process_func.argtypes = [ + POINTER(self.CFalcon), + POINTER(c_short), + c_int32, + POINTER(c_int32), + POINTER(POINTER(self.CSegment)), + ] + self._process_func.restype = self.PicovoiceStatuses + + self._process_file_func = library.pv_falcon_process_file + self._process_file_func.argtypes = [ + POINTER(self.CFalcon), + c_char_p, + POINTER(c_int32), + POINTER(POINTER(self.CSegment)), + ] + self._process_file_func.restype = self.PicovoiceStatuses + + version_func = library.pv_falcon_version + version_func.argtypes = [] + version_func.restype = c_char_p + self._version = version_func().decode("utf-8") + + self._sample_rate = library.pv_sample_rate() + + self._segments_delete_func = library.pv_falcon_segments_delete + self._segments_delete_func.argtypes = [POINTER(self.CSegment)] + self._segments_delete_func.restype = None + + Segment = namedtuple("Segment", ["start_sec", "end_sec", "speaker_tag"]) + + def process(self, pcm: Sequence[int]) -> Sequence[Segment]: + """ + Processes the given audio data and returns the diarization output. + + :param pcm: Audio data. The audio needs to have a sample rate equal to `.sample_rate` and be 16-bit + linearly-encoded. This function operates on single-channel audio. If you wish to process data in a different + sample rate or format consider using `.process_file`. + :return: Diarization output as a sequence of segments. Each segment is a tuple of + (start_sec, end_sec, speaker_tag). + """ + + if len(pcm) == 0: + raise FalconInvalidArgumentError() + + num_segments = c_int32() + c_segments = POINTER(self.CSegment)() + status = self._process_func( + self._handle, (c_short * len(pcm))(*pcm), len(pcm), byref(num_segments), byref(c_segments) + ) + if status is not self.PicovoiceStatuses.SUCCESS: + raise self._PICOVOICE_STATUS_TO_EXCEPTION[status]( + message="Initialization failed", message_stack=self._get_error_stack() + ) + + segments = list() + for i in range(num_segments.value): + word = self.Segment( + start_sec=c_segments[i].start_sec, end_sec=c_segments[i].end_sec, speaker_tag=c_segments[i].speaker_tag + ) + segments.append(word) + + self._segments_delete_func(c_segments) + + return segments + + def process_file(self, audio_path: str) -> Sequence[Segment]: + """ + Processes the given audio file and returns the diarization output. + + :param audio_path: Absolute path to the audio file. The file needs to have a sample rate equal to or greater + than `.sample_rate`. The supported formats are: `FLAC`, `MP3`, `Ogg`, `Opus`, `Vorbis`, `WAV`, and `WebM`. + :return: Diarization output as a sequence of segments. Each segment is a tuple of + (start_sec, end_sec, speaker_tag). + """ + + if not os.path.exists(audio_path): + raise FalconIOError("Could not find the audio file at `%s`" % audio_path) + + num_segments = c_int32() + c_segments = POINTER(self.CSegment)() + status = self._process_file_func(self._handle, audio_path.encode(), byref(num_segments), byref(c_segments)) + if status is not self.PicovoiceStatuses.SUCCESS: + if status is self.PicovoiceStatuses.INVALID_ARGUMENT: + if not audio_path.lower().endswith(self._VALID_EXTENSIONS): + raise self._PICOVOICE_STATUS_TO_EXCEPTION[status]( + "Specified file with extension '%s' is not supported" % pathlib.Path(audio_path).suffix + ) + raise self._PICOVOICE_STATUS_TO_EXCEPTION[status]() + + segments = list() + for i in range(num_segments.value): + word = self.Segment( + start_sec=c_segments[i].start_sec, end_sec=c_segments[i].end_sec, speaker_tag=c_segments[i].speaker_tag + ) + segments.append(word) + + self._segments_delete_func(c_segments) + + return segments + + def delete(self) -> None: + """Releases resources acquired by Falcon.""" + + self._delete_func(self._handle) + + @property + def version(self) -> str: + """Version.""" + + return self._version + + @property + def sample_rate(self) -> int: + """Audio sample rate accepted by `.process`.""" + + 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__ = [ + "Falcon", + "FalconActivationError", + "FalconActivationLimitError", + "FalconActivationRefusedError", + "FalconActivationThrottledError", + "FalconError", + "FalconIOError", + "FalconInvalidArgumentError", + "FalconInvalidStateError", + "FalconKeyError", + "FalconMemoryError", + "FalconRuntimeError", + "FalconStopIterationError", +] diff --git a/binding/python/_util.py b/binding/python/_util.py new file mode 100644 index 0000000..3ec21e2 --- /dev/null +++ b/binding/python/_util.py @@ -0,0 +1,78 @@ +# +# Copyright 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. +# + +import os +import platform +import subprocess + + +def _is_64bit(): + return "64bit" in platform.architecture()[0] + + +def _linux_machine() -> str: + machine = platform.machine() + if machine == "x86_64": + return machine + elif machine in ["aarch64", "armv7l"]: + arch_info = ("-" + machine) if _is_64bit() else "" + else: + raise NotImplementedError("Unsupported CPU architecture: `%s`" % machine) + + 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() + 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 + 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"} + + +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_falcon.dylib") + elif platform.machine() == "arm64": + return os.path.join(os.path.dirname(__file__), relative, "lib/mac/arm64/libpv_falcon.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_falcon.so") + elif linux_machine in _JETSON_MACHINES: + return os.path.join(os.path.dirname(__file__), relative, "lib/jetson/%s/libpv_falcon.so" % linux_machine) + elif linux_machine in _RASPBERRY_PI_MACHINES: + return os.path.join( + os.path.dirname(__file__), relative, "lib/raspberry-pi/%s/libpv_falcon.so" % linux_machine + ) + elif platform.system() == "Windows": + return os.path.join(os.path.dirname(__file__), relative, "lib", "windows", "amd64", "libpv_falcon.dll") + + raise NotImplementedError("Unsupported platform.") + + +def default_model_path(relative: str = "") -> str: + return os.path.join(os.path.dirname(__file__), relative, "lib", "common", "falcon_params.pv") + + +__all__ = ["default_library_path", "default_model_path"] diff --git a/binding/python/requirements.txt b/binding/python/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/binding/python/setup.py b/binding/python/setup.py new file mode 100644 index 0000000..77dbca2 --- /dev/null +++ b/binding/python/setup.py @@ -0,0 +1,69 @@ +# +# Copyright 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. +# + +import os +import shutil + +import setuptools + +INCLUDE_FILES = ("../../LICENSE", "__init__.py", "_factory.py", "_falcon.py", "_util.py") +INCLUDE_LIBS = ( + "common", + "linux", +) +# TODO: Uncomment the following line to include all libraries +# INCLUDE_LIBS = ('common', 'jetson', 'linux', 'mac', 'raspberry-pi', 'windows') + +os.system("git clean -dfx") + +package_folder = os.path.join(os.path.dirname(__file__), "pvfalcon") +os.mkdir(package_folder) +manifest_in = "" + +for rel_path in INCLUDE_FILES: + shutil.copy(os.path.join(os.path.dirname(__file__), rel_path), package_folder) + manifest_in += "include pvfalcon/%s\n" % os.path.basename(rel_path) + +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) + ) +manifest_in += "recursive-include pvfalcon/lib *\n" + +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: + long_description = f.read() + +setuptools.setup( + name="pvfalcon", + version="1.0.0", + author="Picovoice", + author_email="hello@picovoice.ai", + description="Falcon Speaker Diarization Engine", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/Picovoice/falcon", + packages=["pvfalcon"], + include_package_data=True, + classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Topic :: Multimedia :: Sound/Audio :: Speech", + ], + python_requires=">=3.7", + keywords="Speaker Diarization, Speaker Identification, Voice Identification", +) diff --git a/binding/python/test_falcon.py b/binding/python/test_falcon.py new file mode 100644 index 0000000..b2851ba --- /dev/null +++ b/binding/python/test_falcon.py @@ -0,0 +1,163 @@ +# +# Copyright 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. +# + +import os +import sys +import unittest +from typing import * + +from _falcon import * +from _util import * +from test_util import * + +diarization_tests = load_test_data() + + +class FalconTestCase(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls._access_key = sys.argv[1] + cls._audio_directory = os.path.join(os.path.dirname(__file__), "..", "..", "resources", "audio_samples") + cls._error_threshold = 0.05 + + def _validate_metadata(self, segments: Sequence[Falcon.Segment], audio_length: float): + for i in range(len(segments)): + self.assertGreaterEqual(segments[i].start_sec, 0) + self.assertLessEqual(segments[i].start_sec, segments[i].end_sec) + if i < len(segments) - 1: + self.assertLessEqual(segments[i].end_sec, segments[i + 1].start_sec) + else: + self.assertLessEqual(segments[i].end_sec, audio_length) + self.assertTrue(segments[i].speaker_tag > 0) + + def test_invalid_access_key(self): + with self.assertRaises(FalconInvalidArgumentError): + Falcon( + access_key="invalid", + model_path=default_model_path("../../"), + library_path=default_library_path("../../"), + ) + + def test_invalid_model_path(self): + with self.assertRaises(FalconIOError): + Falcon(access_key=self._access_key, model_path="invalid", library_path=default_library_path("../../")) + + def test_invalid_library_path(self): + with self.assertRaises(FalconIOError): + Falcon(access_key=self._access_key, model_path=default_model_path("../../"), library_path="invalid") + + def test_version(self): + o = Falcon( + access_key=self._access_key, + model_path=default_model_path("../../"), + library_path=default_library_path("../../"), + ) + self.assertIsInstance(o.version, str) + self.assertGreater(len(o.version), 0) + + def test_falcon_process(self): + o = None + + try: + o = Falcon( + access_key=self._access_key, + model_path=default_model_path("../../"), + library_path=default_library_path("../../"), + ) + + pcm = read_wav_file(file_name=os.path.join(self._audio_directory, "test.wav"), sample_rate=o.sample_rate) + + segments = o.process(pcm) + self._validate_metadata(segments, len(pcm) / o.sample_rate) + error = calculate_error(segments, diarization_tests[0][1]) + self.assertLess(error, self._error_threshold) + finally: + if o is not None: + o.delete() + + def test_falcon_process_file(self): + o = None + + try: + o = Falcon( + access_key=self._access_key, + model_path=default_model_path("../../"), + library_path=default_library_path("../../"), + ) + + pcm = read_wav_file(file_name=os.path.join(self._audio_directory, "test.wav"), sample_rate=o.sample_rate) + + segments = o.process_file(audio_path=os.path.join(self._audio_directory, "test.wav")) + self._validate_metadata(segments, len(pcm) / o.sample_rate) + error = calculate_error(segments, diarization_tests[0][1]) + self.assertLess(error, self._error_threshold) + finally: + if o is not None: + o.delete() + + def test_message_stack(self): + relative_path = "../.." + + error = None + try: + f = Falcon( + access_key="invalid", + model_path=default_model_path(relative_path), + library_path=default_library_path(relative_path), + ) + self.assertIsNone(f) + except FalconError as e: + error = e.message_stack + + self.assertIsNotNone(error) + self.assertGreater(len(error), 0) + + try: + f = Falcon( + access_key="invalid", + model_path=default_model_path(relative_path), + library_path=default_library_path(relative_path), + ) + self.assertIsNone(f) + except FalconError as e: + self.assertEqual(len(error), len(e.message_stack)) + self.assertListEqual(list(error), list(e.message_stack)) + + def test_process_message_stack(self): + relative_path = "../.." + + f = Falcon( + access_key=self._access_key, + model_path=default_model_path(relative_path), + library_path=default_library_path(relative_path), + ) + + pcm = read_wav_file(file_name=os.path.join(self._audio_directory, "test.wav"), sample_rate=f.sample_rate) + + address = f._handle + f._handle = None + try: + res = f.process(pcm) + self.assertEqual(res, 100) + except FalconError as e: + self.assertGreater(len(e.message_stack), 0) + self.assertLess(len(e.message_stack), 8) + finally: + f._handle = address + f.delete() + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("usage: %s ${ACCESS_KEY}" % sys.argv[0]) + exit(1) + + unittest.main(argv=sys.argv[:1]) diff --git a/binding/python/test_falcon_perf.py b/binding/python/test_falcon_perf.py new file mode 100644 index 0000000..91e1a87 --- /dev/null +++ b/binding/python/test_falcon_perf.py @@ -0,0 +1,63 @@ +# +# Copyright 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. +# + +import argparse +import os +import sys +import unittest +from time import perf_counter + +from _falcon import Falcon +from _util import default_library_path, default_model_path + + +class FalconPerformanceTestCase(unittest.TestCase): + TEST_PATH = os.path.join(os.path.dirname(__file__), "../../resources/audio_samples/test.wav") + + access_key: str + num_test_iterations: int + performance_threshold_sec: float + + def test_performance_proc(self): + falcon = Falcon( + access_key=self.access_key, + library_path=default_library_path("../.."), + model_path=default_model_path("../.."), + ) + + perf_results = list() + for i in range(self.num_test_iterations): + start = perf_counter() + falcon.process_file(self.TEST_PATH) + proc_time = perf_counter() - start + + if i > 0: + perf_results.append(proc_time) + + falcon.delete() + + avg_perf = sum(perf_results) / self.num_test_iterations + print("Average proc performance: %s" % avg_perf) + self.assertLess(avg_perf, self.performance_threshold_sec) + + +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("--performance-threshold-sec", type=float, required=True) + args = parser.parse_args() + + FalconPerformanceTestCase.access_key = args.access_key + FalconPerformanceTestCase.num_test_iterations = args.num_test_iterations + FalconPerformanceTestCase.performance_threshold_sec = args.performance_threshold_sec + + unittest.main(argv=sys.argv[:1]) diff --git a/binding/python/test_util.py b/binding/python/test_util.py new file mode 100644 index 0000000..9fe40ec --- /dev/null +++ b/binding/python/test_util.py @@ -0,0 +1,84 @@ +# +# Copyright 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. +# + +import json +import os +import struct +import wave +from typing import * + +from _falcon import * + + +def load_test_data() -> List[Tuple[str, List[Falcon.Segment]]]: + data_file_path = os.path.join(os.path.dirname(__file__), "../../resources/.test/test_data.json") + with open(data_file_path, encoding="utf8") as data_file: + json_test_data = data_file.read() + test_data = json.loads(json_test_data)['tests'] + + diarization_tests = [ + ( + t['audio_file'], + [ + Falcon.Segment( + start_sec=x['start_sec'], + end_sec=x['end_sec'], + speaker_tag=x['speaker_tag']) + for x in t['segments'] + ] + ) + for t in test_data['diarization_tests'] + ] + + return diarization_tests + + +def read_wav_file(file_name: str, sample_rate: int) -> Tuple: + wav_file = wave.open(file_name, mode="rb") + channels = wav_file.getnchannels() + num_frames = wav_file.getnframes() + + if wav_file.getframerate() != sample_rate: + raise ValueError("Audio file should have a sample rate of %d, got %d" % (sample_rate, wav_file.getframerate())) + + samples = wav_file.readframes(num_frames) + wav_file.close() + + frames = struct.unpack("h" * num_frames * channels, samples) + + if channels == 2: + print("Picovoice processes single-channel audio but stereo file is provided. Processing left channel only.") + + return frames[::channels] + + +def calculate_error(segments: Sequence[Falcon.Segment], expected_segments: Sequence[Falcon.Segment]) -> float: + error_sec = 0.0 + expected_segment_idx = 0 + for segment in segments: + expected_segment = expected_segments[expected_segment_idx] + if segment.speaker_tag != expected_segment.speaker_tag: + error_sec += abs(segment.end_sec - segment.start_sec) + else: + error_sec += abs(expected_segment.end_sec - segment.end_sec) + error_sec += abs(expected_segment.start_sec - segment.start_sec) + expected_segment_idx += 1 + + length_sec = expected_segments[-1].end_sec + + return error_sec / length_sec + + +__all__ = [ + 'load_test_data', + 'read_wav_file', + 'calculate_error' +] diff --git a/demo/c/.gitignore b/demo/c/.gitignore new file mode 100644 index 0000000..a9c3deb --- /dev/null +++ b/demo/c/.gitignore @@ -0,0 +1,3 @@ +cmake-build-debug +build +__pycache__ \ No newline at end of file diff --git a/demo/c/CMakeLists.txt b/demo/c/CMakeLists.txt new file mode 100644 index 0000000..d617b7b --- /dev/null +++ b/demo/c/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.13) +project(falcon_demo) + +set(CMAKE_C_STANDARD 99) +set(CMAKE_BUILD_TYPE Release) + +include_directories("${PROJECT_SOURCE_DIR}/../../include") + +add_executable(falcon_demo falcon_demo.c) + +if (NOT WIN32) + target_link_libraries(falcon_demo dl) +endif() diff --git a/demo/c/README.md b/demo/c/README.md new file mode 100644 index 0000000..b6c88b0 --- /dev/null +++ b/demo/c/README.md @@ -0,0 +1,47 @@ +# C Demo + +## Compatibility + +- C99-compatible compiler + +## Requirements + +- [CMake](https://cmake.org/) version 3.13 or higher +- [MinGW](https://mingw-w64.org/) (**Windows Only**) + +## AccessKey + +Falcon requires a valid Picovoice `AccessKey` at initialization. `AccessKey` acts as your credentials when using Falcon SDKs. +You can get your `AccessKey` for free. Make sure to keep your `AccessKey` secret. +Signup or Login to [Picovoice Console](https://console.picovoice.ai/) to get your `AccessKey`. + +## Usage + +### Build Linux/MacOS + +Build the demo by running this from the root of the repository: + +```console +cmake -S demo/c/ -B demo/c/build +cmake --build demo/c/build +``` + +### Build Windows + +Build the demo by running this from the root of the repository: + +```console +cmake -S demo/c/ -B demo/c/build -G "MinGW Makefiles" +cmake --build demo/c/build +``` + +### Run + +Running the demo without arguments prints the usage: + +```console +usage: -a ACCESS_KEY -l LIBRARY_PATH -m MODEL_PATH [-d] [-v] audio_path0 audio_path1 ... +``` + +Run the command corresponding to your platform from the root of the repository. Replace `${ACCESS_KEY}` with yours +obtained from [Picovoice Console](https://console.picovoice.ai/) and `${AUDIO_PATH}` with the path to an audio file. diff --git a/demo/c/falcon_demo.c b/demo/c/falcon_demo.c new file mode 100644 index 0000000..d401e97 --- /dev/null +++ b/demo/c/falcon_demo.c @@ -0,0 +1,316 @@ +/* + Copyright 2019-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. +*/ + +#if !(defined(_WIN32) || defined(_WIN64)) + +#include + +#endif + +#include +#include +#include +#include +#include + +#if defined(_WIN32) || defined(_WIN64) + +#include + +#endif + +#include "pv_falcon.h" + +static void *open_dl(const char *dl_path) { + +#if defined(_WIN32) || defined(_WIN64) + + return LoadLibrary(dl_path); + +#else + + return dlopen(dl_path, RTLD_NOW); + +#endif +} + +static void *load_symbol(void *handle, const char *symbol) { + +#if defined(_WIN32) || defined(_WIN64) + + return GetProcAddress((HMODULE) handle, symbol); + +#else + + return dlsym(handle, symbol); + +#endif +} + +static void close_dl(void *handle) { + +#if defined(_WIN32) || defined(_WIN64) + + FreeLibrary((HMODULE) handle); + +#else + + dlclose(handle); + +#endif +} + +static void print_dl_error(const char *message) { + +#if defined(_WIN32) || defined(_WIN64) + + fprintf(stderr, "%s with code `%lu`.\n", message, GetLastError()); + +#else + + fprintf(stderr, "%s with `%s`.\n", message, dlerror()); + +#endif +} + +static void print_error_message(char **message_stack, int32_t message_stack_depth) { + for (int32_t i = 0; i < message_stack_depth; i++) { + fprintf(stderr, "\n [%d] %s", i, message_stack[i]); + } +} + +int picovoice_main(int argc, char **argv) { + const char *access_key = NULL; + const char *model_path = NULL; + const char *library_path = NULL; + + int opt; + while ((opt = getopt(argc, argv, "a:m:l:")) != -1) { + switch (opt) { + case 'a': + access_key = optarg; + break; + case 'm': + model_path = optarg; + break; + case 'l': + library_path = optarg; + break; + default: + break; + } + } + + if (!(access_key && library_path && model_path && (optind < argc))) { + fprintf(stderr, "usage: -a ACCESS_KEY -m MODEL_PATH -l LIBRARY_PATH audio_path0 audio_path1 ...\n"); + exit(1); + } + + void *dl_handle = open_dl(library_path); + if (!dl_handle) { + fprintf(stderr, "failed to load library at `%s`.\n", library_path); + exit(1); + } + + const char *(*pv_status_to_string_func)(pv_status_t) = + load_symbol(dl_handle, "pv_status_to_string"); + if (!pv_status_to_string_func) { + print_dl_error("failed to load `pv_status_to_string`"); + exit(1); + } + + const int32_t (*pv_sample_rate_func)() = + load_symbol(dl_handle, "pv_sample_rate"); + if (!pv_sample_rate_func) { + print_dl_error("failed to load `pv_sample_rate`"); + exit(1); + } + + const char *(*pv_falcon_version_func)() = load_symbol(dl_handle, "pv_falcon_version"); + + pv_status_t (*pv_falcon_init_func)(const char *, const char *, pv_falcon_t **) = + load_symbol(dl_handle, "pv_falcon_init"); + if (!pv_falcon_init_func) { + print_dl_error("failed to load `pv_falcon_init`"); + exit(1); + } + + void (*pv_falcon_delete_func)(pv_falcon_t *) = + load_symbol(dl_handle, "pv_falcon_delete"); + if (!pv_falcon_delete_func) { + print_dl_error("failed to load `pv_falcon_delete`"); + exit(1); + } + + pv_status_t (*pv_falcon_process_file_func)( + pv_falcon_t *, + const char *, + int32_t *, + pv_segment_t **) = + load_symbol(dl_handle, "pv_falcon_process_file"); + if (!pv_falcon_process_file_func) { + print_dl_error("failed to load `pv_falcon_process_file`"); + exit(1); + } + + pv_status_t (*pv_falcon_segments_delete_func)(pv_segment_t *) = + load_symbol(dl_handle, "pv_falcon_segments_delete"); + if (!pv_falcon_segments_delete_func) { + print_dl_error("failed to load `pv_falcon_segments_delete`"); + exit(1); + } + + pv_status_t (*pv_get_error_stack_func)(char ***, int32_t *) = load_symbol(dl_handle, "pv_get_error_stack"); + if (!pv_get_error_stack_func) { + print_dl_error("failed to load 'pv_get_error_stack_func'"); + exit(1); + } + + void (*pv_free_error_stack_func)(char **) = load_symbol(dl_handle, "pv_free_error_stack"); + if (!pv_free_error_stack_func) { + print_dl_error("failed to load 'pv_free_error_stack_func'"); + exit(1); + } + + char **message_stack = NULL; + int32_t message_stack_depth = 0; + pv_status_t error_status = PV_STATUS_RUNTIME_ERROR; + + fprintf(stdout, "Falcon %s\n", pv_falcon_version_func()); + + struct timeval before; + gettimeofday(&before, NULL); + + pv_falcon_t *falcon = NULL; + pv_status_t status = pv_falcon_init_func( + access_key, + model_path, + &falcon); + if (status != PV_STATUS_SUCCESS) { + fprintf(stderr, "failed to init with `%s`.\n", pv_status_to_string_func(status)); + error_status = pv_get_error_stack_func(&message_stack, &message_stack_depth); + + if (error_status != PV_STATUS_SUCCESS) { + fprintf(stderr, ".\nUnable to get Falcon error state with '%s'\n", pv_status_to_string_func(error_status)); + exit(1); + } + + if (message_stack_depth > 0) { + fprintf(stderr, ":\n"); + print_error_message(message_stack, message_stack_depth); + } + + pv_free_error_stack_func(message_stack); + exit(1); + } + + struct timeval after; + gettimeofday(&after, NULL); + + double init_sec = ((double) (after.tv_sec - before.tv_sec) + + ((double) (after.tv_usec - before.tv_usec)) * 1e-6); + fprintf(stdout, "init took %.1f sec\n", init_sec); + + double proc_sec = 0.; + + for (int32_t i = optind; i < argc; i++) { + gettimeofday(&before, NULL); + + int32_t num_segments = 0; + pv_segment_t *segments = NULL; + status = pv_falcon_process_file_func(falcon, argv[i], &num_segments, &segments); + if (status != PV_STATUS_SUCCESS) { + fprintf(stderr, "'pv_falcon_process' failed with '%s'", pv_status_to_string_func(status)); + error_status = pv_get_error_stack_func(&message_stack, &message_stack_depth); + + if (error_status != PV_STATUS_SUCCESS) { + fprintf(stderr, ".\nUnable to get Falcon error state with '%s'\n", pv_status_to_string_func(error_status)); + exit(1); + } + + if (message_stack_depth > 0) { + fprintf(stderr, ":\n"); + print_error_message(message_stack, message_stack_depth); + } + + pv_free_error_stack_func(message_stack); + exit(1); + } + + gettimeofday(&after, NULL); + + proc_sec += ((double) (after.tv_sec - before.tv_sec) + + ((double) (after.tv_usec - before.tv_usec)) * 1e-6); + + for (int32_t j = 0; j < num_segments; j++) { + pv_segment_t *segment = &segments[j]; + fprintf(stdout, + "Speaker: %d -> Start: %5.2f, End: %5.2f\n", + segment->speaker_tag, + segment->start_sec, + segment->end_sec); + } + pv_falcon_segments_delete_func(segments); + } + + fprintf(stdout, "proc took %.2f sec\n", proc_sec); + + close_dl(dl_handle); + + return 0; +} + +int main(int argc, char *argv[]) { + +#if defined(_WIN32) || defined(_WIN64) + +#define UTF8_COMPOSITION_FLAG (0) +#define NULL_TERMINATED (-1) + + LPWSTR *wargv = CommandLineToArgvW(GetCommandLineW(), &argc); + if (wargv == NULL) { + fprintf(stderr, "CommandLineToArgvW failed\n"); + exit(1); + } + + char *utf8_argv[argc]; + + for (int i = 0; i < argc; ++i) { + // WideCharToMultiByte: + // https://docs.microsoft.com/en-us/windows/win32/api/stringapiset/nf-stringapiset-widechartomultibyte + int arg_chars_num = + WideCharToMultiByte(CP_UTF8, UTF8_COMPOSITION_FLAG, wargv[i], NULL_TERMINATED, NULL, 0, NULL, NULL); + utf8_argv[i] = (char *) malloc(arg_chars_num * sizeof(char)); + if (!utf8_argv[i]) { + fprintf(stderr, "failed to to allocate memory for converting args"); + } + WideCharToMultiByte(CP_UTF8, UTF8_COMPOSITION_FLAG, wargv[i], NULL_TERMINATED, utf8_argv[i], arg_chars_num, NULL, NULL); + } + + LocalFree(wargv); + argv = utf8_argv; + +#endif + + int result = picovoice_main(argc, argv); + +#if defined(_WIN32) || defined(_WIN64) + + for (int i = 0; i < argc; ++i) { + free(utf8_argv[i]); + } + +#endif + + return result; +} diff --git a/demo/c/test/requirements.txt b/demo/c/test/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/demo/c/test/test_falcon_c.py b/demo/c/test/test_falcon_c.py new file mode 100644 index 0000000..1b7150e --- /dev/null +++ b/demo/c/test/test_falcon_c.py @@ -0,0 +1,63 @@ +# +# Copyright 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. +# + +import os.path +import subprocess +import sys +import unittest + + +def get_lib_ext(platform): + if platform == "windows": + return "dll" + elif platform == "mac": + return "dylib" + else: + return "so" + + +class FalconCTestCase(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls._access_key = sys.argv[1] + cls._platform = sys.argv[2] + cls._arch = "" if len(sys.argv) != 4 else sys.argv[3] + cls._root_dir = os.path.join(os.path.dirname(__file__), "../../..") + + def _get_library_file(self): + return os.path.join( + self._root_dir, + "lib", + self._platform, + self._arch, + "libpv_falcon." + get_lib_ext(self._platform) + ) + + def test_falcon(self): + args = [ + os.path.join(os.path.dirname(__file__), "../build/falcon_demo"), + "-a", self._access_key, + "-l", self._get_library_file(), + "-m", os.path.join(self._root_dir, 'lib/common/falcon_params.pv'), + os.path.join(self._root_dir, 'resources/audio_samples/test.wav'), + ] + process = subprocess.Popen(args, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + stdout, stderr = process.communicate() + self.assertEqual(process.poll(), 0) + self.assertEqual(stderr.decode('utf-8'), '') + + +if __name__ == '__main__': + if len(sys.argv) < 3 or len(sys.argv) > 4: + print("usage: test_falcon_c.py ${AccessKey} ${Platform} [${Arch}]") + exit(1) + unittest.main(argv=sys.argv[:1]) diff --git a/demo/python/.gitignore b/demo/python/.gitignore new file mode 100644 index 0000000..b1fa3f3 --- /dev/null +++ b/demo/python/.gitignore @@ -0,0 +1,5 @@ +build +dist +MANIFEST.in +pvfalcondemo +pvfalcondemo.egg-info \ No newline at end of file diff --git a/demo/python/README.md b/demo/python/README.md new file mode 100644 index 0000000..b4bedf1 --- /dev/null +++ b/demo/python/README.md @@ -0,0 +1,63 @@ +# Falcon Speaker Diarization Demos + +Made in Vancouver, Canada by [Picovoice](https://picovoice.ai) + +## Falcon + +Falcon is an on-device speaker diarization engine. Falcon is: + +- Private; All voice processing runs locally. +- Cross-Platform: + - Linux (x86_64), macOS (x86_64, arm64), Windows (x86_64) + - Raspberry Pi (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. + +## Installation + +```console +pip3 install pvfalcondemo +``` + +## AccessKey + +Falcon requires a valid Picovoice `AccessKey` at initialization. `AccessKey` acts as your credentials when using Falcon SDKs. +You can get your `AccessKey` for free. Make sure to keep your `AccessKey` secret. +Signup or Login to [Picovoice Console](https://console.picovoice.ai/) to get your `AccessKey`. + +## Usage + +### File Demo + +Run the following in the terminal: + +```console +falcon_demo_file --access_key ${ACCESS_KEY} --audio_paths ${AUDIO_PATH} +``` + +Replace `${ACCESS_KEY}` with yours obtained from Picovoice Console and `${AUDIO_PATH}` with a path to an audio file. + +### Microphone Demo + +You need a working microphone connected to your machine for this demo. Run the following in the terminal: + +```console +falcon_demo_mic --access_key ${ACCESS_KEY} +``` + +Replace `${ACCESS_KEY}` with yours obtained from Picovoice Console. Once running, the demo prints: + +```console +>>> Press `ENTER` to start: +``` + +Press `ENTER` key and wait for the following message in the terminal: + +```console +>>> Recording ... Press `ENTER` to stop: +``` + +Now start recording and when done press `ENTER` key. \ No newline at end of file diff --git a/demo/python/falcon_demo_file.py b/demo/python/falcon_demo_file.py new file mode 100644 index 0000000..345573c --- /dev/null +++ b/demo/python/falcon_demo_file.py @@ -0,0 +1,52 @@ +# +# Copyright 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. +# + +import argparse + +from pvfalcon import create, FalconActivationLimitError +from tabulate import tabulate + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + '--access_key', + help='AccessKey obtained from Picovoice Console (https://console.picovoice.ai/)', + required=True) + parser.add_argument( + '--library_path', + help='Absolute path to dynamic library. Default: using the library provided by `pvfalcon`') + parser.add_argument( + '--model_path', + help='Absolute path to Falcon model. Default: using the model provided by `pvfalcon`') + parser.add_argument( + '--wav_paths', + nargs='+', + required=True, + metavar='PATH', + help='Absolute paths to `.wav` files') + args = parser.parse_args() + + falcon = create( + access_key=args.access_key, + model_path=args.model_path, + library_path=args.library_path) + + try: + for wav_path in args.wav_paths: + segments = falcon.process_file(wav_path) + print(tabulate(segments, headers=['start_sec', 'end_sec', 'speaker_tag'], floatfmt='.2f')) + except FalconActivationLimitError: + print('AccessKey has reached its processing limit.') + + +if __name__ == '__main__': + main() diff --git a/demo/python/falcon_demo_mic.py b/demo/python/falcon_demo_mic.py new file mode 100644 index 0000000..8af667b --- /dev/null +++ b/demo/python/falcon_demo_mic.py @@ -0,0 +1,129 @@ +# +# 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. +# +# 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. +# + +import signal +import sys +import time +from argparse import ArgumentParser +from threading import Thread + +from pvfalcon import create, FalconActivationLimitError +from pvrecorder import PvRecorder +from tabulate import tabulate + + +class Recorder(Thread): + def __init__(self, audio_device_index): + super().__init__() + self._pcm = list() + self._is_recording = False + self._stop = False + self._audio_device_index = audio_device_index + + def is_recording(self): + return self._is_recording + + def run(self): + self._is_recording = True + + recorder = PvRecorder(frame_length=160, device_index=self._audio_device_index) + recorder.start() + + while not self._stop: + self._pcm.extend(recorder.read()) + recorder.stop() + + self._is_recording = False + + def stop(self): + self._stop = True + while self._is_recording: + pass + + return self._pcm + + +def main(): + parser = ArgumentParser() + parser.add_argument( + '--access_key', + help='AccessKey obtained from Picovoice Console (https://console.picovoice.ai/)') + parser.add_argument( + '--library_path', + help='Absolute path to dynamic library. Default: using the library provided by `pvfalcon`') + parser.add_argument( + '--model_path', + help='Absolute path to Falcon model. Default: using the model provided by `pvfalcon`') + parser.add_argument( + '--show_audio_devices', + action='store_true', + help='Only list available devices and exit') + parser.add_argument( + '--audio_device_index', + default=-1, + type=int, + help='Audio device index to use from --show_audio_devices') + args = parser.parse_args() + + if args.show_audio_devices: + for index, name in enumerate(PvRecorder.get_available_devices()): + print('Device #%d: %s' % (index, name)) + return + + if args.audio_device_index != -1: + devices_length = len(PvRecorder.get_available_devices()) + if args.audio_device_index < 0 or args.audio_device_index >= devices_length: + print('Invalid audio device index provided.') + sys.exit(1) + + if not args.access_key: + print('--access_key is required.') + return + + falcon = create( + access_key=args.access_key, + model_path=args.model_path, + library_path=args.library_path) + + recorder = None + + def on_exit(_, __): + falcon.delete() + + if recorder is not None: + recorder.stop() + + print() + sys.exit(0) + + signal.signal(signal.SIGINT, on_exit) + + print('>>> Press `CTRL+C` to exit: ') + + while True: + if recorder is not None: + input('>>> Recording ... Press `ENTER` to stop: ') + try: + segments = falcon.process(recorder.stop()) + print(tabulate(segments, headers=['start_sec', 'end_sec', 'speaker_tag'], floatfmt='.2f')) + except FalconActivationLimitError: + print('AccessKey has reached its processing limit.') + print() + recorder = None + else: + input('>>> Press `ENTER` to start: ') + recorder = Recorder(args.audio_device_index) + recorder.start() + time.sleep(.25) + + +if __name__ == '__main__': + main() diff --git a/demo/python/requirements.txt b/demo/python/requirements.txt new file mode 100644 index 0000000..dfec275 --- /dev/null +++ b/demo/python/requirements.txt @@ -0,0 +1,3 @@ +pvfalcon==1.0.0 +pvrecorder==1.2.1 +tabulate==0.9.0 diff --git a/demo/python/setup.py b/demo/python/setup.py new file mode 100644 index 0000000..3122449 --- /dev/null +++ b/demo/python/setup.py @@ -0,0 +1,57 @@ +import os +import shutil + +import setuptools + +os.system('git clean -dfx') + +package_folder = os.path.join(os.path.dirname(__file__), 'pvfalcondemo') +os.mkdir(package_folder) + +shutil.copy(os.path.join(os.path.dirname(__file__), '../../LICENSE'), package_folder) + +shutil.copy( + os.path.join(os.path.dirname(__file__), 'falcon_demo_file.py'), + os.path.join(package_folder, 'falcon_demo_file.py')) + +shutil.copy( + os.path.join(os.path.dirname(__file__), 'falcon_demo_mic.py'), + os.path.join(package_folder, 'falcon_demo_mic.py')) + +with open(os.path.join(os.path.dirname(__file__), 'MANIFEST.in'), 'w') as f: + f.write('include pvfalcondemo/LICENSE\n') + f.write('include pvfalcondemo/falcon_demo_file.py\n') + f.write('include pvfalcondemo/falcon_demo_mic.py\n') + +with open(os.path.join(os.path.dirname(__file__), 'README.md'), 'r') as f: + long_description = f.read() + +setuptools.setup( + name="pvfalcondemo", + version="1.0.0", + author="Picovoice", + author_email="hello@picovoice.ai", + description="Falcon Speaker Diarization engine demos", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/Picovoice/falcon", + packages=["pvfalcondemo"], + install_requires=["pvfalcon==1.0.0", "pvrecorder==1.2.1", "tabulate==0.8.10"], + include_package_data=True, + classifiers=[ + "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" + ], + entry_points=dict( + console_scripts=[ + 'falcon_demo_file=pvfalcondemo.falcon_demo_file:main', + 'falcon_demo_mic=pvfalcondemo.falcon_demo_mic:main', + ], + ), + python_requires='>=3.5', + keywords="Speaker Diarization, Speaker Identification, Voice Identification", +) diff --git a/include/picovoice.h b/include/picovoice.h new file mode 100644 index 0000000..6905124 --- /dev/null +++ b/include/picovoice.h @@ -0,0 +1,77 @@ +/* + Copyright 2018-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. +*/ + +#ifndef PICOVOICE_H +#define PICOVOICE_H + +#include + +#ifdef __cplusplus + +extern "C" { + +#endif + +#define PV_API __attribute__((visibility("default"))) + +/** + * Audio sample rate accepted by Picovoice. + */ +PV_API int32_t pv_sample_rate(void); + +/** + * Status codes. + */ +typedef enum { + PV_STATUS_SUCCESS = 0, + PV_STATUS_OUT_OF_MEMORY, + PV_STATUS_IO_ERROR, + PV_STATUS_INVALID_ARGUMENT, + PV_STATUS_STOP_ITERATION, + PV_STATUS_KEY_ERROR, + PV_STATUS_INVALID_STATE, + PV_STATUS_RUNTIME_ERROR, + PV_STATUS_ACTIVATION_ERROR, + PV_STATUS_ACTIVATION_LIMIT_REACHED, + PV_STATUS_ACTIVATION_THROTTLED, + PV_STATUS_ACTIVATION_REFUSED +} pv_status_t; + +/** + * Provides string representations of status codes. + * + * @param status Status code. + * @return String representation. + */ +PV_API const char *pv_status_to_string(pv_status_t status); + +/** + * If a function returns a failure (any pv_status_t other than PV_STATUS_SUCCESS), this function can be called + * to get a series of error messages related to the failure. This function must be called before any resources + * are cleaned up or freed. + * + * @param object Pointer to the object that returned the failure status. Use `NULL` for init failures or functions that + * do not take an object pointer. + * @param[out] message_stack Array of messages relating to the failure. Messages are NULL terminated strings. + * The array and messages are freed when the associated object is deleted. + * @param[out] message_stack_depth The number of messages in the `message_stack` array. + */ +PV_API void pv_get_error_stack( + const void *object, + const char ***message_stack, + int32_t *message_stack_depth); + +#ifdef __cplusplus +} + +#endif + +#endif // PICOVOICE_H diff --git a/include/pv_falcon.h b/include/pv_falcon.h new file mode 100644 index 0000000..3733886 --- /dev/null +++ b/include/pv_falcon.h @@ -0,0 +1,151 @@ +/* + 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. +*/ + +#ifndef PV_FALCON_H +#define PV_FALCON_H + +#include + +#include "picovoice.h" + +#ifdef __cplusplus + +extern "C" { + +#endif + +/** +* Forward Declaration for Falcon Speaker Diarization engine. +*/ +typedef struct pv_falcon pv_falcon_t; + +/** + * Constructor. + * + * @param access_key AccessKey obtained from Picovoice Console (https://console.picovoice.ai/) + * @param model_path The absolute path to the file containing Falcon's model parameters. + * @param[out] object Constructed instance of Falcon. + * @return A status code indicating the result of the initialization. Possible values include: + * - `PV_STATUS_OUT_OF_MEMORY`: Memory allocation failure. + * - `PV_STATUS_IO_ERROR`: Input/output error. + * - `PV_STATUS_INVALID_ARGUMENT`: Invalid input argument. + * - `PV_STATUS_RUNTIME_ERROR`: Error during runtime. + * - `PV_STATUS_ACTIVATION_ERROR`: Activation-related error. + * - `PV_STATUS_ACTIVATION_LIMIT_REACHED`: Activation limit reached. + * - `PV_STATUS_ACTIVATION_THROTTLED`: Activation throttled. + * - `PV_STATUS_ACTIVATION_REFUSED`: Activation refused. + */ +PV_API pv_status_t pv_falcon_init( + const char *access_key, + const char *model_path, + pv_falcon_t **object); + +/** + * Deallocate resources associated with a Falcon instance. + * + * This function releases the resources associated with a Falcon instance that was previously initialized using the + * `pv_falcon_init()` function. It deallocates memory and performs cleanup tasks, ensuring proper resource management. + * + * @param object A pointer to the Falcon object to be deallocated. + */ +PV_API void pv_falcon_delete(pv_falcon_t *object); + +/** + * Represents a segment with its start, end, and associated speaker tag. + */ +typedef struct { + float start_sec; /** Start time of the segment in seconds. */ + float end_sec; /** End time of the segment in seconds. */ + int32_t speaker_tag; /** Speaker tag identifier. */ +} pv_segment_t; + +/** + * Processes the given audio data and returns the diarization output. + * + * This function analyzes the provided audio data, which should be single-channel, 16-bit linearly-encoded, + * and have a sample rate matching `pv_sample_rate()`. It identifies different segments in the audio, each + * represented by a `pv_segment_t` structure. The caller is responsible for freeing the segments buffer + * using appropriate memory management. + * + * @param object A pointer to the Falcon object. + * @param pcm Pointer to the audio data array. + * @param num_samples Number of audio samples in the data array. + * @param[out] num_segments Pointer to store the number of segments in the output. + * @param[out] segments Pointer to an array of `pv_segment_t` structures representing the identified segments. + * The caller is responsible for freeing this buffer. + * @return Status code indicating the result of the processing. Possible values include: + * - `PV_STATUS_OUT_OF_MEMORY`: Memory allocation failure. + * - `PV_STATUS_IO_ERROR`: Input/output error. + * - `PV_STATUS_INVALID_ARGUMENT`: Invalid input argument. + * - `PV_STATUS_RUNTIME_ERROR`: Error during runtime. + * - `PV_STATUS_ACTIVATION_ERROR`: Activation-related error. + * - `PV_STATUS_ACTIVATION_LIMIT_REACHED`: Activation limit reached. + * - `PV_STATUS_ACTIVATION_THROTTLED`: Activation throttled. + * - `PV_STATUS_ACTIVATION_REFUSED`: Activation refused. + */ +PV_API pv_status_t pv_falcon_process( + pv_falcon_t *object, + const int16_t *pcm, + int32_t num_samples, + int32_t *num_segments, + pv_segment_t **segments); + +/** + * Processes a given audio file and returns the diarization output. + * + * + * @param object A pointer to the Falcon object. + * @param audio_path Absolute path to the audio file. The file needs to have a sample rate equal to or greater than + * `pv_sample_rate()`. The supported formats are: `3gp (AMR)`, `FLAC`, `MP3`, `MP4/m4a (AAC)`, `Ogg`, `WAV`, `WebM`. + * Files with stereo audio are mixed into a single mono channel and then processed. * @param num_samples Number of audio samples in the data array. + * @param[out] num_segments Pointer to store the number of segments in the output. + * @param[out] segments Pointer to an array of `pv_segment_t` structures representing the identified segments. + * The caller is responsible for freeing this buffer. + * @return Status code indicating the result of the processing. Possible values include: + * - `PV_STATUS_OUT_OF_MEMORY`: Memory allocation failure. + * - `PV_STATUS_IO_ERROR`: Input/output error. + * - `PV_STATUS_INVALID_ARGUMENT`: Invalid input argument. + * - `PV_STATUS_RUNTIME_ERROR`: Error during runtime. + * - `PV_STATUS_ACTIVATION_ERROR`: Activation-related error. + * - `PV_STATUS_ACTIVATION_LIMIT_REACHED`: Activation limit reached. + * - `PV_STATUS_ACTIVATION_THROTTLED`: Activation throttled. + * - `PV_STATUS_ACTIVATION_REFUSED`: Activation refused. + */ +PV_API pv_status_t pv_falcon_process_file( + pv_falcon_t *object, + const char *audio_path, + int32_t *num_segments, + pv_segment_t **segments); + +/** + * Deletes segments allocated by `pv_falcon_process()`. + * + * Use this function to properly release memory allocated for segments returned by the + * `pv_falcon_process()` functions. + * + * @param segments Pointer to the array of segments to be deleted. + */ +PV_API void pv_falcon_segments_delete(pv_segment_t *segments); + +/** + * Get the version of the Falcon library. + * + * @return A pointer to a string containing the version information. + */ +PV_API const char *pv_falcon_version(void); + +#ifdef __cplusplus + +} + +#endif + +#endif // PV_FALCON_H diff --git a/lib/common/falcon_params.pv b/lib/common/falcon_params.pv new file mode 100644 index 0000000..722955f Binary files /dev/null and b/lib/common/falcon_params.pv differ diff --git a/lib/jetson/cortex-a57-aarch64/libpv_falcon.so b/lib/jetson/cortex-a57-aarch64/libpv_falcon.so new file mode 100755 index 0000000..161eef1 Binary files /dev/null and b/lib/jetson/cortex-a57-aarch64/libpv_falcon.so differ diff --git a/lib/linux/x86_64/libpv_falcon.so b/lib/linux/x86_64/libpv_falcon.so new file mode 100755 index 0000000..161eef1 Binary files /dev/null and b/lib/linux/x86_64/libpv_falcon.so differ diff --git a/lib/mac/arm64/libpv_falcon.dylib b/lib/mac/arm64/libpv_falcon.dylib new file mode 100755 index 0000000..161eef1 Binary files /dev/null and b/lib/mac/arm64/libpv_falcon.dylib differ diff --git a/lib/mac/x86_64/libpv_falcon.dylib b/lib/mac/x86_64/libpv_falcon.dylib new file mode 100755 index 0000000..161eef1 Binary files /dev/null and b/lib/mac/x86_64/libpv_falcon.dylib differ diff --git a/lib/raspberry-pi/cortex-a53-aarch64/libpv_falcon.so b/lib/raspberry-pi/cortex-a53-aarch64/libpv_falcon.so new file mode 100755 index 0000000..161eef1 Binary files /dev/null and b/lib/raspberry-pi/cortex-a53-aarch64/libpv_falcon.so differ diff --git a/lib/raspberry-pi/cortex-a53/libpv_falcon.so b/lib/raspberry-pi/cortex-a53/libpv_falcon.so new file mode 100755 index 0000000..161eef1 Binary files /dev/null and b/lib/raspberry-pi/cortex-a53/libpv_falcon.so differ diff --git a/lib/raspberry-pi/cortex-a72-aarch64/libpv_falcon.so b/lib/raspberry-pi/cortex-a72-aarch64/libpv_falcon.so new file mode 100755 index 0000000..161eef1 Binary files /dev/null and b/lib/raspberry-pi/cortex-a72-aarch64/libpv_falcon.so differ diff --git a/lib/raspberry-pi/cortex-a72/libpv_falcon.so b/lib/raspberry-pi/cortex-a72/libpv_falcon.so new file mode 100755 index 0000000..161eef1 Binary files /dev/null and b/lib/raspberry-pi/cortex-a72/libpv_falcon.so differ diff --git a/lib/windows/amd64/libpv_falcon.dll b/lib/windows/amd64/libpv_falcon.dll new file mode 100755 index 0000000..161eef1 Binary files /dev/null and b/lib/windows/amd64/libpv_falcon.dll differ diff --git a/resources/.lint/c/formatter.py b/resources/.lint/c/formatter.py new file mode 100644 index 0000000..964ba3d --- /dev/null +++ b/resources/.lint/c/formatter.py @@ -0,0 +1,81 @@ +import fnmatch +import os +import re +import subprocess +from argparse import ArgumentParser + +import sys + +IGNORE_LIST = { + 'demo/c/dr_libs', + 'demo/c/pvrecorder', + 'lib' +} + +EXCLUDE_PATTERN = { + '.*ios.*', + '.*node_modules.*', + '.*?/mcu(?!.*src/(pv|main)).*', + '.*build.*', +} + + +def main(): + parser = ArgumentParser() + + parser.add_argument('--verbose', + '-v', + action='store_true', + help='If set, shows the list of processed files') + parser.add_argument('--check-only', + '-c', + action='store_true', + help='If set, checks for warnings only') + + input_args = parser.parse_args() + formatter(input_args.verbose, input_args.check_only) + + +def formatter(verbose, check_only): + if check_only: + cmd = "clang-format --dry-run --Werror --verbose" + else: + cmd = "clang-format -i -style=file --verbose" + print(cmd) + + src_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../../../') + all_files = find('*.c', src_dir) + all_files.extend(find('*.h', src_dir)) + + c_source_files = [file_path for file_path in all_files if + not any(ignored_path in file_path for ignored_path in IGNORE_LIST)] + + c_source_files = [file_path for file_path in c_source_files if + not any(re.match(pattern, file_path, flags=re.IGNORECASE) for pattern in EXCLUDE_PATTERN)] + + c_source_files_num = len(c_source_files) + for index, c_source_file in enumerate(c_source_files): + format_command = f"{cmd} {c_source_file}" + try: + result = subprocess.check_output(format_command, shell=True, stderr=subprocess.STDOUT).decode('utf-8') + except subprocess.CalledProcessError as e: + print(f'Formatter failed with ({e.returncode}):\n{e.output.decode("utf-8")}') + continue + + if verbose: + print(result) + sys.stdout.write(f"Completion: {index / c_source_files_num * 100:.2f}%\r") + sys.stdout.flush() + + +def find(pattern, path): + file_list = [] + for root, dirs, files in os.walk(path): + for name in files: + if fnmatch.fnmatch(name, pattern): + file_list.append(os.path.join(root, name)) + return file_list + + +if __name__ == '__main__': + main() diff --git a/resources/.lint/spell-check/.cspell.json b/resources/.lint/spell-check/.cspell.json new file mode 100644 index 0000000..17ff2af --- /dev/null +++ b/resources/.lint/spell-check/.cspell.json @@ -0,0 +1,21 @@ +{ + "language": "en", + "dictionaries": [ + "dict" + ], + "dictionaryDefinitions": [ + { + "name": "dict", + "path": "./dict.txt", + "addWords": true + } + ], + "ignorePaths": [ + // binaries + "**/*.dll", + "**/*.mp3", + "**/*.so", + "**/*.wav", + "**/*.pv", + ] +} diff --git a/resources/.lint/spell-check/dict.txt b/resources/.lint/spell-check/dict.txt new file mode 100644 index 0000000..27bf359 --- /dev/null +++ b/resources/.lint/spell-check/dict.txt @@ -0,0 +1,20 @@ +LPWSTR +Makefiles +Picovoice +aarch +armv +diarization +diarize +flac +floatfmt +fprintf +jetson +libpv +pvfalcon +pvfalcondemo +pvrecorder +signup +stdbool +stdlib +vorbis +wargv \ No newline at end of file diff --git a/resources/.scripts/machine-state.sh b/resources/.scripts/machine-state.sh new file mode 100644 index 0000000..05d7021 --- /dev/null +++ b/resources/.scripts/machine-state.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +echo -e "-------------------------------Machine State----------------------------" +echo -e "CPU Usage:\t"`cat /proc/stat | awk '/cpu/{printf("%.2f%%\n"), ($2+$4)*100/($2+$4+$5)}' | awk '{print $0}' | head -1` +echo -e "Memory Usage:\t"`free | awk '/Mem/{printf("%.2f%%"), $3/$2*100}'` +echo -e "Swap Usage:\t"`free | awk '/Swap/{printf("%.2f%%"), $3/$2*100}'` +paste <(cat /sys/class/thermal/thermal_zone*/type) <(cat /sys/class/thermal/thermal_zone*/temp) | column -s $'\t' -t | sed 's/\(.\)..$/.\1°C/' diff --git a/resources/.test/test_data.json b/resources/.test/test_data.json new file mode 100644 index 0000000..508536a --- /dev/null +++ b/resources/.test/test_data.json @@ -0,0 +1,14 @@ +{ + "tests": { + "diarization_tests": [ + { + "audio_file": "test.wav", + "segments": [ + {"start_sec": 1.70, "end_sec": 3.90, "speaker_tag": 1}, + {"start_sec": 6.20, "end_sec": 11.10, "speaker_tag": 2}, + {"start_sec": 12.80, "end_sec": 18.00, "speaker_tag": 3} + ] + } + ] + } +} \ No newline at end of file diff --git a/resources/audio_samples/test.wav b/resources/audio_samples/test.wav new file mode 100644 index 0000000..37f8a30 Binary files /dev/null and b/resources/audio_samples/test.wav differ