[go: up one dir, main page]

Skip to content

Commit

Permalink
[services]: add voice_recorder
Browse files Browse the repository at this point in the history
  • Loading branch information
muqiuhan committed Jun 25, 2024
1 parent d758c3f commit 61fdb1b
Show file tree
Hide file tree
Showing 10 changed files with 294 additions and 52 deletions.
47 changes: 37 additions & 10 deletions autumnbot/autumnbot/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,37 +28,64 @@

from services.speak_to_text.speak_to_text import SpeakToText
from services.camera_saver.camera_saver import CameraSaver
from services.voice_recorder.voice_recorder import VoiceRecorder
from services.service import Service
from services.service_manager import ServiceManager
from preimport import *

# These are all services of AutumnBot
SERVICES: set[Type[Service]] = {CameraSaver, SpeakToText}
SERVICES: set[Type[Service]] = {
# CameraSaver,
# SpeakToText,
VoiceRecorder
}


def camera_saver_example(service_manager: ServiceManager) -> None:
async def camera_saver_example(service_manager: ServiceManager) -> None:
import cv2
from time import sleep

camera = service_manager.get_started_service(CameraSaver)
if camera is not None:
camera = cast(ActorRef[Any], camera)
img = cast(Optional[cv2.typing.MatLike], camera.ask({}))
camera_saver = service_manager.get_started_service(CameraSaver)
if camera_saver is not None:
camera_saver = cast(ActorRef[Any], camera_saver)
img = cast(Optional[cv2.typing.MatLike], camera_saver.ask({}))

if img is not None:
cv2.imshow("test.png", img)
cv2.waitKey()


async def speak_to_text_example(service_manager: ServiceManager) -> None:
import os

speak_to_text = service_manager.get_started_service(SpeakToText)
if speak_to_text is not None:
speak_to_text = cast(ActorRef[Any], speak_to_text)
speak_to_text.ask(os.path.join(os.path.dirname(__file__), "test.wav"))


async def voice_recorder_example(service_manager: ServiceManager) -> None:
voice_recorder = service_manager.get_started_service(VoiceRecorder)

if voice_recorder is not None:
while True:
frames = voice_recorder.ask({})
if frames is not []: print(frames)


# An example of managing and using all services through ServiceManager
def example() -> None:
async def example() -> None:
service_manager = ServiceManager(SERVICES)
service_manager.start_all_services()

try:
camera_saver_example(service_manager)
# await camera_saver_example(service_manager)
# await speak_to_text_example(service_manager)
await voice_recorder_example(service_manager)
finally:
service_manager.stop_all_services()


def main() -> None:
example()
import asyncio

asyncio.run(example())
2 changes: 1 addition & 1 deletion autumnbot/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
10 changes: 5 additions & 5 deletions autumnbot/services/camera_saver/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Copyright (c) 2024 Muqiu Han
#
#
# All rights reserved.
#
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
Expand All @@ -13,7 +13,7 @@
# * Neither the name of AutumnBot nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
Expand All @@ -24,4 +24,4 @@
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
4 changes: 2 additions & 2 deletions autumnbot/services/camera_saver/camera_saver.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import typing
import cv2
from .. import service
from preimport import *


# Capture a frame from the camera
Expand All @@ -45,7 +45,7 @@ def on_start(self) -> None:
return super().on_start()

# Returns a frame of the camera, or None if an error occurs
def on_receive(self, message: typing.Any) -> typing.Optional[cv2.typing.MatLike]:
def on_receive(self, message: Any) -> Optional[cv2.typing.MatLike]:
self.info("request to obtain the current camera picture")
ret, frame = self.camera0.read()

Expand Down
13 changes: 6 additions & 7 deletions autumnbot/services/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,25 +27,24 @@
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import utils.logging
import pykka
import typing
from preimport import *


class Service(pykka.ThreadingActor, utils.logging.Logging):
class Service(ThreadingActor, utils.logging.Logging):
module_name = "service"

def __init__(self) -> None:
super().__init__()

def on_failure(
self,
exception_type: typing.Optional[type[BaseException]],
exception_value: typing.Optional[BaseException],
traceback: typing.Optional[typing.Any],
exception_type: Optional[type[BaseException]],
exception_value: Optional[BaseException],
traceback: Optional[Any],
) -> None:
return super().on_failure(exception_type, exception_value, traceback)

def on_receive(self, message: typing.Any) -> typing.Any:
def on_receive(self, message: Any) -> Any:
self.info("receive")
return super().on_receive(message)

Expand Down
47 changes: 21 additions & 26 deletions autumnbot/services/service_manager.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Copyright (c) 2024 Muqiu Han
#
#
# All rights reserved.
#
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
Expand All @@ -13,7 +13,7 @@
# * Neither the name of AutumnBot nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
Expand All @@ -29,8 +29,7 @@
from services.service import Service

import utils.logging
import typing
import pykka
from preimport import *


