vosk-api icon indicating copy to clipboard operation
vosk-api copied to clipboard

Can vosk recognise numbers as numbers?

Open UnexpectedMaker opened this issue 4 years ago • 10 comments

Hi folks,

Is it possible to make vosk recognise numbers as numbers?

Like when I say "150" vosk returns "one hundred and fifty" instead of 150.

Thanks :)

UnexpectedMaker avatar Jul 09 '21 07:07 UnexpectedMaker

Hi Unexpected Maker

this is not an issue.

This is something you can do make your function to convert the transcript sentence produced by Vosk, in digits.

toDigits("one hundred and fifty")
// 150

Please close the issue

solyarisoftware avatar Jul 09 '21 08:07 solyarisoftware

You can use https://github.com/allo-media/text2num

nshmyrev avatar Jul 09 '21 08:07 nshmyrev

Ok, sure, but none of the other STT platforms I am playing with require any extra parsing to get numbers. They just do the numbers by default, so I thought there would be a way to just have that happen in vosk.

UnexpectedMaker avatar Jul 09 '21 08:07 UnexpectedMaker

Yeah, these solutions are no good as they don't do mixed numbers and non number words. If vosk doesn't support this out of the box, I'll move on to a different STT platform.

UnexpectedMaker avatar Jul 09 '21 08:07 UnexpectedMaker

"digitizing" out of the box, is not the common basic goal of an ASR. Doing it is even wrong conceptually, for me. Conversion of a numeric value entity into digits depends on each natural language/jargons conventions and above all it depends on a contextual semantic interpretation of that "number". You have to implement that conversion at an upper level in you application. You probably find OSS libraries that does that.

solyarisoftware avatar Jul 09 '21 09:07 solyarisoftware

they don't do mixed numbers and non number words

text2num works just fine

nshmyrev avatar Jul 10 '21 22:07 nshmyrev

text2num is a python library, you have any option for Java on Android?

merc74 avatar Mar 14 '22 17:03 merc74

@merc74 no good solution for now, maybe something like https://github.com/jgraham0325/words-to-numbers

nshmyrev avatar Mar 14 '22 18:03 nshmyrev

There is a text2num rust version, this might work under Android Java app. https://github.com/allo-media/text2num-rs

Do you think it would be hard to create a fork of "vosk-api" and add an option to it to return the actual number representation instead of the word representing the number? Which section of the code should I look at to see the implication of doing this version.

merc74 avatar Mar 14 '22 21:03 merc74

Try this AGI to convert words to numbers in the dialplan. Save it to /var/lib/asterisk/agi-bin/convertir-palabras-a-numeros.agi

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys
import os
import unicodedata # Para normalizar texto (quitar acentos)

# --- INICIO: Logging para Depuración ---
LOG_FILE = '/tmp/agi_debug.log'
def log_debug(message):
    with open(LOG_FILE, 'a') as f:
        f.write(f"[{os.getpid()}] {message}\n")
# --- FIN: Logging ---

# Diccionario MEJORADO.
# 'cero' es el único dígito como string.
# Los números 1-9 son enteros para poder sumarlos en compuestos.
# ¡AÑADIDAS VARIANTES SIN ACENTO PARA MAYOR ROBUSTEZ!
PALABRAS_A_NUMEROS = {
    'cero': '0', 'uno': 1, 'dos': 2, 'tres': 3, 'cuatro': 4,
    'cinco': 5, 'seis': 6, 'siete': 7, 'ocho': 8, 'nueve': 9,
    'diez': 10, 'once': 11, 'doce': 12, 'trece': 13, 'catorce': 14,
    'quince': 15, 'dieciseis': 16, 'dieciséis': 16, 'diecisiete': 17,
    'dieciocho': 18, 'diecinueve': 19, 'veinte': 20, 'veintiun': 21,
    'veintiún': 21, 'veintiuno': 21, 'veintidos': 22, 'veintidós': 22,
    'veintitres': 23, 'veintitrés': 23, 'veinticuatro': 24,
    'veinticinco': 25, 'veintiseis': 26, 'veintiséis': 26,
    'veintisiete': 27, 'veintiocho': 28, 'veintinueve': 29,
    'treinta': 30, 'cuarenta': 40, 'cincuenta': 50, 'sesenta': 60,
    'setenta': 70, 'ochenta': 80, 'noventa': 90,
    'cien': 100, 'ciento': 100, 'doscientos': 200, 'trescientos': 300,
    'cuatrocientos': 400, 'quinientos': 500, 'seiscientos': 600,
    'setecientos': 700, 'ochocientos': 800, 'novecientos': 900,
    'mil': 1000, 'millon': 1000000, 'millones': 1000000
}

