diff --git a/code/arlab_common_interfaces/CMakeLists.txt b/code/arlab_common_interfaces/CMakeLists.txt index 9b8e53dc..43d144d9 100644 --- a/code/arlab_common_interfaces/CMakeLists.txt +++ b/code/arlab_common_interfaces/CMakeLists.txt @@ -43,6 +43,8 @@ rosidl_generate_interfaces(${PROJECT_NAME} "msg/ManipulationCommand.msg" "msg/VisionSnapshotCommand.msg" + "msg/WhisperTranscript.msg" + "action/MovementAction.action" "action/ManipulationAction.action" "action/OrchestratorAction.action" diff --git a/code/arlab_common_interfaces/msg/WhisperTranscript.msg b/code/arlab_common_interfaces/msg/WhisperTranscript.msg new file mode 100644 index 00000000..69037020 --- /dev/null +++ b/code/arlab_common_interfaces/msg/WhisperTranscript.msg @@ -0,0 +1,2 @@ +builtin_interfaces/Time stamp +string transcript diff --git a/code/arlab_whisper/LICENSE b/code/arlab_whisper/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/code/arlab_whisper/LICENSE @@ -0,0 +1,202 @@ + + 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 [yyyy] [name of copyright owner] + + 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/code/arlab_whisper/arlab_whisper/__init__.py b/code/arlab_whisper/arlab_whisper/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/code/arlab_whisper/arlab_whisper/whisper_node.py b/code/arlab_whisper/arlab_whisper/whisper_node.py new file mode 100644 index 00000000..d235049e --- /dev/null +++ b/code/arlab_whisper/arlab_whisper/whisper_node.py @@ -0,0 +1,431 @@ +""" +This package allows speech recognition using Faster-Whisper with wakeword detection. +It continuously monitors microphone input, detects configurable wakewords, +transcribes them and publishes the result +""" + +#!/usr/bin/env python3 + +import os +import tempfile +import time +import wave +from collections import deque +from enum import Enum + +import numpy as np +import re +import rclpy +import sounddevice as sd + +from faster_whisper import WhisperModel + +from rclpy.node import Node +from std_msgs.msg import Bool + +from arlab_common_interfaces.msg import WhisperTranscript + + +class PipelineState(Enum): + """ + Pipeline execution states for the speech processing pipeline + """ + + IDLE = 1 + CHECK_SPEECH = 2 + CHECK_WAKEWORD = 3 + RECORD_COMMAND = 4 + TRANSCRIBE_COMMAND = 5 + + +class WhisperNode(Node): + """ + ros2 node for wakeword detection and speech transcription + """ + + def __init__(self): + """ + loads parameters, initializes the wakeword and whisper models, + configures the audio stream creates ros publishers and subscription + and starts the pipeline + + """ + super().__init__("whisper") + + # load all parameters from a yaml file + self.load_parameters() + + # check if the zirbi wakewords should be added to the wakeword detection + self.checkedWakewords = self.wakewords + if self.use_zirbi_wakewords: + self.checkedWakewords += self.zirbi_wakewords + + # set the pipeline start + self.state = PipelineState.CHECK_SPEECH + self.listening_enabled = True + self.last_detection_time = 0.0 + self.command_audio = [] + self.recording_command = False + self.record_buffer = [] + + # load the whisper models + self.get_logger().info("Loading wakeword model") + self.wakeword_model = WhisperModel( + self.wakeword_model_name, + device=self.device, + compute_type=self.compute_type, + ) + + self.get_logger().info("loading command model") + self.command_model = WhisperModel( + self.command_model_name, + device=self.device, + compute_type=self.compute_type, + ) + + # init the audio input buffer + self.audio_buffer = deque(maxlen=int(self.sample_rate * self.rolling_buffer_seconds)) + + # ros subscriptions/publisher + self.create_subscription( + Bool, + "/whisper/enabled", + self.enabled_callback, + 10, + ) + self.transcript_publisher = self.create_publisher( + WhisperTranscript, + "/whisper/transcript", + 10, + ) + + # init the audio input stream + self.stream = sd.InputStream( + samplerate=self.sample_rate, + channels=self.channels, + dtype=np.int16, + callback=self.audio_callback, + ) + # start listening + self.stream.start() + + # init the timer to start the pipline process + self.timer = self.create_timer(self.check_interval, self.process_pipeline) + + self.get_logger().info("Whisper node ready") + + def load_parameters(self): + """ + declares all configurable parameters with default values and loads the + parameter values provided by the launch file via a yaml file + """ + + # declare all default parameters + + self.declare_parameter("wakewords", ["robot"]) + self.declare_parameter("use_zirbi_wakewords", True) + self.declare_parameter("zirbi_wakewords", ["zirbi"]) + self.declare_parameter("wakeword_model", "tiny.en") + self.declare_parameter("command_model", "large-v3-turbo") + self.declare_parameter("device", "cuda") + self.declare_parameter("compute_type", "int8") + self.declare_parameter("sample_rate", 16000) + self.declare_parameter("channels", 1) + self.declare_parameter("rolling_buffer_seconds", 3.0) + self.declare_parameter("command_record_seconds", 4.0) + self.declare_parameter("speech_threshold", 150) + self.declare_parameter("check_interval", 0.25) + self.declare_parameter("language", "en") + self.declare_parameter("beam_size", 5) + + # load the yaml parameters loaded in the launch file + + self.wakewords = [word.lower() for word in self.get_parameter("wakewords").value] + + self.use_zirbi_wakewords = self.get_parameter("use_zirbi_wakewords").value + + self.zirbi_wakewords = [word.lower() for word in self.get_parameter("zirbi_wakewords").value] + + self.wakeword_model_name = self.get_parameter("wakeword_model").value + self.command_model_name = self.get_parameter("command_model").value + self.device = self.get_parameter("device").value + self.compute_type = self.get_parameter("compute_type").value + self.sample_rate = self.get_parameter("sample_rate").value + self.channels = self.get_parameter("channels").value + self.rolling_buffer_seconds = self.get_parameter("rolling_buffer_seconds").value + self.command_record_seconds = self.get_parameter("command_record_seconds").value + self.speech_threshold = self.get_parameter("speech_threshold").value + self.check_interval = self.get_parameter("check_interval").value + self.language = self.get_parameter("language").value + self.beam_size = self.get_parameter("beam_size").value + + def enabled_callback(self, msg: Bool): + """ + en- or disables wakeword detection and the pipeline through a recieved ros2 message + + args: msg (Bool): ros bool msg + + """ + + self.listening_enabled = msg.data + self.get_logger().info(f"Listening ON/OFF: {self.listening_enabled}") + + def audio_callback(self, indata, frames, time_info, status): + """ + receives audio chunks from the audio input stream, stores them in the + rolling audio buffer + + Args: + indata (numpy.ndarray): incoming audio data + frames (int): number of audio frames + time_info (CData): timing information + status: status information + """ + + # retrun issues + if status: + self.get_logger().warning(str(status)) + + samples = indata[:, 0] + + # fill the buffer + self.audio_buffer.extend(samples) + + # if record is enabled + if self.recording_command: + self.record_buffer.extend(samples) + + def process_pipeline(self): + """ + Main pipeline structure + + checks the current pipeline state and executes the corresponding processingstep + includs speech detection, wakeword detection command recording and + command transcription + """ + + # returns if the pipeline is disables + if not self.listening_enabled: + self.state = PipelineState.IDLE + return + + # starts the pipeline if it was disabled + if self.state == PipelineState.IDLE: + self.state = PipelineState.CHECK_SPEECH + + # waits till the audio buffer is full, checks if speech is detected + # if this is the case switches the state to check for a wakeword + if self.state == PipelineState.CHECK_SPEECH: + if len(self.audio_buffer) < self.sample_rate: + return + if self.detect_speech(): + self.state = PipelineState.CHECK_WAKEWORD + return + + # tries to detect a wakeword, if this was the case and no other wakeword was detected for some time + # switch the state and save the input buffer + if self.state == PipelineState.CHECK_WAKEWORD: + audio = np.array(self.audio_buffer, dtype=np.int16) + if self.detect_wakeword(audio): + now = time.time() + if now - self.last_detection_time > self.rolling_buffer_seconds: + self.last_detection_time = now + self.get_logger().info("Wakeword detected") + # save old buffer + self.command_audio = list(self.audio_buffer) + self.state = PipelineState.RECORD_COMMAND + else: + self.state = PipelineState.CHECK_SPEECH + return + + # records a command if triggerd and switch to transcribe + if self.state == PipelineState.RECORD_COMMAND: + self.command_audio = self.record_command() + self.state = PipelineState.TRANSCRIBE_COMMAND + return + + # transcibes and sends the command resets the pipeline + if self.state == PipelineState.TRANSCRIBE_COMMAND: + transcript = self.transcribe_command(self.command_audio) + if transcript: + self.send_transcript(transcript) + self.command_audio = [] + self.state = PipelineState.CHECK_SPEECH + return + + def detect_speech(self): + """ " + calculates the "energy" of the rolling audio buffer + and compares it vs the configured speech threshold + + return: bool: true if the detected audio exeeds the threshold, oetherwise False. + """ + + audio = np.array(self.audio_buffer, dtype=np.float32) + rms = np.sqrt(np.mean(audio**2)) + if rms < self.speech_threshold: + return False + return True + + def detect_wakeword(self, audio): + """detects whether a configured wakeword is part of the audio using a whisper model + Args: + audio (numpy.ndarray): audio sample + + Returns: + bool: true if a configured wakeword was detected, otherweise false + """ + + # save the audio temporarly and run the whisper model + wav_path = self.save_temp_wav(audio) + try: + segments, info = self.wakeword_model.transcribe( + wav_path, + language=self.language, + beam_size=self.beam_size, + # initial_prompt="zirbi" + ) + """ + #debug output of the transcript + for segment in segments: + self.get_logger().info( + f"debug: {segment.text}" + ) + """ + # refactor the text to all lowercase + text = " ".join(segment.text for segment in segments).lower() + + # self.get_logger().debug(f"Wakeword model: {text}") + + # extract all used words from the text + words = re.findall(r"[a-zäöüß]+", text.lower()) + + # check if the refactored words fit a wakeword word beginning + for wakeword_i in self.checkedWakewords: + wakeword = wakeword_i.lower() + for word in words: + if word == wakeword: + return True + if word.startswith(wakeword): + return True + return False + + except Exception as e: + self.get_logger().error(f"wakeword detect error: {e}") + return False + + finally: + try: + os.remove(wav_path) + except Exception: + pass + + def record_command(self): + """ + initializes the command recording buffer with the audio captured before the + wakeword + continues recording incoming audio for some time + + return: + numpy.ndarray: recorded command audio samples + """ + + self.record_buffer = list(self.command_audio) + self.recording_command = True + time.sleep(self.command_record_seconds) + self.recording_command = False + + audio = np.array(self.record_buffer, dtype=np.int16) + return audio + + def save_temp_wav(self, audio): + """ + creates a temporary WAV file from the provided audio data + Args: audio (numpy.ndarray): audio sample + + Return: str: path to the created tfile + """ + temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".wav") + filename = temp_file.name + temp_file.close() + with wave.open(filename, "wb") as wf: + wf.setnchannels(self.channels) + wf.setsampwidth(2) + wf.setframerate(self.sample_rate) + wf.writeframes(audio.tobytes()) + return filename + + def transcribe_command(self, audio): + """ + converts the provided audio data into a temporary -wav fileand runs the + whisper model to generate a transcription + + args: audio (numpy.ndarray): recorded audio + return: str: transcribed text, empty if the transcript fails + """ + + # run model + wav_path = self.save_temp_wav(audio) + try: + segments, info = self.command_model.transcribe( + wav_path, language=self.language, beam_size=self.beam_size, initial_prompt="Your name is Zirbi." + ) + # combines the output into one clean string + text = " ".join(segment.text for segment in segments) + text = text.strip() + return text + + except Exception as e: + self.get_logger().error(f"command transcriptio error: {e}") + return "" + + finally: + try: + os.remove(wav_path) + except Exception: + pass + + def send_transcript(self, transcript): + """ + creates a WhisperTranscript message from the provided text and publishes it + + args: transcript (str): transcribed text to publish + + """ + if not transcript: + return + msg = WhisperTranscript() + msg.transcript = transcript + self.transcript_publisher.publish(msg) + self.get_logger().info(f"Published transcript: {transcript}") + + def destroy_node(self): + """ + cleans up resources before shutting down node. + """ + try: + self.stream.stop() + self.stream.close() + except Exception: + pass + super().destroy_node() + + +def main(args=None): + + rclpy.init(args=args) + node = WhisperNode() + + try: + rclpy.spin(node) + + except KeyboardInterrupt: + pass + + finally: + node.destroy_node() + rclpy.shutdown() + + +if __name__ == "__main__": + main() diff --git a/code/arlab_whisper/audio_test/recordings/testaudio_CHERRY.wav b/code/arlab_whisper/audio_test/recordings/testaudio_CHERRY.wav new file mode 100644 index 00000000..d9937e15 Binary files /dev/null and b/code/arlab_whisper/audio_test/recordings/testaudio_CHERRY.wav differ diff --git a/code/arlab_whisper/audio_test/recordings/testaudio_JABRA.wav b/code/arlab_whisper/audio_test/recordings/testaudio_JABRA.wav new file mode 100644 index 00000000..334afbff Binary files /dev/null and b/code/arlab_whisper/audio_test/recordings/testaudio_JABRA.wav differ diff --git a/code/arlab_whisper/audio_test/save_wav.py b/code/arlab_whisper/audio_test/save_wav.py new file mode 100644 index 00000000..a4c3b98f --- /dev/null +++ b/code/arlab_whisper/audio_test/save_wav.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +import os +import wave + +import numpy as np +import sounddevice as sd + + +SAMPLE_RATE = 16000 +CHANNELS = 1 +DTYPE = np.int16 +OUTPUT_FOLDER = "recordings" +RECORD_SECONDS = 5 +BLOCKSIZE = int(SAMPLE_RATE * RECORD_SECONDS) + + +def save_wav(filename, audio): + """saves audio samples to a WAV file + + Args: + filename (String): path to the stored file location + audio (numpy.ndarray): Audio samples as a array of the + dtype datatype + """ + with wave.open(filename, "wb") as wf: + wf.setnchannels(CHANNELS) + wf.setsampwidth(2) # int16 = 2 bytes + wf.setframerate(SAMPLE_RATE) + wf.writeframes(audio.tobytes()) + + +def main(): + """continuously record audio and save stores it to a wav file + + creates the output subdirectory, opens an audio stream, records audio and saves + each block to the path, records until interrupted + """ + os.makedirs(OUTPUT_FOLDER, exist_ok=True) + # print(sd.query_devices()) + try: + with sd.InputStream( + samplerate=SAMPLE_RATE, + channels=CHANNELS, + dtype=DTYPE, + blocksize=BLOCKSIZE, + ) as stream: + while True: + audio, overflowed = stream.read(BLOCKSIZE) + + filename = os.path.join( + OUTPUT_FOLDER, + "testaudio.wav", + ) + save_wav(filename, audio) + print(f"Saved: {filename}") + + except KeyboardInterrupt: + print("\nStopped.") + + +if __name__ == "__main__": + main() diff --git a/code/arlab_whisper/config/whisper.yaml b/code/arlab_whisper/config/whisper.yaml new file mode 100644 index 00000000..935a1bdb --- /dev/null +++ b/code/arlab_whisper/config/whisper.yaml @@ -0,0 +1,40 @@ + +whisper: + ros__parameters: + + "wakewords": + - robot + - assistant + - computer + + use_zirbi_wakewords: true + + "zirbi_wakewords": + - sylvie + - serbian + - serbi + - serby + - zybi + - zerbi + - zirbi + + # tiny, base, small, medium, large-v3, large-v3-turbo + wakeword_model: "small" + command_model: "large-v3-turbo" + + device: "cuda" + compute_type: "int8" #better than int8 + + sample_rate: 32000 #16000 is the max resolution for whisper + channels: 1 + + #audio command length + rolling_buffer_seconds: 2.75 + command_record_seconds: 3.5 + + #detect speech audio threshold + speech_threshold: 75 + + check_interval: 0.25 + language: "en" + beam_size: 5 \ No newline at end of file diff --git a/code/arlab_whisper/launch/whisper.launch.py b/code/arlab_whisper/launch/whisper.launch.py new file mode 100644 index 00000000..436718ff --- /dev/null +++ b/code/arlab_whisper/launch/whisper.launch.py @@ -0,0 +1,26 @@ +from launch import LaunchDescription +from launch_ros.actions import Node +from ament_index_python.packages import get_package_share_directory +import os + + +def generate_launch_description(): + + # import the yaml parameters + config = os.path.join( + get_package_share_directory("arlab_whisper"), + "config", + "whisper.yaml", + ) + + return LaunchDescription( + [ + Node( + package="arlab_whisper", + executable="whisper", + name="whisper", + output="screen", + parameters=[config], + ), + ] + ) diff --git a/code/arlab_whisper/package.xml b/code/arlab_whisper/package.xml new file mode 100644 index 00000000..dd4123aa --- /dev/null +++ b/code/arlab_whisper/package.xml @@ -0,0 +1,18 @@ + + + + arlab_whisper + 0.0.0 + TODO: Package description + arlab + Apache-2.0 + + ament_copyright + ament_flake8 + ament_pep257 + python3-pytest + + + ament_python + + diff --git a/code/arlab_whisper/requirements.txt b/code/arlab_whisper/requirements.txt new file mode 100644 index 00000000..249571ab --- /dev/null +++ b/code/arlab_whisper/requirements.txt @@ -0,0 +1,3 @@ +numpy +sounddevice +faster_whisper diff --git a/code/arlab_whisper/resource/arlab_whisper b/code/arlab_whisper/resource/arlab_whisper new file mode 100644 index 00000000..e69de29b diff --git a/code/arlab_whisper/setup.cfg b/code/arlab_whisper/setup.cfg new file mode 100644 index 00000000..a29e3a40 --- /dev/null +++ b/code/arlab_whisper/setup.cfg @@ -0,0 +1,4 @@ +[develop] +script_dir=$base/lib/arlab_whisper +[install] +install_scripts=$base/lib/arlab_whisper diff --git a/code/arlab_whisper/setup.py b/code/arlab_whisper/setup.py new file mode 100644 index 00000000..314d5d3a --- /dev/null +++ b/code/arlab_whisper/setup.py @@ -0,0 +1,32 @@ +from setuptools import find_packages, setup + +package_name = "arlab_whisper" + +setup( + name=package_name, + version="0.0.1", + packages=find_packages(exclude=["test"]), + data_files=[ + ("share/ament_index/resource_index/packages", ["resource/" + package_name]), + ("share/" + package_name, ["package.xml"]), + ("share/" + package_name + "/config", ["config/whisper.yaml"]), + ("share/" + package_name + "/launch", ["launch/whisper.launch.py"]), + ], + install_requires=["setuptools"], + zip_safe=True, + maintainer="arlab", + maintainer_email="tobias.neher@uni-a.de", + description="This package allows speech recognition using Faster-Whisper with wakeword detection. " + "It continuously monitors microphone input, detects configurable wakewords, transcribes them and publishes the result", + license="Apache-2.0", + extras_require={ + "test": [ + "pytest", + ], + }, + entry_points={ + "console_scripts": [ + "whisper = arlab_whisper.whisper_node:main", + ], + }, +)