[go: up one dir, main page]

Skip to content

Commit

Permalink
Merge pull request muqiuhan#4 from X-FRI/main
Browse files Browse the repository at this point in the history
 [services]: add voice_recorder and speak_to_text
  • Loading branch information
muqiuhan authored Jun 25, 2024
2 parents 9aa6058 + ff393e0 commit ae25ef4
Show file tree
Hide file tree
Showing 16 changed files with 517 additions and 79 deletions.
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ __pycache__/

poetry.lock

.venv/
.venv/

/autumnbot/services/speak_to_text/vosk-model-small-cn-0.22

*.wav
70 changes: 51 additions & 19 deletions autumnbot/autumnbot/main.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 @@ -26,34 +26,66 @@
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import cv2
import typing
import pykka

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[typing.Type[Service]] = {CameraSaver}
SERVICES: set[Type[Service]] = {
# CameraSaver,
SpeakToText,
VoiceRecorder,
}


def camera_saver_example(service_manager: ServiceManager) -> None:
import cv2

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()


def voice_recorder_example(service_manager: ServiceManager) -> None:
import os

voice_recorder = service_manager.get_started_service(VoiceRecorder)
speak_to_text = service_manager.get_started_service(SpeakToText)

if voice_recorder is not None and speak_to_text is not None:
pre_voice = ""
while True:
voice = voice_recorder.ask({})
if voice == pre_voice:
continue
else:
pre_voice = voice
text = cast(str, speak_to_text.ask(voice))
if "退" in text and "出" in text:
break
os.remove(cast(str, voice))


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

try:
camera = service_manager.get_started_service(CameraSaver)
if camera is not None:
camera = typing.cast(pykka.ActorRef[typing.Any], camera)
img = typing.cast(typing.Optional[cv2.Mat], camera.ask({}))
if img is not None:
cv2.imshow("test", img)
cv2.waitKey(0)
# await camera_saver_example(service_manager)
# await speak_to_text_example(service_manager)
voice_recorder_example(service_manager)
finally:
service_manager.stop_all_service()
service_manager.stop_all_services()


def main() -> None:
example()
example()
31 changes: 31 additions & 0 deletions autumnbot/preimport/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# 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 typing import Any, Optional, Union, cast, Type
from pykka import ActorRef, ThreadingActor
from abc import ABC as AbstractClass, abstractmethod
3 changes: 2 additions & 1 deletion autumnbot/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ readme = "README.md"
[tool.poetry.dependencies]
python = "^3.11"
opencv-python = "^4.10.0.84"

pykka = "^4.0.2"
structlog = "^24.2.0"
pyaudio = "^0.2.14"
wave = "^0.0.2"
vosk = "^0.3.45"
progress = "^1.6"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
Expand Down
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.
10 changes: 5 additions & 5 deletions autumnbot/services/camera_saver/camera_saver.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,14 @@
# 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
class CameraSaver(service.Service):
class_name = "CameraSaver"
CLASS_NAME = "CameraSaver"
camera0: cv2.VideoCapture = cv2.VideoCapture(0)

def __init__(self) -> None:
Expand All @@ -45,15 +45,15 @@ 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]:
self.info("Request to obtain the current camera picture")
def on_receive(self, message: Any) -> Optional[cv2.typing.MatLike]:
self.info("request to obtain the current camera picture")
ret, frame = self.camera0.read()

if ret:
self.info("Get camera image")
return frame
else:
self.error("Unable to get camera image")
self.error("unable to get camera image")
return None

def on_stop(self) -> None:
Expand Down
15 changes: 7 additions & 8 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):
module_name = "service"
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
57 changes: 27 additions & 30 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,77 +29,74 @@
from services.service import Service

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


# Dynamically manage AutumnBot services
class ServiceManager(utils.logging.Logging):
module_name: str = "service"
class_name: str = "ServiceManager"
MODULE_NAME: str = "service"
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 started 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:
self.start_service(service)

# Start all services at once
def start_all_service(self) -> None:
def start_all_services(self) -> None:
self.info("start all services")
for service_name in self.services:
self.start_service(service_name)

# 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_service(self) -> None:
def stop_all_services(self) -> None:
self.info("stop all services")
for service_name in self.services:
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("The service {} is not started.".format(service))
self.error("service {} is not started.".format(service.CLASS_NAME))
Loading

0 comments on commit ae25ef4

Please sign in to comment.