Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add voice timer support #168

Merged
merged 4 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ tmp/
build
htmlcov

/.venv/
.venv/
.mypy_cache/
__pycache__/

Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## 1.3.0

- Bump to wyoming 1.5.4 (timers)
- Add support for voice timers
- Add `--timer-finished-wav` and `--timer-finished-wav-repeat`
- Add `--timer-started-command`
- Add `--timer-updated-command`
- Add `--timer-cancelled-command`
- Add `--timer-finished-command`

## 1.2.0

- Add `--tts-played-command`
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,12 @@ You can play a WAV file when the wake word is detected (locally or remotely), an

* `--awake-wav <WAV>` - played when the wake word is detected
* `--done-wav <WAV>` - played when the voice command is finished
* `--timer-finished-wav <WAV>` - played when a timer is finished

If you want to play audio files other than WAV, use [event commands](#event-commands). Specifically, the `--detection-command` to replace `--awake-wav` and `--transcript-command` to replace `--done-wav`.

The timer finished sound can be repeated with `--timer-finished-wav-repeat <repeats> <delay>` where `<repeats>` is the number of times to repeat the WAV, and `<delay>` is the number of seconds to wait between repeats.

## Audio Enhancements

Install the dependencies for webrtc:
Expand Down Expand Up @@ -179,5 +182,9 @@ Satellites can respond to events from the server by running commands:
* `--error-command` - an error was sent from the server (text on stdin)
* `--connected-command` - satellite connected to server
* `--disconnected-command` - satellite disconnected from server
* `--timer-started-command` - new timer has started (json on stdin)
* `--timer-updated-command` - timer has been paused/unpaused or has time added/removed (json on stdin)
* `--timer-cancelled-command` - timer has been cancelled (timer id on stdin)
* `--timer-finished-command` - timer has finished (timer id on stdin)

For more advanced scenarios, use an event service (`--event-uri`). See `wyoming_satellite/example_event_client.py` for a basic client that just logs events.
1 change: 1 addition & 0 deletions examples/websocket-service/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
websockets==12.0
100 changes: 100 additions & 0 deletions examples/websocket-service/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
import argparse
import asyncio
import json
import logging
from functools import partial
from typing import Optional

import websockets
from wyoming.event import Event
from wyoming.server import AsyncEventHandler, AsyncServer

_LOGGER = logging.getLogger()


async def main() -> None:
"""Main entry point."""
parser = argparse.ArgumentParser()
parser.add_argument("--uri", required=True, help="unix:// or tcp://")
parser.add_argument("--websocket-host", default="localhost")
parser.add_argument("--websocket-port", type=int, default=8675)
#
parser.add_argument("--debug", action="store_true", help="Log DEBUG messages")
args = parser.parse_args()

logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO)
_LOGGER.debug(args)

_LOGGER.info("Ready")

# Start server
server = AsyncServer.from_uri(args.uri)
queue: "asyncio.Queue[Optional[Event]]" = asyncio.Queue()

try:
async with websockets.serve(
partial(websocket_connected, queue),
args.websocket_host,
args.websocket_port,
):
await server.run(partial(WebsocketEventHandler, args, queue))
finally:
queue.put_nowait(None)


# -----------------------------------------------------------------------------


async def websocket_connected(queue: "asyncio.Queue[Optional[Event]]", websocket):
try:
while True:
event = await queue.get()
if event is None:
# Stop signal
break

await websocket.send(
json.dumps(
{"type": event.type, "data": event.data or {}}, ensure_ascii=False
)
)
except websockets.ConnectionClosed:
pass
except Exception:
_LOGGER.exception("Error in websocket handler")


# -----------------------------------------------------------------------------


class WebsocketEventHandler(AsyncEventHandler):
"""Event handler for clients."""

def __init__(
self,
cli_args: argparse.Namespace,
queue: "asyncio.Queue[Optional[Event]]",
*args,
**kwargs,
) -> None:
super().__init__(*args, **kwargs)

self.cli_args = cli_args
self.queue = queue

async def handle_event(self, event: Event) -> bool:
_LOGGER.debug(event)
self.queue.put_nowait(event)

return True


# -----------------------------------------------------------------------------


if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
pass
Binary file added examples/websocket-service/timer_finished.wav
Binary file not shown.
244 changes: 244 additions & 0 deletions examples/websocket-service/timers.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
<!DOCTYPE html>
<html lang="en">

<head>

<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="Wyoming satellite timer example">
<meta name="author" content="Michael Hansen">

<title>Wyoming Timer Example</title>
<style>
body {
background-color: black;
}

.timer-container {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}

.timer {
display: flex;
width: 100px;
height: auto;
flex-wrap:wrap;
justify-content: center;
align-items: center;
background-color: #03a9f4;
padding: 15px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
color: white;
margin-right: 20px
}

.time {
font-size: 20px;
}

.message {
text-align: center;
width: 90px;
font-size: 24px;
font-weight: bold;
}

