Skip to content

Commit

Permalink
feat(listener): add local intents via api
Browse files Browse the repository at this point in the history
local actions for the speaker, such as bluetooth pair, pause, volume
currently in English, Spanish and Catalan (few basic examples)

this processing is done via API since it already loads into memory,
otherwise running a new Python script would use more start time.
  • Loading branch information
duhow committed Jan 3, 2025
1 parent 41110fa commit 1715346
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 1 deletion.
57 changes: 57 additions & 0 deletions api/intents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from flask import Blueprint, request, jsonify
import yaml
import os

intents = Blueprint('intents', __name__)

intents_file = os.path.join(os.path.dirname(__file__), 'intents.yaml')
intents_data = yaml.load(open(intents_file, 'r'), Loader=yaml.FullLoader)

def remove_accents(input: str) -> str:
characters = {
'á': 'a', 'é': 'e', 'í': 'i', 'ó': 'o', 'ú': 'u',
'à': 'a', 'è': 'e', 'ì': 'i', 'ò': 'o', 'ù': 'u',
}

for char, repl in characters.items():
input = input.replace(char, repl)
return input

@intents.post('/intent')
def get_intents():
text = None
if request.is_json:
data = request.get_json()
text = data.get('text', '').strip()
else:
text = request.form.get('text', '').strip()
if not text:
return jsonify({'error': 'No text provided'}), 400

accepts_json = request.headers.get('Accept') == 'application/json'

text = text.lower()
text = remove_accents(text)
for char in ['.', ',', '?', '!', ':', ';']:
text = text.replace(char, '')

for word, replacements in intents_data.get('replaces', {}).items():
for replacement in replacements:
if text == replacement:
text = word
break
elif f' {replacement} ' in text:
text = text.replace(f' {replacement} ', f' {word} ')
elif f'{replacement} ' in text:
text = text.replace(f'{replacement} ', f'{word} ')
elif text.endswith(replacement):
text = text[:len(text)-len(replacement)] + word

for entry in intents_data['intents']:
if text in entry['sentences']:
if accepts_json:
return jsonify({'intent': entry['action']})
return entry['action'] + '\n'
if accepts_json:
return jsonify({'intent': ''}), 404
return "", 404
94 changes: 94 additions & 0 deletions api/intents.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
replaces:
bluetooth:
- bluetooh
- el blog
- blog
- brutus
- bruto
- lotus
- amb l'atom
desconecta:
- desconec
- desco
- desconnect
- desconnecte

intents:
- action: BluetoothPair
sentences:
- pair bluetooth
- pair bluetooth device
- connect bluetooth
- connect bluetooth device
- connect to my phone
# --
- pareja bluetooth
- en pareja bluetooth
- empareja bluetooth
- empareja dispositivo bluetooth
- empareja mi movil
- conecta bluetooth
- conectate a mi movil
- conecta dispositivo bluetooth
- busca bluetooth
- buscar bluetooth
# --
- connectar bluetooth
- emparella bluetooth
- en parella bluetooth
- amb parella bluetooth
- action: BluetoothDisconnect
sentences:
- disconnect
- disconnect bluetooth
# --
- desconecta
- desconectar
- desconectate
- desconecta bluetooth
- desconectar bluetooth
- desconecta mi movil
# --
- desconnecta
- desconnectar
- desconnectat
- action: Pause
sentences:
- pause
- stop
- pausa
- pausar
- paus
- para
- para la musica
- action: Next
sentences:
- next
- siguiente
- siguiente canción
- siguiente pista
- pasa a la siguiente
- action: VolumeUp
sentences:
- volume up
- sube
- sube volumen
- subir
- subir volumen
- puja
- puja volum
- pujar
- pujar volum
- action: VolumeDown
sentences:
- volume down
- baja
- baja volumen
- baja la voz
- bajar
- bajar volumen
- baix
- baixa
- baixa volum
- baixar
- baixar volum
4 changes: 4 additions & 0 deletions api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@
from utils import get_ip_address, get_wifi_mac_address, get_bt_mac_address, get_device_id, get_uptime, get_load_avg, get_memory_usage, get_volume, set_volume
import const

