"""La palabra de activacion: "JARVIS".

Sin esto, el asistente obedece a cualquier cosa que se diga en la habitacion —y
a la tele—. Con esto, solo escucha de verdad durante un rato despues de oir su
nombre.

## Por que sobre el texto y no sobre el audio

Lo obvio seria un detector como openwakeword, que mira la forma de onda antes de
transcribir. Aqui se hace despues, sobre lo que Whisper ya escribio, y es a
proposito:

  - **Ya tenemos la transcripcion.** El audio pasa por Whisper de todas formas
    porque la captura la dispara el detector de voz; no hay ahorro que ganar.
  - **Un modelo menos.** openwakeword son mas dependencias, otro modelo en
    memoria y otro afinado que mantener.
  - **Y sobre todo: Whisper oye "JARVIS" de mil maneras.** Medido en el registro
    de uso real, el nombre sale como "Jarvis", "Yarvis", "Charles", "Chavez",
    "Harvest"... Sobre el texto eso se arregla comparando como SUENA, que es lo
    que ya hace `manos/fonetica.py` para el catalogo. Sobre la onda habria que
    reentrenar.

Lo que se pierde: la transcripcion corre siempre, tambien cuando nadie ha
llamado. En una maquina donde Whisper va en la grafica y tarda dos segundos, es
un coste que ya estabamos pagando.

## Como se comporta

Se llama una vez y se queda despierto un rato, para poder encadenar ordenes sin
repetir el nombre en cada frase. Y si la frase que trae el nombre lleva ademas
la orden —"JARVIS, cuanto espacio queda"— se atiende esa misma, sin obligar a
decirlo dos veces.
"""
import difflib
import re
import time

# Absolutos, como el resto del nucleo: jarvis.py mete su carpeta en sys.path y
# todos los paquetes son de primer nivel. Con `..manos` esto solo se podria
# importar desde fuera, y las pruebas lo cargan suelto.
from manos.acciones import normaliza
from manos.fonetica import fonetica

NOMBRE = "jarvis"

# Cuanto sigue escuchando tras oir su nombre. Corto obliga a repetirlo en cada
# frase; largo devuelve al problema de origen, que obedezca a la tele.
DESPIERTO_S = 25.0

# Lo que Whisper escribe cuando oye "JARVIS". Salen del registro real de uso,
# no de imaginarlas.
PARECIDAS = (
    "jarvis", "yarvis", "charvis", "jarbis", "yarbis", "harvis", "jervis",
    "jarvi", "yarvi", "javis", "jarves", "charles", "chavez", "harvest",
    "jarvix", "sharvis", "arvis",
)

# Se compara palabra a palabra, asi que el umbral puede ser alto sin perder
# aciertos: "yarvis" contra "jarvis" da 0,83 por letra y sube por sonido.
UMBRAL = 0.80

_LIMPIA = re.compile(r"^[\s,.:;¡!¿?]+|[\s,.:;¡!¿?]+$")

# Lo que suele acompañar al nombre sin ser una orden. Si al quitar el nombre
# solo queda esto, es que llamaron y ya: "oye jarvis" no es "haz oye".
LLAMADAS = {"oye", "eh", "hey", "ey", "hola", "escucha", "atiende", "perdona",
            "por favor", "porfa", "venga", "a ver"}


def _suena_al_nombre(palabra: str) -> bool:
    p = _LIMPIA.sub("", palabra)
    if len(p) < 4:
        return False
    if p in PARECIDAS:
        return True
    for candidata in (NOMBRE,) + PARECIDAS[:6]:
        if difflib.SequenceMatcher(None, p, candidata).ratio() >= UMBRAL:
            return True
        if difflib.SequenceMatcher(None, fonetica(p), fonetica(candidata)).ratio() >= UMBRAL:
            return True
    return False


def parte_el_nombre(texto: str):
    """(lo llamaron, lo que queda de la frase sin el nombre).

    Se devuelve el resto porque "JARVIS, cuanto espacio queda" tiene que
    ejecutarse en la misma frase: obligar a decir el nombre y luego la orden
    aparte es exactamente lo que hace que un asistente canse.
    """
    palabras = normaliza(texto).split()
    if not palabras:
        return False, ""
    for i, p in enumerate(palabras):
        if _suena_al_nombre(p):
            sobra = [q for q in palabras[:i] + palabras[i + 1:]
                     if q not in LLAMADAS]
            return True, " ".join(sobra).strip()
    return False, ""


class Centinela:
    """Decide si una frase hay que atenderla o dejarla pasar."""

    def __init__(self, activo=True, ventana=DESPIERTO_S):
        self.activo = activo
        self.ventana = ventana
        self._hasta = 0.0

    @property
    def despierto(self) -> bool:
        return not self.activo or time.monotonic() < self._hasta

    def despierta(self):
        self._hasta = time.monotonic() + self.ventana

    def duerme(self):
        self._hasta = 0.0

    def filtra(self, texto: str):
        """(hay que atenderla, texto a atender, acaba de despertar).

        - Sin palabra de activacion configurada: se atiende todo.
        - Dormido y sin nombre: se ignora.
        - Dormido y con nombre: despierta. Si la frase traia ademas la orden,
          se devuelve; si solo traia el nombre, se contesta con un acuse.
        - Despierto: se atiende y se renueva la ventana, para poder encadenar.
        """
        if not self.activo:
            return True, texto, False

        llamado, resto = parte_el_nombre(texto)

        if llamado:
            recien = not self.despierto
            self.despierta()
            return (True, resto, recien) if resto else (False, "", recien)

        if self.despierto:
            self.despierta()          # encadenar sin repetir el nombre
            return True, texto, False

        return False, "", False
