Can vosk recognise numbers as numbers?
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 :)
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
You can use https://github.com/allo-media/text2num
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.
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.
"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.
they don't do mixed numbers and non number words
text2num works just fine
text2num is a python library, you have any option for Java on Android?
@merc74 no good solution for now, maybe something like https://github.com/jgraham0325/words-to-numbers
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.
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()