How to count number of words in a spoken number?

I have a script which invokes a TTS message where I need to count the number of words in the message so it can calculate a delay to allow the message to finish being spoken before another TTS message is sent. The problem arises though when the message contains a number which is seen by the script as one word when it needs to be seen as multiple words to speak the number. I.e. “389” would be 5 words: “three hundred and eighty nine” but I can’t find a way to achieve that!
An example of a TTS “problem” message is “Your E V range is 389 kilometers” which is seen as 7 words (which adds a delay of 3 seconds) but needs to be seen as 11 words (“your e v range is three hundred and eight nine km”) which would add a delay of 6 seconds.

I either need to convert “389” in the message to words here:

data:
  message: Your E V range is {{ states('sensor.fordpass_elveh')|float(0) }} kilometres.
action: script.notify_alexa_media

or here:

alias: "Alexa: Notify Alexa Media Last Called"
description: Send notification to sensor.last_alexa
fields:
  message:
    description: The message content
    example: I am notifying you
sequence:
  - action: python_script.notify_alexa_media_last_called
    data:
      message: "{{ message }}"
  - delay:
      seconds: >-
        {{ (message.split(' ') | count *
        states('input_number.alexa_word_duration')|float ) | round(0, 'floor',
        default) }}
    alias: Delay for word count * 0.55s
mode: queued
icon: mdi:account-voice
max: 10

Any ideas? Thanks!

delay for two seconds then wait for trigger media player == idle, then continue with the next TTS

Thanks but while that might work with “normal” media players in HA, unfortunately the Alexa Media Player Echo devices do not update the state in real-time except when starting/stopping music. Volume level changes and media player play/pause/stop music triggers push commands from Amazon which triggers real-time updates but speaking TTS triggers nothing…

I see…

This gives you 10…

{{ ("Your E V range is 389 kilometers").split(" ") | count + ("Your E V range is 389 kilometers" | regex_findall("\d") | count)  }}
1 Like

Simple bit of logic :grin::

{% set x = states('sensor.fordpass_elveh')|int(0) %}
{{((1,(2,1)[x%10<1])[x>20],((5,4)[x//10%10==1],(4,2)[x%100<1])['0'in'%d'%x])[x>99]+6}}

Works for integers up to 1020km (do you really care about decimals?). Adds six for the rest of the phrase. Uses correct English (“three hundred and eighty-nine”).

1 Like

I ended up (with considerable assistance from an AI entity whose name I’m not supposed to mention here but starts with “C” and with whom I get along fabulously!) with this python_script:

# count_words.py
message = data.get("message", "")
result_string = ""

# Function to convert numbers to words
def number_to_words(n):
    ones = ["", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"]
    teens = ["", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen"]
    tens = ["", "ten", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"]

    if n == 0:
        return "zero"
    
    if n < 10:
        return ones[int(n)]
    elif 10 < n < 20:
        return teens[int(n) - 10]
    elif n < 100:
        return tens[int(n // 10)] + (" " + ones[int(n % 10)] if n % 10 != 0 else "")
    elif n < 1000:
        return ones[int(n // 100)] + " hundred" + (" and " + number_to_words(n % 100) if n % 100 != 0 else "")
    else:
        return str(n)  # Handle numbers beyond the scope

def decimal_to_words(num):
    whole, fraction = str(num).split(".")
    whole_words = number_to_words(int(whole))
    fraction_words = "point " + " ".join(number_to_words(int(digit)) for digit in fraction)
    return whole_words + " " + fraction_words

# Process the message
word_list = message.split()

for word in word_list:
    if word.replace('.', '', 1).isnumeric():  # Handle decimal numbers
        if "." in word:
            num_as_words = decimal_to_words(float(word))
        else:
            num_as_words = number_to_words(int(word))
        result_string += num_as_words + " "
    else:
        result_string += word + " "

# Remove trailing space
result_string = result_string.strip()

# Count words in the new string
word_count = len(result_string.split())

# Output the word count and debug information
output = {
    "word_count": word_count,
    "debug_message": f"Processed message: '{message}' to '{result_string}' with word count: {word_count}"
}

and my main HA script that uses it:

alias: "Alexa: Notify Alexa Media Last Called"
description: Send notification to sensor.last_alexa
fields:
  message:
    description: The message content
    example: I am notifying you
sequence:
  - action: python_script.count_words
    data:
      message: "{{ message }}"
    response_variable: word_count
  - action: system_log.write
    data:
      message: "{{ word_count.debug_message }}"
      level: info
  - action: python_script.notify_alexa_media_last_called
    data:
      message: "{{ message }}"
  - delay:
      seconds: >-
        {{ word_count.word_count * states('input_number.alexa_word_duration') |
        float(0.5)  }}
    alias: Delay for word count * input_number.alexa_word_duration
mode: queued
icon: mdi:account-voice
max: 10

and the execution trace shows as:

Executed: January 9, 2025 at 5:32:46 AM
Result:
params:
  domain: system_log
  service: write
  service_data:
    message: >-
      Processed message: 'Your E V range is 279.7 kilometres.' to 'Your E V
      range is two hundred and seventy nine point seven kilometres.' with word
      count: 13
    level: info
  target: {}
running_script: false

BTW, “Her” last reponse was…

You’re very welcome! :tada: It’s great to see everything working smoothly now, including the correct word count and logs displaying as expected. If you need further help in the future—whether with Home Assistant or anything else—don’t hesitate to reach out. Happy automating, and have a fantastic day! :rocket::blush:

1 Like