diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..01ecd10 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +liberapay: jarbasAI diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..26e59a2 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "pip" # See documentation for possible values + directory: "/requirements" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/workflows/build_tests.yml b/.github/workflows/build_tests.yml new file mode 100644 index 0000000..c982751 --- /dev/null +++ b/.github/workflows/build_tests.yml @@ -0,0 +1,25 @@ +name: Run Build Tests +on: + push: + workflow_dispatch: + +jobs: + build_tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.head_ref }} + - name: Setup Python + uses: actions/setup-python@v1 + with: + python-version: 3.8 + - name: Install Build Tools + run: | + python -m pip install build wheel + - name: Build Distribution Packages + run: | + python setup.py bdist_wheel + - name: Install package + run: | + pip install . diff --git a/.github/workflows/conventional-label.yaml b/.github/workflows/conventional-label.yaml new file mode 100644 index 0000000..0a449cb --- /dev/null +++ b/.github/workflows/conventional-label.yaml @@ -0,0 +1,10 @@ +# auto add labels to PRs +on: + pull_request_target: + types: [ opened, edited ] +name: conventional-release-labels +jobs: + label: + runs-on: ubuntu-latest + steps: + - uses: bcoe/conventional-release-labels@v1 \ No newline at end of file diff --git a/.github/workflows/license_tests.yml b/.github/workflows/license_tests.yml new file mode 100644 index 0000000..561e773 --- /dev/null +++ b/.github/workflows/license_tests.yml @@ -0,0 +1,35 @@ +name: Run License Tests +on: + push: + workflow_dispatch: + +jobs: + license_tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.head_ref }} + - name: Setup Python + uses: actions/setup-python@v1 + with: + python-version: 3.8 + - name: Install Build Tools + run: | + python -m pip install build wheel + - name: Install System Dependencies + run: | + sudo apt-get update + sudo apt install python3-dev swig libssl-dev + - name: Install core repo + run: | + pip install . + - name: Install licheck + run: | + pip install git+https://github.com/NeonJarbas/lichecker + - name: Install test dependencies + run: | + pip install pytest pytest-timeout pytest-cov + - name: Test Licenses + run: | + pytest test/license_tests.py \ No newline at end of file diff --git a/.github/workflows/publish_stable.yml b/.github/workflows/publish_stable.yml new file mode 100644 index 0000000..fea875a --- /dev/null +++ b/.github/workflows/publish_stable.yml @@ -0,0 +1,58 @@ +name: Stable Release +on: + push: + branches: [master] + workflow_dispatch: + +jobs: + publish_stable: + uses: TigreGotico/gh-automations/.github/workflows/publish-stable.yml@master + secrets: inherit + with: + branch: 'master' + version_file: 'hivemind_mic_sat/version.py' + setup_py: 'setup.py' + publish_release: true + + publish_pypi: + needs: publish_stable + if: success() # Ensure this job only runs if the previous job succeeds + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + ref: dev + fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository. + - name: Setup Python + uses: actions/setup-python@v1 + with: + python-version: 3.8 + - name: Install Build Tools + run: | + python -m pip install build wheel + - name: version + run: echo "::set-output name=version::$(python setup.py --version)" + id: version + - name: Build Distribution Packages + run: | + python setup.py sdist bdist_wheel + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@master + with: + password: ${{secrets.PYPI_TOKEN}} + + + sync_dev: + needs: publish_stable + if: success() # Ensure this job only runs if the previous job succeeds + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository. + ref: master + - name: Push master -> dev + uses: ad-m/github-push-action@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + branch: dev \ No newline at end of file diff --git a/.github/workflows/release_workflow.yml b/.github/workflows/release_workflow.yml new file mode 100644 index 0000000..93b4103 --- /dev/null +++ b/.github/workflows/release_workflow.yml @@ -0,0 +1,108 @@ +name: Release Alpha and Propose Stable + +on: + pull_request: + types: [closed] + branches: [dev] + +jobs: + publish_alpha: + if: github.event.pull_request.merged == true + uses: TigreGotico/gh-automations/.github/workflows/publish-alpha.yml@master + secrets: inherit + with: + branch: 'dev' + version_file: 'hivemind_mic_sat/version.py' + setup_py: 'setup.py' + update_changelog: true + publish_prerelease: true + changelog_max_issues: 100 + + notify: + if: github.event.pull_request.merged == true + needs: publish_alpha + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Send message to Matrix bots channel + id: matrix-chat-message + uses: fadenb/matrix-chat-message@v0.0.6 + with: + homeserver: 'matrix.org' + token: ${{ secrets.MATRIX_TOKEN }} + channel: '!WjxEKjjINpyBRPFgxl:krbel.duckdns.org' + message: | + new ${{ github.event.repository.name }} PR merged! https://github.com/${{ github.repository }}/pull/${{ github.event.number }} + + publish_pypi: + needs: publish_alpha + if: success() # Ensure this job only runs if the previous job succeeds + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + ref: dev + fetch-depth: 0 # otherwise, there would be errors pushing refs to the destination repository. + - name: Setup Python + uses: actions/setup-python@v1 + with: + python-version: 3.8 + - name: Install Build Tools + run: | + python -m pip install build wheel + - name: version + run: echo "::set-output name=version::$(python setup.py --version)" + id: version + - name: Build Distribution Packages + run: | + python setup.py sdist bdist_wheel + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@master + with: + password: ${{secrets.PYPI_TOKEN}} + + + propose_release: + needs: publish_alpha + if: success() # Ensure this job only runs if the previous job succeeds + runs-on: ubuntu-latest + steps: + - name: Checkout dev branch + uses: actions/checkout@v3 + with: + ref: dev + + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: '3.10' + + - name: Get version from setup.py + id: get_version + run: | + VERSION=$(python setup.py --version) + echo "VERSION=$VERSION" >> $GITHUB_ENV + + - name: Create and push new branch + run: | + git checkout -b release-${{ env.VERSION }} + git push origin release-${{ env.VERSION }} + + - name: Open Pull Request from dev to master + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Variables + BRANCH_NAME="release-${{ env.VERSION }}" + BASE_BRANCH="master" + HEAD_BRANCH="release-${{ env.VERSION }}" + PR_TITLE="Release ${{ env.VERSION }}" + PR_BODY="Human review requested!" + + # Create a PR using GitHub API + curl -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: token $GITHUB_TOKEN" \ + -d "{\"title\":\"$PR_TITLE\",\"body\":\"$PR_BODY\",\"head\":\"$HEAD_BRANCH\",\"base\":\"$BASE_BRANCH\"}" \ + https://api.github.com/repos/${{ github.repository }}/pulls + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c8ece8c --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +dev.env +.dev_opts.json +.idea +*.code-workspace +*.pyc +*.swp +*~ +*.egg-info/ +build +dist +.coverage +/htmlcov +.installed +.mypy_cache +.vscode +.theia +.venv/ + +# Created by unit tests +.pytest_cache/ +/.gtm/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9d7a7d1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +## [0.2.0a1](https://github.com/JarbasHiveMind/hivemind-mic-satellite/tree/0.2.0a1) (2024-10-25) + +[Full Changelog](https://github.com/JarbasHiveMind/hivemind-mic-satellite/compare/fb82358fa1493c32676647a2ace2420bc6aa4986...0.2.0a1) + +**Merged pull requests:** + +- feat:semver [\#1](https://github.com/JarbasHiveMind/hivemind-mic-satellite/pull/1) ([JarbasAl](https://github.com/JarbasAl)) + + + +\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)* diff --git a/README.md b/README.md new file mode 100644 index 0000000..9c65096 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# HiveMind Microphone Satellite + +OpenVoiceOS Microphone Satellite, connect to [HiveMind Listener](https://github.com/JarbasHiveMind/HiveMind-listener) + +A super lightweight version of [voice-satellite](https://github.com/JarbasHiveMind/HiveMind-voice-sat), only Microphone and VAD plugins runs on the mic-satellite, voice activity is streamed to `hivemind-listener` and all the processing happens there + +> NOTE: `hivemind-listener` is required server side, the default `hivemind-core` does not provide audio streaming capabilities + +## Install + +Install dependencies (if needed) + +```bash +sudo apt-get install -y libpulse-dev libasound2-dev +``` + +Install with pip + +```bash +$ pip install git+https://github.com/JarbasHiveMind/hivemind-mic-satellite +``` + + +## Configuration + +Voice relay is built on top of [ovos-simple-listener](https://github.com/TigreGotico/ovos-simple-listener), it uses the same OpenVoiceOS configuration `~/.config/mycroft/mycroft.conf` + +Supported plugins: + +| Plugin Type | Description | Required | Link | +|-------------|-------------|----------|------| +| Microphone | Captures voice input | Yes | [Microphone](https://openvoiceos.github.io/ovos-technical-manual/mic_plugins/) | +| VAD | Voice Activity Detection | Yes | [VAD](https://openvoiceos.github.io/ovos-technical-manual/vad_plugins/) | + +> NOTE: the mic satellite can not (yet) play media, if you ask OVOS to "play XXX" nothing will happen as the mic-satellite will ignore the received uri diff --git a/hivemind_mic_sat/__init__.py b/hivemind_mic_sat/__init__.py index 526f51c..355d3fa 100644 --- a/hivemind_mic_sat/__init__.py +++ b/hivemind_mic_sat/__init__.py @@ -2,14 +2,13 @@ import os.path from ovos_bus_client.message import Message -from ovos_microphone_plugin_alsa import AlsaMicrophone -from ovos_vad_plugin_silero import SileroVAD -from ovos_plugin_manager.microphone import OVOSMicrophoneFactory, Microphone -from ovos_plugin_manager.vad import OVOSVADFactory, VADEngine + from hivemind_bus_client.client import HiveMessageBusClient from hivemind_bus_client.message import HiveMessage, HiveMessageType from hivemind_bus_client.serialization import HiveMindBinaryPayloadType +from ovos_plugin_manager.microphone import OVOSMicrophoneFactory, Microphone from ovos_plugin_manager.utils.tts_cache import hash_sentence +from ovos_plugin_manager.vad import OVOSVADFactory, VADEngine from ovos_utils.fakebus import FakeBus from ovos_utils.log import LOG from ovos_utils.sound import play_audio @@ -21,7 +20,7 @@ def __init__(self): self.hm_bus = HiveMessageBusClient() self.hm_bus.connect(FakeBus()) self.hm_bus.connected_event.wait() - print("== connected to HiveMind") + LOG.info("== connected to HiveMind") self.mic: Microphone = OVOSMicrophoneFactory.create() self.vad: VADEngine = OVOSVADFactory.create() self.running = False @@ -83,10 +82,6 @@ def handle_speak_b64(self, message: Message): def handle_complete(self, message: Message): LOG.info("UTTERANCE HANDLED!") - def get_silence(self, seconds: int): - total_silence_size = self.mic.sample_rate * self.mic.sample_width * seconds - return b"0" * int(total_silence_size) - def run(self): self.running = True self.mic.start() @@ -112,30 +107,23 @@ def run(self): LOG.info("Speech start, initiating audio transmission") in_speech = True + if in_speech: self.hm_bus.emit( HiveMessage(msg_type=HiveMessageType.BINARY, payload=chunk), binary_type=HiveMindBinaryPayloadType.RAW_AUDIO ) - # reached the max allowed silence time, stop sending audio - elif in_speech and total_silence_duration >= max_silence_duration: - in_speech = False - LOG.info(f"No speech for {max_silence_duration} seconds, stopping audio transmission") - # send silence audio to ensure DinkumVoiceLoop gets enough silence and triggers STT step - # otherwise it will hand waiting for extra chunks and never do the actual STT - self.hm_bus.emit( - HiveMessage(msg_type=HiveMessageType.BINARY, - payload=self.get_silence(3)), # pad - binary_type=HiveMindBinaryPayloadType.RAW_AUDIO - ) + # reached the max allowed silence time, stop sending audio + if total_silence_duration >= max_silence_duration: + in_speech = False + LOG.info(f"No speech for {max_silence_duration} seconds, stopping audio transmission") self.running = False -def test(): +def run(): h = HiveMindMicrophoneClient() - h.run() if __name__ == "__main__": - test() + run() diff --git a/hivemind_mic_sat/version.py b/hivemind_mic_sat/version.py new file mode 100644 index 0000000..d446f48 --- /dev/null +++ b/hivemind_mic_sat/version.py @@ -0,0 +1,6 @@ +# START_VERSION_BLOCK +VERSION_MAJOR = 0 +VERSION_MINOR = 2 +VERSION_BUILD = 0 +VERSION_ALPHA = 1 +# END_VERSION_BLOCK diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ebaa935 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +hivemind_bus_client +ovos-plugin-manager \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..1dda594 --- /dev/null +++ b/setup.py @@ -0,0 +1,57 @@ +import os +from setuptools import setup + +BASEDIR = os.path.abspath(os.path.dirname(__file__)) + + +def get_version(): + """Find the version of the package""" + version = None + version_file = os.path.join(BASEDIR, "hivemind_mic_sat", "version.py") + major, minor, build, alpha = (None, None, None, None) + with open(version_file) as f: + for line in f: + if "VERSION_MAJOR" in line: + major = line.split("=")[1].strip() + elif "VERSION_MINOR" in line: + minor = line.split("=")[1].strip() + elif "VERSION_BUILD" in line: + build = line.split("=")[1].strip() + elif "VERSION_ALPHA" in line: + alpha = line.split("=")[1].strip() + + if (major and minor and build and alpha) or "# END_VERSION_BLOCK" in line: + break + version = f"{major}.{minor}.{build}" + if int(alpha) > 0: + version += f"a{alpha}" + return version + + +def required(requirements_file): + """Read requirements file and remove comments and empty lines.""" + with open(os.path.join(BASEDIR, requirements_file), "r") as f: + requirements = f.read().splitlines() + if "MYCROFT_LOOSE_REQUIREMENTS" in os.environ: + print("USING LOOSE REQUIREMENTS!") + requirements = [ + r.replace("==", ">=").replace("~=", ">=") for r in requirements + ] + return [pkg for pkg in requirements if pkg.strip() and not pkg.startswith("#")] + + +setup( + name="hivemind-mic-satellite", + version=get_version(), + packages=["hivemind_mic_sat"], + include_package_data=True, + install_requires=required("requirements.txt"), + url="https://github.com/JarbasHiveMind/hivemind-mic-satellite", + license="Apache2.0", + author="jarbasAI", + author_email="jarbasai@mailfence.com", + description="Remote microphone for HiveMind", + entry_points={ + "console_scripts": ["hivemind-mic-sat=hivemind_mic_sat:run"] + }, +)