# Tipos de números para lógica
TIPOS_NUMERICOS = {
    'digit': [0,1,2,3,4,5,6,7,8,9], # 'cero' a 'nueve'
    'tens': list(range(10, 100)), # 'diez' a 'noventa y nueve' (incluye 21-29, que son compuestos)
    'hundred': [100, 200, 300, 400, 500, 600, 700, 800, 900], # 'cien' a 'novecientos'
    'thousand': [1000], # 'mil'
    'million': [1000000] # 'millón', 'millones'
}

def get_tipo_numero(valor):
    if valor is None:
        return None
    if isinstance(valor, str) and valor == '0': # Cero es un caso especial por ser string
        return 'digit'
    if isinstance(valor, int):
        for tipo, rango in TIPOS_NUMERICOS.items():
            if valor in rango:
                return tipo
    return None

def limpiar_texto(texto):
    # Convertir a minúsculas
    texto_limpio = texto.lower()
    # Eliminar acentos
    texto_limpio = ''.join(c for c in unicodedata.normalize('NFD', texto_limpio) if unicodedata.category(c) != 'Mn')
    # Eliminar 'y' si está rodeada de espacios, ya que los números como "treintaydos" ya están en el diccionario
    texto_limpio = texto_limpio.replace(' y ', ' ')
    # Eliminar dobles espacios
    texto_limpio = ' '.join(texto_limpio.split()).strip()
    return texto_limpio

def procesar_bloque_menor_mil(bloque_palabras_numericas):
    """
    Procesa un bloque de palabras numéricas que forman un número menor a mil.
    Ej: ['ciento', 'veintidos'] -> 122
    Ej: ['sesenta', 'dos'] -> 62
    """
    log_debug(f" [procesar_bloque_menor_mil] INICIO - Bloque: {bloque_palabras_numericas}")
    block_value = 0
    current_units_tens = 0 # Acumula unidades y decenas para un posible "sumatorio" (ej. "sesenta" + "dos")
    has_hundred = False

    for i, palabra in enumerate(bloque_palabras_numericas):
        valor = PALABRAS_A_NUMEROS.get(palabra)
        tipo = get_tipo_numero(valor)
        log_debug(f"  [procesar_bloque_menor_mil] Procesando palabra: '{palabra}' (Tipo: {tipo}, Valor: {valor})")

        if tipo == 'hundred':
            if has_hundred: # Si ya tenemos una centena, esto es un error de agrupación o una nueva centena
                log_debug(f"  [procesar_bloque_menor_mil] ADVERTENCIA: Múltiples centenas en un bloque de suma. Procesando sub-bloque anterior.")
                block_value += current_units_tens # Sumar cualquier decena/unidad acumulada
                current_units_tens = 0 # Reset para la nueva centena
                block_value = valor # La nueva centena sobrescribe o inicia un nuevo sub-bloque
            else:
                block_value += valor
                has_hundred = True
            log_debug(f"  [procesar_bloque_menor_mil] Añadida centena. block_value={block_value}")
            current_units_tens = 0 # Reiniciar unidades/decenas después de una centena, se suman directamente al block_value.
        elif tipo in ['tens', 'digit']:
            # Lógica para sumar tens y digit
            if has_hundred: # Si ya hay una centena, sumar unidades/decenas directamente a block_value
                block_value += valor
                log_debug(f"  [procesar_bloque_menor_mil] Tras centena, añadidas unidades/decenas. block_value={block_value}")
            else:
                # Si es una decena (ej. 30) y no hay unidades/decenas previas, o si es un número compuesto (ej. 22)
                if tipo == 'tens' and current_units_tens == 0:
                    current_units_tens = valor
                # Si es un dígito y la ultima palabra fue una decena (ej. 'treinta' + 'dos')
                elif tipo == 'digit' and get_tipo_numero(current_units_tens) == 'tens':
                    current_units_tens += valor
                # Si ya hay un valor en current_units_tens y llega otra decena, esto es un error de agrupamiento (ej. 'veinte' 'treinta')
                elif tipo == 'tens' and current_units_tens > 0:
                    log_debug(f"  [procesar_bloque_menor_mil] ADVERTENCIA: Múltiples decenas en el mismo bloque (sin ser compuestas). current_units_tens={current_units_tens}, new_val={valor}")
                    block_value += current_units_tens # Sumar la decena anterior
                    current_units_tens = valor # Empezar una nueva decena
                else: # Si es un dígito simple sin decena precedente, o una decena iniciando
                    current_units_tens = valor
            log_debug(f"  [procesar_bloque_menor_mil] Unidades/Decenas procesadas. current_units_tens={current_units_tens}, block_value={block_value}")
        else:
            log_debug(f"  [procesar_bloque_menor_mil] ADVERTENCIA: Palabra inesperada en bloque_menor_mil: '{palabra}'")

    # Al finalizar el bucle, añadir cualquier unidad/decena restante
    if current_units_tens > 0:
        block_value += current_units_tens

    log_debug(f" [procesar_bloque_menor_mil] FIN - Resultado: {block_value}")
    return block_value