# Dynamically manage AutumnBot services
Expand All @@ -39,35 +38,31 @@ class ServiceManager(utils.logging.Logging):
class_name: str = "ServiceManager"

# Initial service collection (not started)
services: set[typing.Type[Service]]
services: set[Type[Service]]

# A started service can access its started ActorRef through itself
started_services: dict[typing.Type[Service], pykka.ActorRef[typing.Any]] = {}
started_services: dict[Type[Service], ActorRef[Any]] = {}

def __init__(self, services: set[typing.Type[Service]]) -> None:
def __init__(self, services: set[Type[Service]]) -> None:
self.info("initialize")
self.services = services

# Get unstarted services, return None if the service does not exist
def __get_service(
self, service: typing.Type[Service]
) -> typing.Optional[typing.Type[Service]]:
def __get_service(self, service: Type[Service]) -> Optional[Type[Service]]:
return next((s for s in self.services if s == service), None)

# Get the started service, return None if the service does not exist
def get_started_service(
self, service: typing.Type[Service]
) -> typing.Optional[pykka.ActorRef[typing.Any]]:
self.info("get service {}".format(service))
def get_started_service(self, service: Type[Service]) -> Optional[ActorRef[Any]]:
self.info("get service {}".format(service.class_name))

try:
return self.started_services[service]
except KeyError:
self.error("The service {} is not started.".format(service))
self.error("The service {} is not started.".format(service.class_name))
return None

# Add a service without starting it, If now = True, start immediately。
def add_service(self, service: typing.Type[Service], now: bool = False) -> None:
def add_service(self, service: Type[Service], now: bool = False) -> None:
self.services.add(service)

if now:
Expand All @@ -81,16 +76,16 @@ def start_all_services(self) -> None:

# Start a service that has not been started. If the service is already started, it will do nothing.
# NOTE: If the service doesn't exist, something strange might be going on :(
def start_service(self, service: typing.Type[Service]) -> None:
self.info("start service {}".format(Service))
def start_service(self, service: Type[Service]) -> None:
self.info("start service {}".format(service.class_name))

service_will_be_started = self.__get_service(service)
if service_will_be_started is not None:
self.started_services[service] = typing.cast(
typing.Type[Service], service_will_be_started
self.started_services[service] = cast(
Type[Service], service_will_be_started
).start()
else:
self.warn("unable to start service {}".format(service))
self.warn("unable to start service {}".format(service.class_name))

# Stop all services at once
def stop_all_services(self) -> None:
Expand All @@ -99,9 +94,9 @@ def stop_all_services(self) -> None:
self.stop_service(service_name)

# Stop a service that has been started. If the service is not started, it will do nothing.
def stop_service(self, service: typing.Type[Service]) -> None:
self.info("stop service {}".format(Service))
def stop_service(self, service: Type[Service]) -> None:
self.info("stop service {}".format(service.class_name))
try:
self.started_services.pop(service).stop()
except KeyError:
self.error("service {} is not started.".format(service))
self.error("service {} is not started.".format(service.class_name))
91 changes: 91 additions & 0 deletions autumnbot/services/speak_to_text/speak_to_text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Copyright (c) 2024 Muqiu Han
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of AutumnBot nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

from .. import service
from preimport import *
import json
import wave
import vosk
import os


# Chinese speech to text is currently implemented using Vosk and uses a relatively small embedded vosk model.
# Download the model from here and modify __model to customize the model: https://alphacephei.com/vosk/models
class SpeakToText(service.Service):
__model: vosk.Model
class_name: str = "SpeakToText"

def __init__(self) -> None:
self.info("initialize")
super().__init__()
vosk.SetLogLevel(level=-1)

def on_start(self) -> None:
self.info("start")
try:
self.__model = vosk.Model(
model_path=os.path.join(
os.path.dirname(__file__), "vosk-model-small-cn-0.22"
)
)
except Exception:
return
finally:
return super().on_start()

# Returns a frame of the camera, or None if an error occurs
# The message is the path of voice
def on_receive(self, message: str) -> Optional[str]:
self.info("Request speak to text")

wav_file: wave.Wave_read = wave.open(message, "rb")
if (
wav_file.getnchannels() != 1
or wav_file.getsampwidth() != 2
or wav_file.getcomptype() != "NONE"
):
self.error("Audio file must be WAV format mono PCM.")
return None

rec = vosk.KaldiRecognizer(self.__model, wav_file.getframerate())
rec.SetWords(True)
rec.SetPartialWords(True)

self.info("trying to parse the voice file...")
while True:
try:
if rec.AcceptWaveform(wav_file.readframes(4000)):
text = json.loads(rec.Result())["text"]
self.info("speak to text: {}".format(text))
return text
except Exception as e:
self.error("unable to parse the voice file: {}".format(e))
return None

def on_stop(self) -> None:
self.info("stop")
27 changes: 27 additions & 0 deletions autumnbot/services/voice_recorder/__init.__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Copyright (c) 2024 Muqiu Han
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of AutumnBot nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Loading

0 comments on commit 61fdb1b

Please sign in to comment.