Skip to content

Commit

Permalink
Merge pull request #168 from rhasspy/synesthesiam-20240522-timers
Browse files Browse the repository at this point in the history
Add voice timer support
  • Loading branch information
synesthesiam authored May 24, 2024
2 parents 9b35e03 + 2824b65 commit 41f1075
Show file tree
Hide file tree
Showing 14 changed files with 494 additions and 10 deletions.
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

0 comments on commit 41f1075

Please sign in to comment.