diff --git a/api/intents.py b/api/intents.py new file mode 100644 index 0000000..a0eeff4 --- /dev/null +++ b/api/intents.py @@ -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 \ No newline at end of file diff --git a/api/intents.yaml b/api/intents.yaml new file mode 100644 index 0000000..8a4c09d --- /dev/null +++ b/api/intents.yaml @@ -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 \ No newline at end of file diff --git a/api/main.py b/api/main.py index b7d20eb..67b228c 100644 --- a/api/main.py +++ b/api/main.py @@ -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) diff --git a/api/requirements.txt b/api/requirements.txt index b01cf64..2da3f25 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -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 @@ -14,3 +14,5 @@ wyoming==1.6.0 #numpy<2 pyring-buffer + +pyyaml diff --git a/packages/porcupine/config/launcher b/packages/porcupine/config/launcher index cdbc2a0..5458242 100755 --- a/packages/porcupine/config/launcher +++ b/packages/porcupine/config/launcher @@ -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 @@ -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 -" } @@ -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 @@ -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" \ @@ -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"