Skip to content

Commit

Permalink
Adding more Sage features!
Browse files Browse the repository at this point in the history
  • Loading branch information
QuinnDamerell committed Jan 15, 2025
1 parent ade4977 commit 7445e6c
Show file tree
Hide file tree
Showing 4 changed files with 44 additions and 13 deletions.
2 changes: 1 addition & 1 deletion homeway/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,4 @@ image: ghcr.io/homewayio/homeway/{arch}
# Note when this version number changes, we must make a release to start a docker container build immediately, since HA will start looking for the new version.
# Basically: Make the final commit -> test and check lint actions (if a docker change, push to docker-test to ensure it builds) -> bump the version number -> create GitHub release.
# UPDATE THE CHANGE LOG!
version: 2.1.2
version: 2.1.3
5 changes: 0 additions & 5 deletions homeway/homeway/httprequest.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,11 +259,6 @@ def MakeHttpCallStreamHelper(logger:logging.Logger, httpInitialContext:HttpIniti

# Handle special API type targets.
if httpInitialContext.ApiTarget() == HaApiTarget.Core:
# We only allow two API to prevent this from being abused.
# Even though only the service can set this flag, isolating what can be called here is a layer of security.
if path != "/api/google_assistant" and path != "/api/alexa/smart_home":
raise Exception("A HA core api targeted call was made, but the path is not allowed. "+path)

# We need to get the access token and the correct server path, depending on if we are running in the addon container or not.
serverInfoHandler = Compat.GetServerInfoHandler()
if serverInfoHandler is None:
Expand Down
2 changes: 1 addition & 1 deletion homeway/homeway_linuxhost/ha/eventhandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class EventHandler:

# How long we will wait from the first request to send in a period a collapsed batch of events.
# This allows one off changes to be more responsive.
c_RequestCollapseDelayTimeSecFirstSend = 0.1
c_RequestCollapseDelayTimeSecFirstSend = 0.5

# How often we will reset the dict keeping track of spammy events.
c_SpammyEntityResetWindowSec = 60.0 * 30 # 30 minutes
Expand Down
48 changes: 42 additions & 6 deletions homeway/homeway_linuxhost/sage/sagehandler.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import time
import json
import logging
import threading
import requests

from wyoming.asr import Transcript, Transcribe
Expand All @@ -27,6 +28,12 @@ class SageHandler(AsyncEventHandler):
# This is more of a sanity check, the server will enforce it's own limits.
c_MaxStringLength = 500

# A quick static cache of the info event, since it's gets called in rapid succession sometimes.
c_InfoEventMaxCacheTimeSec = 15.0
s_InfoEvent:Info = None
s_InfoEventTime:float = 0.0
s_InfoEventLock = threading.Lock()


# Note this handler is created for each new request flow, like for a full Listen session.
def __init__(self, logger:logging.Logger, fabric:Fabric, fiberManger:FiberManager, homeContext:HomeContext, sageHistory:SageHistory, sagePrefix_CanBeNone:str, devLocalHomewayServerAddress_CanBeNone:str, *args, **kwargs) -> None:
Expand Down Expand Up @@ -201,6 +208,20 @@ def _EnforceMaxStringLength(self, text:str) -> str:
# Queries the service for the models and details we current have to offer.
async def _HandleDescribe(self) -> bool:

# Since the discovery command is called rapidly sometimes, we will cache the info event for a few seconds.
try:
info:Info = None
with SageHandler.s_InfoEventLock:
if SageHandler.s_InfoEvent is not None and time.time() - SageHandler.s_InfoEventTime < SageHandler.c_InfoEventMaxCacheTimeSec:
info = SageHandler.s_InfoEvent

if info is not None:
self.Logger.debug("Returning a cached info event for Discovery.")
await self._CacheAndWriteInfoEvent(info, False)
return True
except Exception as e:
Sentry.Exception("Sage - _HandleDescribe failed to write the cached info object, we will try for a new info object.", e)

# Note for some reason the name of the AsrProgram is what will show up in the discovery for users.
# Get the current programs / models / voices from the service.
try:
Expand All @@ -219,7 +240,10 @@ async def _HandleDescribe(self) -> bool:
self.Logger.debug(f"Sage - Starting Info Service Request - {url}")
response = requests.get(url, timeout=5)
if response.status_code == 200:
await self._HandleServiceInfoAndWriteEvent(response.json())
info = self._BuildInfoEvent(response.json())
if info is None:
raise Exception("Failed to build info event.")
await self._CacheAndWriteInfoEvent(info)
return True
self.Logger.warning(f"Sage - Failed to get models from service. Attempt: {attempt} - {response.status_code}")
except Exception as e:
Expand All @@ -238,9 +262,21 @@ async def _HandleDescribe(self) -> bool:
return False


# Writes the info event back to the client.
async def _CacheAndWriteInfoEvent(self, info:Info, cache:bool = True) -> None:
# Write it
await self.write_event(info.event())

if cache:
# After we successfully write the info event, we will cache it.
with SageHandler.s_InfoEventLock:
SageHandler.s_InfoEvent = info
SageHandler.s_InfoEventTime = time.time()


# Handles the service info and writes the event back to the client.
# This must write the info event or throw, so it will retry the process.
async def _HandleServiceInfoAndWriteEvent(self, response:dict) -> None:
def _BuildInfoEvent(self, response:dict) -> Info:

# This is a list of languages we support.
# We keep them here on the client side so they don't have to be sent every time.
Expand All @@ -264,7 +300,7 @@ def getAttribution(d:dict) -> Attribution:

def addSagePrefixIfNeeded(s:str) -> str:
if self.SagePrefix_CanBeNone is None:
return input
return s
return f"{self.SagePrefix_CanBeNone} - {s}"

# Parse the response.
Expand Down Expand Up @@ -334,7 +370,7 @@ def addSagePrefixIfNeeded(s:str) -> str:
info.tts.append(program)
for m in getOrThrow(p, "Options", list):
voice = TtsVoice(
name=addSagePrefixIfNeeded(getOrThrow(m, "Name", str)),
name=getOrThrow(m, "Name", str),
description=getOrThrow(m, "Description", str),
version=getOrThrow(m, "Version", str),
languages=getOrThrow(m, "Languages", list, sageLanguages),
Expand All @@ -344,5 +380,5 @@ def addSagePrefixIfNeeded(s:str) -> str:
voiceCount += 1
program.voices.append(voice)

self.Logger.info("Returning Sage Info to client. Voices: " + str(voiceCount))
await self.write_event(info.event())
self.Logger.info("Sage Info built. Voices: " + str(voiceCount))
return info

0 comments on commit 7445e6c

Please sign in to comment.