.finished {
background-color: red;
}

.paused {
background-color: yellow;
color: black;
}

.blink {
animation: blink-animation 1s linear infinite;
-webkit-animation: blink-animation 1s linear infinite;
}
@keyframes blink-animation {
0%, 100% {opacity: 1;}
50% {opacity: 0;}
}
@-webkit-keyframes blink-animation {
0%, 100% {opacity: 1;}
50% {opacity: 0;}
}
</style>
</head>

<body>
<div id="timers" class="timer-container">
</div>
<audio id="audio-finished" hidden controls loop src="timer_finished.wav"></audio>

<script>
let socket = new WebSocket("ws://localhost:8675")
var timers = []
var finished_timers = 0

function q(selector) {return document.querySelector(selector)}

socket.onopen = function(e) {
console.log("Connected")
};

socket.onmessage = function(event) {
wyo_event = JSON.parse(event.data)
if (wyo_event.type == "timer-started") {
let timer_info = wyo_event.data
timer_info.is_active = true
console.log(timer_info)

timers.push(timer_info)

let timers_elem = q("#timers")
let timer_div = document.createElement("div")
timer_div.id = "timer-" + timer_info.id
timer_div.classList.add("timer")

let message_div = document.createElement("div")
message_div.classList.add("message")

if (timer_info.name) {
message_div.innerHTML = timer_info.name
} else {
if (timer_info.start_hours) {
message_div.innerHTML += timer_info.start_hours + " hr "
}
if (timer_info.start_minutes) {
message_div.innerHTML += timer_info.start_minutes + " min "
}
if (timer_info.start_seconds) {
message_div.innerHTML += timer_info.start_seconds + " sec "
}
}
timer_div.appendChild(message_div)

let time_div = document.createElement("div")
time_div.classList.add("time")

let hours = timer_info.start_hours ?? 0
let minutes = timer_info.start_minutes ?? 0
let seconds = timer_info.start_seconds ?? 0

time_div.innerHTML =
hours.toString().padStart(2, 0) + ":" +
minutes.toString().padStart(2, 0) + ":" +
seconds.toString().padStart(2, 0);

timer_div.appendChild(time_div)

timers_elem.appendChild(timer_div)

setTimeout(update_timer, 1000, timer_info)
}
else if (wyo_event.type == "timer-cancelled") {
let timer_info = wyo_event.data
timers = timers.filter(t => t.id != timer_info.id)
let timer_div = q("#timer-" + timer_info.id)
if (timer_div) {
timer_div.remove()
}
}
else if (wyo_event.type == "timer-updated") {
let timer_info = wyo_event.data
for (i = 0; i < timers.length; i++) {
if (timers[i].id == timer_info.id) {
timers[i].is_active = timer_info.is_active
timers[i].total_seconds = timer_info.total_seconds
break
}
}

let timer_div = q("#timer-" + timer_info.id)
if (timer_div) {
if (timer_info.is_active) {
timer_div.classList.remove("paused")
} else {
timer_div.classList.add("paused")
}
}
}
else if (wyo_event.type == "timer-finished") {
let timer_info = wyo_event.data
for (i = 0; i < timers.length; i++) {
if (timers[i].id == timer_info.id) {
timers[i].total_seconds = 0
finished_timers += 1
if (finished_timers == 1) {
q("#audio-finished").play()
}
break
}
}
timers = timers.filter(t => t.id != timer_info.id)
let timer_div = q("#timer-" + timer_info.id)
if (timer_div) {
timer_div.classList.add("finished")
timer_div.classList.add("blink")

timer_div.onclick = function() {
timer_div.remove()
finished_timers = Math.max(0, finished_timers - 1)
if (finished_timers == 0) {
let audio = q("#audio-finished")
audio.pause()
audio.currentTime = 0
}
};
}

let time_div = q("#timer-" + timer_info.id + " .time")
if (time_div) {
time_div.style.opacity = 0
}
}
};

socket.onclose = function(event) {
console.log("Disconnected")
};

socket.onerror = function(error) {
};

function update_timer(timer_info) {
if (timer_info.total_seconds < 0) {
return
}

let time_div = q("#timer-" + timer_info.id + " .time")
if (!time_div) {
return
}

if (!timer_info.is_active) {
// Paused
setTimeout(update_timer, 1000, timer_info)
return
}

if (timer_info.total_seconds > 0) {
timer_info.total_seconds -= 1
}
let total_minutes = Math.floor(timer_info.total_seconds / 60)

let hours = Math.floor(total_minutes / 60)
let minutes = total_minutes % 60
let seconds = timer_info.total_seconds % 60

time_div.innerHTML =
hours.toString().padStart(2, 0) + ":" +
minutes.toString().padStart(2, 0) + ":" +
seconds.toString().padStart(2, 0)

setTimeout(update_timer, 1000, timer_info)
};
</script>
</body>
</html>
Loading
Loading