def convertir(texto):
    log_debug(f"[convertir] INICIO - Texto original: '{texto}'")
    texto_limpio = limpiar_texto(texto)
    palabras = texto_limpio.split()
    log_debug(f"[convertir] Texto limpio: '{texto_limpio}', Palabras: {palabras}")

    final_numerical_parts = [] # Almacenará números como enteros (ej. 123) o strings de dígitos ('1', '2', '3')
    current_segment_for_sum = [] # Acumula palabras para bloques que se SUMAN (ej. "ciento veintidos")

    i = 0
    while i < len(palabras):
        palabra = palabras[i]
        valor = PALABRAS_A_NUMEROS.get(palabra)
        tipo = get_tipo_numero(valor)
        log_debug(f"[convertir] --- Procesando palabra [{i}]: '{palabra}' (Valor: {valor}, Tipo: {tipo}) ---")

        # Caso: Palabra no reconocida o no numérica
        if valor is None:
            if current_segment_for_sum:
                calculated_value = procesar_bloque_menor_mil(current_segment_for_sum)
                final_numerical_parts.append(calculated_value)
                current_segment_for_sum = []
                log_debug(f"[convertir] Segmento compuesto '{current_segment_for_sum}' procesado y añadido por palabra no reconocida. Final_numerical_parts: {final_numerical_parts}")
            i += 1
            continue

        if tipo in ['thousand', 'million']:
            multiplier = valor
            val_to_multiply = 1 # Por defecto si es solo "mil" o "millon"

            # 1. Prioridad: ¿Hay un segmento de suma pendiente?
            if current_segment_for_sum:
                val_to_multiply = procesar_bloque_menor_mil(current_segment_for_sum)
                current_segment_for_sum = []
            # 2. Segunda prioridad: ¿El último elemento en final_numerical_parts es un número (int)?
            elif final_numerical_parts and isinstance(final_numerical_parts[-1], int):
                val_to_multiply = final_numerical_parts.pop() # Quitarlo y usarlo
                log_debug(f"[convertir] Usando {val_to_multiply} como multiplicador para {palabra} (de int).")
            # 3. Tercera prioridad: ¿El último elemento es un dígito string?
            elif final_numerical_parts and isinstance(final_numerical_parts[-1], str) and final_numerical_parts[-1].isdigit():
                val_to_multiply = int(final_numerical_parts.pop()) # Quitarlo y usarlo
                log_debug(f"[convertir] Usando dígito '{val_to_multiply}' como multiplicador para {palabra} (de str).")
            
            final_numerical_parts.append(val_to_multiply * multiplier)
            log_debug(f"[convertir] Magnitud '{palabra}' detectada. Valor_previo: {val_to_multiply}. Multiplicador: {multiplier}. Final_numerical_parts: {final_numerical_parts}")

        elif tipo == 'digit':
            # Determinar si el dígito debe sumarse al segmento actual o concatenarse.
            should_add_to_segment = False
            
            if current_segment_for_sum:
                last_word_in_segment = current_segment_for_sum[-1]
                last_val_in_segment = PALABRAS_A_NUMEROS.get(last_word_in_segment)
                last_tipo_in_segment = get_tipo_numero(last_val_in_segment)

                # Regla 1: Si el segmento actual termina en una decena (ej. "treinta") y la palabra actual es un dígito (ej. "dos"), sumarlos.
                if last_tipo_in_segment == 'tens' and valor != 0 and (last_val_in_segment % 10 == 0) : # "treinta" + "dos" = 32
                    should_add_to_segment = True
                
                # Regla 2: Si el segmento actual termina en una centena (ej. "cien") y la palabra actual es un dígito, sumarlos. (ej. "cien dos" -> 102)
                elif last_tipo_in_segment == 'hundred':
                    should_add_to_segment = True
            
            # Si el dígito no se añade a un segmento de suma, se gestiona para concatenación
            if not should_add_to_segment:
                if current_segment_for_sum: # Si hay un segmento de suma pendiente, procesarlo primero
                    calculated_value = procesar_bloque_menor_mil(current_segment_for_sum)
                    final_numerical_parts.append(calculated_value)
                    current_segment_for_sum = []
                    log_debug(f"[convertir] Se cierra segmento de suma antes de dígito. Valor: {calculated_value}. Final_numerical_parts: {final_numerical_parts}")

                # Después de cerrar, el dígito actual se concatena si el anterior era un dígito string,
                # o se suma si el anterior era un int de magnitud.
                if final_numerical_parts and isinstance(final_numerical_parts[-1], int) and \
                   (final_numerical_parts[-1] % 1000 == 0 or final_numerical_parts[-1] % 100 == 0):
                    # Este es un caso para "mil cuatro" (1000 + 4 = 1004) o "cien dos" (100 + 2 = 102).
                    # Si el último número es un múltiplo exacto de 1000 o 100 y el actual es un dígito, súmalos.
                    last_num = final_numerical_parts.pop()
                    final_numerical_parts.append(last_num + valor)
                    log_debug(f"[convertir] Dígito '{palabra}' sumado a número previo. Final_numerical_parts: {final_numerical_parts}")
                else:
                    final_numerical_parts.append(str(valor)) # Añadir como string para concatenar
                    log_debug(f"[convertir] Dígito '{palabra}' añadido como string. Final_numerical_parts: {final_numerical_parts}")
            else: # should_add_to_segment is True
                current_segment_for_sum.append(palabra)
                log_debug(f"[convertir] Dígito '{palabra}' añadido a current_segment_for_sum (para suma). Contenido: {current_segment_for_sum}")

        elif tipo in ['tens', 'hundred']:
            # Lógica para decidir si la palabra actual extiende el segmento de suma actual
            # o si inicia un nuevo número (cerrando el segmento actual).
            
            should_close_current_segment = False
            if current_segment_for_sum:
                last_word_in_segment = current_segment_for_sum[-1]
                last_val_in_segment = PALABRAS_A_NUMEROS.get(last_word_in_segment)
                last_tipo_in_segment = get_tipo_numero(last_val_in_segment)

                # Reglas para forzar el cierre del segmento actual:
                # 1. Si el segmento actual ya tiene una centena y la nueva palabra es otra centena. (Ej: 'ciento' + 'doscientos')
                # 2. Si el segmento actual ya tiene una decena o número compuesto (ej. 'doce', 'veintidos')
                #    y la nueva palabra es una centena (ej. 'doscientos').
                # 3. Si el segmento actual ya tiene una decena y la nueva palabra es otra decena (no son un compuesto, y la anterior no es 'veinti-X').
                #    Es decir, si son dos decenas "redondas" o compuestas que no se suman entre sí.
                if (last_tipo_in_segment == 'hundred' and tipo == 'hundred') or \
                   (last_tipo_in_segment == 'tens' and last_val_in_segment < 30 and tipo == 'tens') or \
                   (last_tipo_in_segment == 'tens' and tipo == 'hundred') or \
                   (last_tipo_in_segment == 'tens' and tipo == 'tens' and last_val_in_segment % 10 == 0 and valor % 10 == 0): # Ambos son decenas "redondas"
                    should_close_current_segment = True
            
            if should_close_current_segment:
                calculated_value = procesar_bloque_menor_mil(current_segment_for_sum)
                final_numerical_parts.append(calculated_value)
                current_segment_for_sum = [palabra] # Iniciar un nuevo segmento con la palabra actual
                log_debug(f"[convertir] Forzando cierre de segmento de suma. Valor: {calculated_value}. Nueva current_segment_for_sum: {current_segment_for_sum}. Final_numerical_parts: {final_numerical_parts}")
            else:
                current_segment_for_sum.append(palabra)
                log_debug(f"[convertir] Añadido '{palabra}' a current_segment_for_sum. Contenido: {current_segment_for_sum}")

        log_debug(f"[convertir] Estado al final de la iteración para '{palabra}': current_segment_for_sum={current_segment_for_sum}, final_numerical_parts={final_numerical_parts}")
        i += 1

    # Al finalizar el bucle, procesar cualquier segmento de SUMA restante
    if current_segment_for_sum:
        calculated_value = procesar_bloque_menor_mil(current_segment_for_sum)
        final_numerical_parts.append(calculated_value)
        log_debug(f"[convertir] Procesando current_segment_for_sum restante: {current_segment_for_sum} -> {calculated_value}. Final_numerical_parts: {final_numerical_parts}")

    # Unir las partes numéricas. Convertir todo a string antes de unir.
    final_result_str = "".join(str(part) for part in final_numerical_parts)
    
    # Manejar caso de entrada vacía o solo palabras no numéricas
    if not final_result_str and texto_limpio:
        if texto_limpio == 'cero': # Asegurarse de que 'cero' se traduzca correctamente.
            final_result_str = '0'
        else: # Si no se pudo convertir nada y no era 'cero', dejar vacío.
            final_result_str = ""

    log_debug(f"[convertir] FIN - Resultado final: '{final_result_str}'")
    return final_result_str