from intents import intents

hostname = os.uname()[1]
speaker_ip = get_ip_address('wlan0')
app = Flask(__name__)

app.register_blueprint(intents)

config = ConfigManager(const.config_listener)
config_tts = ConfigManager(const.config_tts)
system_version = ConfigUci(const.mico_version)
Expand Down
4 changes: 3 additions & 1 deletion api/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Flask>=3
Flask-APScheduler==1.13.1
Flask-MQTT==1.2.1
#Flask-MQTT==1.2.1

requests>2,<3
certifi>=2024
Expand All @@ -14,3 +14,5 @@ wyoming==1.6.0
#numpy<2

pyring-buffer

pyyaml
38 changes: 38 additions & 0 deletions packages/porcupine/config/launcher
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ RECORDING_CHANNEL=1
VOLUME_THRESHOLD=10
VOLUME_DURING_STT=10
SOX_SILENCE_ARGS="1 0.2 1% 0.5 1.2 1%"
API_AVAILABLE=$(/etc/init.d/api status >/dev/null && echo 1 || echo 0)

CONFIG_FILE=/data/listener

Expand Down Expand Up @@ -141,6 +142,29 @@ get_stt_settings(){
#echo "end $(date)"
}

intent_run(){
if [ "$1" = "BluetoothPair" ] ; then
if bluetoothctl list | grep -q .; then
/bin/bluetooth_pair &
fi
elif [ "$1" = "BluetoothDisconnect" ] ; then
BT_STATUS=$(timeout 1 bluetoothctl player.show | grep -i status | awk '{print $2}')
if [ -n "$BT_STATUS" ]; then
timeout 8 bluetoothctl disconnect &
miplay sound shutdown
fi
elif [ "$1" = "Pause" ] ; then
/bin/play_button
elif [ "$1" = "VolumeUp" ] ; then
# HACK! We are restoring volume after STT
#/bin/volume +10
SAVED_VOL=$((SAVED_VOL + 10))
elif [ "$1" = "VolumeDown" ] ; then
#/bin/volume -15
SAVED_VOL=$((SAVED_VOL - 15))
fi
}

which arecord &>/dev/null && {
RECORD_COMMAND="arecord -N -D$MIC -d $TIME -f S16_LE -c $RECORDING_CHANNEL -r $RECORDING_RATE -"
}
Expand Down Expand Up @@ -196,6 +220,8 @@ log "activated"
ubus send wakeword '{"name": "'${WORD}'"}'

ERROR_LISTENER=0
INTENT=
INTENT_SUCCESS=
SAVED_VOL=`current_volume`
# lower volume EXCEPT for notifications
if [ "$SAVED_VOL" -gt ${VOLUME_THRESHOLD} ]; then
Expand Down Expand Up @@ -246,6 +272,17 @@ if [ "${STT_SUCCESS}" = 1 ]; then
# NOTE: if empty text received, there may be some error with recording or other.
miplay sound notice
else
if [ "${API_AVAILABLE}" = "1" ]; then
# Attempt to check for local intents before asking HA
INTENT=$(curl -fs -XPOST -F "text=${STT_TEXT}" localhost/intent)
INTENT_SUCCESS=$?
if [ "$INTENT_SUCCESS" -eq 0 ] && [ -n "$INTENT" ]; then
echo "intent found: $INTENT"
log "intent: ${INTENT}"
intent_run $INTENT
fi
fi
if [ "${INTENT_SUCCESS}" != 0 ]; then
CONVERSATION_RESPONSE=$(curl \
-H "Authorization: Bearer ${HA_TOKEN}" \
-H "Content-Type: application/json" \
Expand All @@ -258,6 +295,7 @@ if [ "${STT_SUCCESS}" = 1 ]; then
fi
TTS_TEXT=$(echo "${CONVERSATION_RESPONSE}" | jq -r .response.speech.plain.speech)
${SPEAK} "${TTS_TEXT}"
fi # INTENT_SUCCESS
fi
else
log "error"
Expand Down

0 comments on commit 1715346

Please sign in to comment.