# --- Funciones de comunicación AGI (sin cambios) ---
def leer_variables_agi():
    env = {}
    log_debug("Iniciando lectura de variables AGI.")
    while True:
        line = sys.stdin.readline().strip()
        if line == '':
            log_debug("Línea en blanco recibida. Fin de variables AGI.")
            break
        key, value = line.split(':', 1)
        env[key.strip()] = value.strip()
    return env

def enviar_comando_agi(comando):
    log_debug(f"Enviando comando: {comando}")
    sys.stdout.write(comando + '\n')
    sys.stdout.flush()
    resultado = sys.stdin.readline().strip()
    log_debug(f"Respuesta de Asterisk: {resultado}")
    return resultado

# --- Flujo Principal del Script ---
log_debug("================ SCRIPT AGI INICIADO ================")
try:
    env = leer_variables_agi()
    texto_a_convertir = env.get('agi_arg_1', '')

    if not texto_a_convertir:
        texto_a_convertir = os.environ.get('INPUT_WORDS_FROM_DIALPLAN', '')
        if texto_a_convertir:
            log_debug(f"Texto a convertir obtenido de variable de entorno 'INPUT_WORDS_FROM_DIALPLAN': '{texto_a_convertir}'")
        else:
            if len(sys.argv) > 1:
                texto_a_convertir = ' '.join(sys.argv[1:])
                log_debug(f"Texto a convertir obtenido de sys.argv (ejecución manual): '{texto_a_convertir}'")
            else:
                log_debug("ERROR: No se pudo obtener texto a convertir de agi_arg_1, variable de entorno ni sys.argv.")
                enviar_comando_agi(f'SET VARIABLE CONVERTED_NUMBER ""')
                sys.exit(1)

    log_debug(f"Texto a convertir: '{texto_a_convertir}'")
    numero_convertido = convertir(texto_a_convertir)
    log_debug(f"Número convertido FINAL: '{numero_convertido}'")
    enviar_comando_agi(f'SET VARIABLE CONVERTED_NUMBER "{numero_convertido}"')

except Exception as e:
    log_debug(f"ERROR INESPERADO: {e}")
    enviar_comando_agi(f'SET VARIABLE CONVERTED_NUMBER ""')
    sys.exit(1)

finally:
    log_debug("================ SCRIPT AGI FINALIZADO ================")

On extensions.conf:

[from-vosk]
exten => s,1,Answer()
same => n,Wait(1)
same => n,Set(TIMEOUT(response)=5) ; Espera 5 segundos para que el usuario empiece a hablar
same => n,Set(TIMEOUT(digit)=3)   ; Espera 3 segundos entre "dígitos" (silencio)
same => n,SpeechCreate
same => n,AGI(googletts.agi,"Por favor, despues del tono, dígame el número al que desea llamar",es)
same => n,SpeechBackground(beep)
same => n,Verbose(0,Result was ${SPEECH_TEXT(0)})
same => n,Set(INPUT_WORDS=${SPEECH_TEXT(0)})
same => n,AGI(convertir-palabras-a-numeros.agi,"${INPUT_WORDS}")
same => n,NoOp(DEBUG_DIALPLAN: El valor antes de llamar a AGI es: "${INPUT_WORDS}")
same => n,NoOp(DEBUG_DIALPLAN: El valor convertido es: ${CONVERTED_NUMBER})
same => n,AGI(googletts.agi,"Gracias. Estoy llamando al número ${CONVERTED_NUMBER}.",es)
same => n,Set(__CALLINGPRES_SV=${CALLINGPRES}) ; Guardar el CallerID Pres de la llamada
same => n,Set(CALLINGPRES=allowed_passed_screen) ; Asegurar que el CallerID se pase
same => n,Goto(from-internal,${CONVERTED_NUMBER},1) ; Intenta marcar la extensión usando el contexto interno de FreePBX
same => n,Hangup()

razametal avatar Jun 21 '25 07:06 razametal