diff options
Diffstat (limited to 'scripts')
| -rwxr-xr-x | scripts/bootstrap.sh | 155 | ||||
| -rwxr-xr-x | scripts/buscar-ddgs.sh | 54 | ||||
| -rwxr-xr-x | scripts/capturar-pantalla.sh | 52 | ||||
| -rwxr-xr-x | scripts/capture-screen.sh | 53 | ||||
| -rwxr-xr-x | scripts/clonar-voz.sh | 47 | ||||
| -rwxr-xr-x | scripts/clone-voice.sh | 47 | ||||
| -rwxr-xr-x | scripts/search-ddgs.sh | 54 | ||||
| -rwxr-xr-x | scripts/servers.sh | 84 | ||||
| -rwxr-xr-x | scripts/servidores.sh | 83 |
9 files changed, 315 insertions, 314 deletions
diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh index 9ba694e..809e4e2 100755 --- a/scripts/bootstrap.sh +++ b/scripts/bootstrap.sh @@ -1,132 +1,131 @@ #!/usr/bin/env bash -# Deja el repositorio en condiciones de ejecutarse: submódulos, parches, -# binarios y modelos. +# Gets the repository ready to run: submodules, patches, binaries and models. # -# Es idempotente: se puede volver a lanzar sin miedo. Y no copia modelos —son -# más de 5 GB—, sino que los enlaza desde donde ya estén. +# It is idempotent: it can be run again safely. And it does not copy models +# (they are more than 5 GB); it links them from wherever they already are. set -euo pipefail cd "$(dirname "$0")/.." -RAIZ="$PWD" +ROOT="$PWD" -# Repositorios ya clonados en el sistema, para no volver a bajar de la red ni -# recompilar lo que ya está compilado. -ESPEJO="${ASIST_ESPEJO:-$HOME/GIT-MIRRO}" +# Repositories already cloned on the system, so nothing is downloaded or +# rebuilt again. ASIST_ESPEJO is the old name of ASIST_MIRROR. +MIRROR="${ASIST_MIRROR:-${ASIST_ESPEJO:-$HOME/GIT-MIRRO}}" HF="${ASIST_HF:-$HOME/HF}" -azul() { printf '\033[36m%s\033[0m\n' "$*"; } -verde() { printf '\033[32m%s\033[0m\n' "$*"; } -aviso() { printf '\033[33m%s\033[0m\n' "$*" >&2; } -malo() { printf '\033[31m%s\033[0m\n' "$*" >&2; } +blue() { printf '\033[36m%s\033[0m\n' "$*"; } +green() { printf '\033[32m%s\033[0m\n' "$*"; } +warn() { printf '\033[33m%s\033[0m\n' "$*" >&2; } +bad() { printf '\033[31m%s\033[0m\n' "$*" >&2; } -enlazar() { # origen destino descripción - local origen="$1" destino="$2" que="$3" - if [ ! -e "$origen" ]; then - aviso " no está $que: $origen" +link() { # source target description + local src="$1" dst="$2" what="$3" + if [ ! -e "$src" ]; then + warn " $what not found: $src" return 1 fi - mkdir -p "$(dirname "$destino")" - if [ -L "$destino" ] || [ -e "$destino" ]; then - rm -rf "$destino" + mkdir -p "$(dirname "$dst")" + if [ -L "$dst" ] || [ -e "$dst" ]; then + rm -rf "$dst" fi - ln -s "$origen" "$destino" - verde " $que -> $(basename "$origen")" + ln -s "$src" "$dst" + green " $what -> $(basename "$src")" } # --------------------------------------------------------------------------- -azul "1/5 Submódulos" -# Si el repositorio está clonado al lado, se usa como referencia: llama.cpp son -# 406 MB de objetos que no hace falta volver a descargar. -for nombre in canary-rs qwentts.cpp llama.cpp; do - ruta="vendor/$nombre" - if [ -f "$ruta/.git" ] || [ -d "$ruta/.git" ]; then +blue "1/5 Submodules" +# If the repository is cloned next door, it is used as a reference: llama.cpp +# is 406 MB of objects that do not need downloading again. +for name in canary-rs qwentts.cpp llama.cpp; do + path="vendor/$name" + if [ -f "$path/.git" ] || [ -d "$path/.git" ]; then continue fi - if [ -d "$ESPEJO/$nombre/.git" ]; then + if [ -d "$MIRROR/$name/.git" ]; then git -c protocol.file.allow=always submodule update --init \ - --reference "$ESPEJO/$nombre" "$ruta" + --reference "$MIRROR/$name" "$path" else - git submodule update --init "$ruta" + git submodule update --init "$path" fi done git submodule status | sed 's/^/ /' # --------------------------------------------------------------------------- -azul "2/5 Parches y ficheros sueltos" -# Los parches recogen los cambios locales sobre cada repositorio: sin ellos, el -# ASR se comporta distinto al que se midió. -for nombre in canary-rs qwentts.cpp llama.cpp; do - parche="$RAIZ/vendor/patches/$nombre.patch" - [ -s "$parche" ] || continue +blue "2/5 Patches and loose files" +# The patches hold the local changes on each repository: without them the ASR +# behaves differently from the one that was measured. +for name in canary-rs qwentts.cpp llama.cpp; do + patch="$ROOT/vendor/patches/$name.patch" + [ -s "$patch" ] || continue ( - cd "vendor/$nombre" - if git apply --check "$parche" 2>/dev/null; then - git apply "$parche" - verde " $nombre: parche aplicado" - elif git apply --reverse --check "$parche" 2>/dev/null; then - verde " $nombre: parche ya aplicado" + cd "vendor/$name" + if git apply --check "$patch" 2>/dev/null; then + git apply "$patch" + green " $name: patch applied" + elif git apply --reverse --check "$patch" 2>/dev/null; then + green " $name: patch already applied" else - malo " $nombre: el parche no aplica; revísalo a mano" + bad " $name: the patch does not apply; review it by hand" fi ) done -# Ficheros que sólo existían en el árbol de trabajo local y que un parche no -# puede llevar. canary-rs no compila sin bench_live.rs: su Cargo.toml lo declara. +# Files that only existed in the local working tree and a patch cannot carry. +# canary-rs does not build without bench_live.rs: its Cargo.toml declares it. if [ -d vendor/extra ]; then - for nombre in canary-rs qwentts.cpp; do - [ -d "vendor/extra/$nombre" ] || continue - cp -rn "vendor/extra/$nombre/." "vendor/$nombre/" 2>/dev/null || true + for name in canary-rs qwentts.cpp; do + [ -d "vendor/extra/$name" ] || continue + cp -rn "vendor/extra/$name/." "vendor/$name/" 2>/dev/null || true done - verde " ficheros sueltos copiados" + green " loose files copied" fi # --------------------------------------------------------------------------- -azul "3/5 Motores en C++" -enlazar_o_construir() { # nombre ruta_build orden_de_construccion... - local nombre="$1" build="$2"; shift 2 - if [ -e "vendor/$nombre/$build" ]; then - verde " $nombre ya está construido" +blue "3/5 C++ engines" +link_or_build() { # name build_path build_command... + local name="$1" build="$2"; shift 2 + if [ -e "vendor/$name/$build" ]; then + green " $name is already built" return fi - if [ -e "$ESPEJO/$nombre/$build" ]; then - enlazar "$ESPEJO/$nombre/$(dirname "$build")" \ - "$RAIZ/vendor/$nombre/$(dirname "$build")" "build de $nombre" + if [ -e "$MIRROR/$name/$build" ]; then + link "$MIRROR/$name/$(dirname "$build")" \ + "$ROOT/vendor/$name/$(dirname "$build")" "$name build" return fi - aviso " $nombre sin construir. Construyendo (esto tarda)…" - ( cd "vendor/$nombre" && "$@" ) + warn " $name not built. Building (this takes a while)…" + ( cd "vendor/$name" && "$@" ) } -enlazar_o_construir llama.cpp build/bin/llama-server \ +link_or_build llama.cpp build/bin/llama-server \ bash -c 'cmake -B build -DCMAKE_BUILD_TYPE=Release -DGGML_CUDA=ON && cmake --build build -j --target llama-server' -enlazar_o_construir qwentts.cpp build/tts-server \ +link_or_build qwentts.cpp build/tts-server \ bash -c './buildcuda.sh' # --------------------------------------------------------------------------- -azul "4/5 Modelos" -# Se enlazan, no se copian: son más de 5 GB entre todos. -enlazar "$HF/Qwen/Qwen3.5-2B.Q5_K_M.gguf" "$RAIZ/models/Qwen3.5-2B.Q5_K_M.gguf" "modelo del LLM" || true -enlazar "$HF/Qwen/mmproj-BF16.gguf" "$RAIZ/models/mmproj-BF16.gguf" "proyector multimodal" || true +blue "4/5 Models" +# Linked, not copied: together they are more than 5 GB. +link "$HF/Qwen/Qwen3.5-2B.Q5_K_M.gguf" "$ROOT/models/Qwen3.5-2B.Q5_K_M.gguf" "LLM model" || true +link "$HF/Qwen/mmproj-BF16.gguf" "$ROOT/models/mmproj-BF16.gguf" "multimodal projector" || true for m in qwen-talker-1.7b-base-Q8_0.gguf qwen-tokenizer-12hz-Q8_0.gguf; do - enlazar "$ESPEJO/qwentts.cpp/models/$m" "$RAIZ/models/$m" "$m" || true + link "$MIRROR/qwentts.cpp/models/$m" "$ROOT/models/$m" "$m" || true done if [ ! -d vendor/canary-rs/models/canary-180m-flash-onnx ] \ - && [ -d "$ESPEJO/canary-rs/models/canary-180m-flash-onnx" ]; then - enlazar "$ESPEJO/canary-rs/models/canary-180m-flash-onnx" \ - "$RAIZ/vendor/canary-rs/models/canary-180m-flash-onnx" "modelo Canary" || true + && [ -d "$MIRROR/canary-rs/models/canary-180m-flash-onnx" ]; then + link "$MIRROR/canary-rs/models/canary-180m-flash-onnx" \ + "$ROOT/vendor/canary-rs/models/canary-180m-flash-onnx" "Canary model" || true fi # --------------------------------------------------------------------------- -azul "5/5 Voz de referencia" -# El asistente habla con una voz clonada. Si no hay una preparada, se dice cómo -# hacerla en vez de fallar: el resto ya funciona con una voz del modelo. +blue "5/5 Reference voice" +# The assistant speaks with a cloned voice. If none is prepared, say how to make +# one instead of failing: everything else already works with a model voice. if [ -f assets/voices/asistente.spk ]; then - verde " ya hay una voz en assets/voices/" + green " there is already a voice in assets/voices/" else - aviso " sin voz clonada. Prepárala con:" - aviso " scripts/clonar-voz.sh <grabacion.wav> <transcripcion.txt>" - aviso " o quita la sección [tts.reference] de config/asistente.toml para" - aviso " usar una voz del propio modelo." + warn " no cloned voice. Prepare one with:" + warn " scripts/clone-voice.sh <recording.wav> <transcript.txt>" + warn " or remove the [tts.reference] section from config/asistente.toml to" + warn " use one of the model's own voices." fi echo -azul "Listo. Comprueba con: cargo run --release -- check" +blue "Done. Check with: cargo run --release -- check" diff --git a/scripts/buscar-ddgs.sh b/scripts/buscar-ddgs.sh deleted file mode 100755 index a366123..0000000 --- a/scripts/buscar-ddgs.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env bash -# Busca con ddgs y escribe los resultados en JSON por la salida estándar. -# -# scripts/buscar-ddgs.sh "<consulta>" [nº de resultados] -# -# ddgs consulta varios buscadores (DuckDuckGo, Brave, Mojeek, Startpage…) sin -# necesitar ninguna clave. Este guion existe para que la configuración del -# asistente no tenga que saber nada de uv ni de entornos de Python: aquí se -# busca un ddgs utilizable y se normaliza la salida. -# -# Ojo con dos cosas que parecen equivalentes y no lo son: -# · `curl` contra html.duckduckgo.com devuelve HTTP 202 con una página -# anti-bot, sin resultados. Medido, no supuesto. -# · La API sin clave api.duckduckgo.com (Instant Answer) devuelve vacío para -# casi todo lo que no sea una entidad de enciclopedia. -# La librería ddgs sí funciona porque rota buscadores y cabeceras. -set -euo pipefail - -CONSULTA="${1:?uso: buscar-ddgs.sh \"<consulta>\" [nº]}" -MAXIMO="${2:-5}" - -PROGRAMA=$(cat <<'PY' -import json, sys -from ddgs import DDGS -consulta, maximo = sys.argv[1], int(sys.argv[2]) -try: - filas = DDGS().text(consulta, max_results=maximo) -except Exception as e: # noqa: BLE001 - print(json.dumps({"error": f"{type(e).__name__}: {e}"}), file=sys.stderr) - raise SystemExit(1) -print(json.dumps(filas, ensure_ascii=False)) -PY -) - -# 1) El entorno de `uv tool install ddgs`: es el camino rápido, porque no hay -# que resolver el paquete en cada búsqueda. -TOOL_PY="$HOME/.local/share/uv/tools/ddgs/bin/python" -if [ -x "$TOOL_PY" ]; then - exec "$TOOL_PY" -c "$PROGRAMA" "$CONSULTA" "$MAXIMO" -fi - -# 2) Un Python del sistema que ya tenga ddgs instalado. -if python3 -c "import ddgs" >/dev/null 2>&1; then - exec python3 -c "$PROGRAMA" "$CONSULTA" "$MAXIMO" -fi - -# 3) uvx, que lo descarga al vuelo. Funciona sin instalar nada, pero añade -# varios segundos a cada búsqueda: mejor `uv tool install ddgs`. -if command -v uvx >/dev/null 2>&1; then - exec uvx --quiet --from ddgs python -c "$PROGRAMA" "$CONSULTA" "$MAXIMO" -fi - -echo '{"error":"no hay ddgs disponible. Instálalo con: uv tool install ddgs"}' >&2 -exit 1 diff --git a/scripts/capturar-pantalla.sh b/scripts/capturar-pantalla.sh deleted file mode 100755 index 6d865a0..0000000 --- a/scripts/capturar-pantalla.sh +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env bash -# Captura la pantalla y escribe un JPEG por la salida estándar. -# -# scripts/capturar-pantalla.sh [ancho] [salida] -# -# ancho Ancho al que reducir la captura. Por defecto 1280, que es el -# mínimo medido para que el modelo lea texto de interfaz sin -# inventárselo (ver docs/RENDIMIENTO.md). -# salida Monitor concreto. Vacío = todo lo que haya. -# -# Está fuera del binario para que añadir un entorno gráfico nuevo sea editar -# este guion y no recompilar. La detección va de más específico a más general. -set -euo pipefail - -ANCHO="${1:-1280}" -SALIDA="${2:-}" - -falta() { echo "no hay con qué capturar la pantalla: $1" >&2; exit 1; } - -# El escalado se hace aquí y no en el binario: reducir antes de mandar la -# imagen al modelo es lo que baja el coste de 23 s a 7 s, y ffmpeg ya hace -# falta para la cámara. -reducir() { - if command -v ffmpeg >/dev/null 2>&1; then - ffmpeg -hide_banner -loglevel error -i - \ - -vf "scale=${ANCHO}:-2:flags=lanczos" -q:v 3 -f image2 -c:v mjpeg - - else - cat # sin ffmpeg se manda a tamaño completo: lento, pero funciona - fi -} - -if [ -n "${WAYLAND_DISPLAY:-}" ] && command -v grim >/dev/null 2>&1; then - # wlroots: Hyprland, Sway, river… grim escribe a stdout con «-». - if [ -n "$SALIDA" ]; then - grim -t png -o "$SALIDA" - | reducir - else - grim -t png - | reducir - fi -elif [ -n "${WAYLAND_DISPLAY:-}" ] && command -v spectacle >/dev/null 2>&1; then - TMP=$(mktemp --suffix=.png); trap 'rm -f "$TMP"' EXIT - spectacle -b -n -f -o "$TMP" >/dev/null 2>&1 - reducir < "$TMP" -elif [ -n "${DISPLAY:-}" ] && command -v maim >/dev/null 2>&1; then - maim --format=png /dev/stdout | reducir -elif [ -n "${DISPLAY:-}" ] && command -v import >/dev/null 2>&1; then - # ImageMagick, presente en casi cualquier X11. - import -silent -window root png:- | reducir -elif [ -n "${DISPLAY:-}" ] && command -v scrot >/dev/null 2>&1; then - scrot -o /dev/stdout | reducir -else - falta "instala grim (Wayland) o maim/imagemagick (X11)" -fi diff --git a/scripts/capture-screen.sh b/scripts/capture-screen.sh new file mode 100755 index 0000000..40bb860 --- /dev/null +++ b/scripts/capture-screen.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Captures the screen and writes a JPEG to standard output. +# +# scripts/capture-screen.sh [width] [output] +# +# width Width to scale the capture down to. Defaults to 1280, the +# measured minimum for the model to read UI text without making +# it up (see docs/RENDIMIENTO.md). +# output A specific monitor. Empty = everything there is. +# +# It lives outside the binary so supporting a new graphical environment means +# editing this script, not recompiling. Detection goes from most specific to +# most general. +set -euo pipefail + +WIDTH="${1:-1280}" +OUTPUT="${2:-}" + +missing() { echo "nothing to capture the screen with: $1" >&2; exit 1; } + +# Scaling happens here and not in the binary: shrinking before sending the +# image to the model is what brings the cost from 23 s down to 7 s, and ffmpeg +# is already needed for the camera. +shrink() { + if command -v ffmpeg >/dev/null 2>&1; then + ffmpeg -hide_banner -loglevel error -i - \ + -vf "scale=${WIDTH}:-2:flags=lanczos" -q:v 3 -f image2 -c:v mjpeg - + else + cat # without ffmpeg it goes full size: slow, but it works + fi +} + +if [ -n "${WAYLAND_DISPLAY:-}" ] && command -v grim >/dev/null 2>&1; then + # wlroots: Hyprland, Sway, river… grim writes to stdout with «-». + if [ -n "$OUTPUT" ]; then + grim -t png -o "$OUTPUT" - | shrink + else + grim -t png - | shrink + fi +elif [ -n "${WAYLAND_DISPLAY:-}" ] && command -v spectacle >/dev/null 2>&1; then + TMP=$(mktemp --suffix=.png); trap 'rm -f "$TMP"' EXIT + spectacle -b -n -f -o "$TMP" >/dev/null 2>&1 + shrink < "$TMP" +elif [ -n "${DISPLAY:-}" ] && command -v maim >/dev/null 2>&1; then + maim --format=png /dev/stdout | shrink +elif [ -n "${DISPLAY:-}" ] && command -v import >/dev/null 2>&1; then + # ImageMagick, present on almost any X11. + import -silent -window root png:- | shrink +elif [ -n "${DISPLAY:-}" ] && command -v scrot >/dev/null 2>&1; then + scrot -o /dev/stdout | shrink +else + missing "install grim (Wayland) or maim/imagemagick (X11)" +fi diff --git a/scripts/clonar-voz.sh b/scripts/clonar-voz.sh deleted file mode 100755 index e68215d..0000000 --- a/scripts/clonar-voz.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash -# Extrae los latentes de una voz a partir de una grabación, para que el -# asistente hable con ella. -# -# scripts/clonar-voz.sh grabacion.wav transcripcion.txt [nombre] -# -# Produce assets/voices/<nombre>.{spk,rvq,txt}, que es lo que el asistente -# registra en tts-server al arrancar. Se hace una vez: en cada arranque se -# mandan los latentes ya extraídos y no la grabación. -set -euo pipefail - -cd "$(dirname "$0")/.." - -WAV="${1:?uso: clonar-voz.sh <grabacion.wav> <transcripcion.txt> [nombre]}" -TXT="${2:?falta la transcripción de la grabación}" -NOMBRE="${3:-asistente}" - -CODEC=vendor/qwentts.cpp/build/qwen-codec -TOKENIZER=models/qwen-tokenizer-12hz-Q8_0.gguf - -for f in "$CODEC" "$TOKENIZER" "$WAV" "$TXT"; do - [ -e "$f" ] || { echo "falta: $f" >&2; exit 1; } -done - -SALIDA="assets/voices" -mkdir -p "$SALIDA" - -echo "Extrayendo la voz de $WAV…" -# --talker produce el embedding del hablante (.spk) y los códigos de -# referencia (.rvq); con la transcripción, el servidor activa el clonado ICL, -# que se parece bastante más al original que sólo el embedding. -"$CODEC" --model "$TOKENIZER" --talker \ - --input "$WAV" \ - -o "$SALIDA/$NOMBRE" - -cp "$TXT" "$SALIDA/$NOMBRE.txt" - -echo -echo "Listo:" -ls -la "$SALIDA/$NOMBRE".{spk,rvq,txt} -echo -echo "Apunta config/asistente.toml a esta voz:" -echo " [tts.reference]" -echo " name = \"$NOMBRE\"" -echo " speaker = \"../$SALIDA/$NOMBRE.spk\"" -echo " codes = \"../$SALIDA/$NOMBRE.rvq\"" -echo " transcript = \"../$SALIDA/$NOMBRE.txt\"" diff --git a/scripts/clone-voice.sh b/scripts/clone-voice.sh new file mode 100755 index 0000000..348b076 --- /dev/null +++ b/scripts/clone-voice.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Extracts the latents of a voice from a recording, so the assistant can +# speak with it. +# +# scripts/clone-voice.sh recording.wav transcript.txt [name] +# +# Produces assets/voices/<name>.{spk,rvq,txt}, which is what the assistant +# registers with tts-server at startup. It is done once: on every start the +# already extracted latents are sent, not the recording. +set -euo pipefail + +cd "$(dirname "$0")/.." + +WAV="${1:?usage: clone-voice.sh <recording.wav> <transcript.txt> [name]}" +TXT="${2:?missing the transcript of the recording}" +NAME="${3:-asistente}" + +CODEC=vendor/qwentts.cpp/build/qwen-codec +TOKENIZER=models/qwen-tokenizer-12hz-Q8_0.gguf + +for f in "$CODEC" "$TOKENIZER" "$WAV" "$TXT"; do + [ -e "$f" ] || { echo "missing: $f" >&2; exit 1; } +done + +OUT="assets/voices" +mkdir -p "$OUT" + +echo "Extracting the voice from $WAV…" +# --talker produces the speaker embedding (.spk) and the reference codes +# (.rvq); with the transcript, the server enables ICL cloning, which sounds a +# lot closer to the original than the embedding alone. +"$CODEC" --model "$TOKENIZER" --talker \ + --input "$WAV" \ + -o "$OUT/$NAME" + +cp "$TXT" "$OUT/$NAME.txt" + +echo +echo "Done:" +ls -la "$OUT/$NAME".{spk,rvq,txt} +echo +echo "Point config/asistente.toml at this voice:" +echo " [tts.reference]" +echo " name = \"$NAME\"" +echo " speaker = \"../$OUT/$NAME.spk\"" +echo " codes = \"../$OUT/$NAME.rvq\"" +echo " transcript = \"../$OUT/$NAME.txt\"" diff --git a/scripts/search-ddgs.sh b/scripts/search-ddgs.sh new file mode 100755 index 0000000..2f818b7 --- /dev/null +++ b/scripts/search-ddgs.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Searches with ddgs and writes the results as JSON to standard output. +# +# scripts/search-ddgs.sh "<query>" [number of results] +# +# ddgs queries several engines (DuckDuckGo, Brave, Mojeek, Startpage…) with no +# key needed. This script exists so the assistant configuration does not have +# to know anything about uv or Python environments: a usable ddgs is found +# here and the output is normalized. +# +# Beware of two things that look equivalent and are not: +# · `curl` against html.duckduckgo.com returns HTTP 202 with an anti-bot +# page and no results. Measured, not assumed. +# · The keyless api.duckduckgo.com API (Instant Answer) returns nothing for +# almost anything that is not an encyclopedia entity. +# The ddgs library does work because it rotates engines and headers. +set -euo pipefail + +QUERY="${1:?usage: search-ddgs.sh \"<query>\" [n]}" +MAX="${2:-5}" + +PROGRAM=$(cat <<'PY' +import json, sys +from ddgs import DDGS +query, max_results = sys.argv[1], int(sys.argv[2]) +try: + rows = DDGS().text(query, max_results=max_results) +except Exception as e: # noqa: BLE001 + print(json.dumps({"error": f"{type(e).__name__}: {e}"}), file=sys.stderr) + raise SystemExit(1) +print(json.dumps(rows, ensure_ascii=False)) +PY +) + +# 1) The `uv tool install ddgs` environment: the fast path, since the package +# does not have to be resolved on every search. +TOOL_PY="$HOME/.local/share/uv/tools/ddgs/bin/python" +if [ -x "$TOOL_PY" ]; then + exec "$TOOL_PY" -c "$PROGRAM" "$QUERY" "$MAX" +fi + +# 2) A system Python that already has ddgs installed. +if python3 -c "import ddgs" >/dev/null 2>&1; then + exec python3 -c "$PROGRAM" "$QUERY" "$MAX" +fi + +# 3) uvx, which downloads it on the fly. Works without installing anything, +# but adds several seconds to every search: better `uv tool install ddgs`. +if command -v uvx >/dev/null 2>&1; then + exec uvx --quiet --from ddgs python -c "$PROGRAM" "$QUERY" "$MAX" +fi + +echo '{"error":"ddgs is not available. Install it with: uv tool install ddgs"}' >&2 +exit 1 diff --git a/scripts/servers.sh b/scripts/servers.sh new file mode 100755 index 0000000..0cf662a --- /dev/null +++ b/scripts/servers.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Starts or stops the two servers on their own, without the assistant. +# +# Handy while developing: loading the models takes more than a minute, and +# this way the Rust binary can be restarted as often as needed without paying +# for it again. The assistant detects they are already listening and reuses them. +# +# scripts/servers.sh start | stop | status +set -euo pipefail +cd "$(dirname "$0")/.." + +LOGS="${ASIST_LOGS:-logs}" +LLM_PORT="${ASIST_LLM_PORT:-${ASIST_LLM_PUERTO:-8012}}" +TTS_PORT="${ASIST_TTS_PORT:-${ASIST_TTS_PUERTO:-8013}}" +mkdir -p "$LOGS" + +alive() { curl -sf -m 2 "http://127.0.0.1:$1/health" >/dev/null 2>&1; } + +wait_for() { # port name seconds + local end=$(( SECONDS + $3 )) + while [ $SECONDS -lt $end ]; do + alive "$1" && { echo " $2 ready"; return 0; } + sleep 1 + done + echo " $2 did NOT answer within $3 s; see $LOGS/$2.log" >&2 + return 1 +} + +start() { + if alive "$LLM_PORT"; then + echo " llama-server was already up" + else + echo " starting llama-server…" + nohup vendor/llama.cpp/build/bin/llama-server \ + --model models/Qwen3.5-2B.Q5_K_M.gguf \ + --mmproj models/mmproj-BF16.gguf \ + --host 127.0.0.1 --port "$LLM_PORT" \ + --threads 10 --threads-batch 10 \ + --batch-size 512 --ubatch-size 256 \ + --gpu-layers 10 --split-mode layer --tensor-split 1 --main-gpu 0 \ + --no-mmap --ctx-size 8192 --parallel 2 --cache-ram 6144 \ + --rope-freq-base 1000000 --rope-freq-scale 0.25 \ + --jinja --chat-template-file config/qwen35-no-think.jinja \ + > "$LOGS/llama-server.log" 2>&1 & + fi + + if alive "$TTS_PORT"; then + echo " tts-server was already up" + else + echo " starting tts-server…" + # --codec-chunk-dur 1.0 is what makes audio come out in blocks as it is + # generated instead of all at the end. See docs/RENDIMIENTO.md. + nohup vendor/qwentts.cpp/build/tts-server \ + --model models/qwen-talker-1.7b-base-Q8_0.gguf \ + --codec models/qwen-tokenizer-12hz-Q8_0.gguf \ + --host 127.0.0.1 --port "$TTS_PORT" \ + --lang spanish --codec-chunk-dur 1.0 \ + > "$LOGS/tts-server.log" 2>&1 & + fi + + wait_for "$LLM_PORT" llama-server 240 + wait_for "$TTS_PORT" tts-server 240 +} + +stop() { + # SIGTERM, not SIGKILL: they need a chance to release the GPU. + pkill -TERM -f 'llama-server .*--port '"$LLM_PORT" 2>/dev/null && echo " llama-server stopped" || true + pkill -TERM -f 'tts-server .*--port '"$TTS_PORT" 2>/dev/null && echo " tts-server stopped" || true +} + +status() { + alive "$LLM_PORT" && echo " llama-server up :$LLM_PORT" || echo " llama-server down" + alive "$TTS_PORT" && echo " tts-server up :$TTS_PORT" || echo " tts-server down" + command -v nvidia-smi >/dev/null && \ + nvidia-smi --query-gpu=memory.used,memory.total --format=csv,noheader | sed 's/^/ GPU: /' +} + +# The Spanish verbs (arrancar/parar/estado) are still accepted. +case "${1:-status}" in + start|arrancar) start ;; + stop|parar) stop ;; + status|estado) status ;; + *) echo "usage: $0 {start|stop|status}" >&2; exit 1 ;; +esac diff --git a/scripts/servidores.sh b/scripts/servidores.sh deleted file mode 100755 index 2e3b769..0000000 --- a/scripts/servidores.sh +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env bash -# Arranca o para los dos servidores por separado, sin el asistente. -# -# Útil para desarrollar: cargar los modelos cuesta más de un minuto y así se -# reinicia el binario de Rust las veces que haga falta sin volver a pagarlo. -# El asistente detecta que ya están escuchando y los reutiliza. -# -# scripts/servidores.sh arrancar | parar | estado -set -euo pipefail -cd "$(dirname "$0")/.." - -LOGS="${ASIST_LOGS:-logs}" -LLM_PUERTO="${ASIST_LLM_PUERTO:-8012}" -TTS_PUERTO="${ASIST_TTS_PUERTO:-8013}" -mkdir -p "$LOGS" - -vivo() { curl -sf -m 2 "http://127.0.0.1:$1/health" >/dev/null 2>&1; } - -esperar() { # puerto nombre segundos - local fin=$(( SECONDS + $3 )) - while [ $SECONDS -lt $fin ]; do - vivo "$1" && { echo " $2 listo"; return 0; } - sleep 1 - done - echo " $2 NO respondió en $3 s; mira $LOGS/$2.log" >&2 - return 1 -} - -arrancar() { - if vivo "$LLM_PUERTO"; then - echo " llama-server ya estaba arriba" - else - echo " arrancando llama-server…" - nohup vendor/llama.cpp/build/bin/llama-server \ - --model models/Qwen3.5-2B.Q5_K_M.gguf \ - --mmproj models/mmproj-BF16.gguf \ - --host 127.0.0.1 --port "$LLM_PUERTO" \ - --threads 10 --threads-batch 10 \ - --batch-size 512 --ubatch-size 256 \ - --gpu-layers 10 --split-mode layer --tensor-split 1 --main-gpu 0 \ - --no-mmap --ctx-size 8192 --parallel 2 --cache-ram 6144 \ - --rope-freq-base 1000000 --rope-freq-scale 0.25 \ - --jinja --chat-template-file config/qwen35-no-think.jinja \ - > "$LOGS/llama-server.log" 2>&1 & - fi - - if vivo "$TTS_PUERTO"; then - echo " tts-server ya estaba arriba" - else - echo " arrancando tts-server…" - # --codec-chunk-dur 1.0 es lo que hace que el audio salga por bloques - # según se genera en vez de todo al final. Ver docs/RENDIMIENTO.md. - nohup vendor/qwentts.cpp/build/tts-server \ - --model models/qwen-talker-1.7b-base-Q8_0.gguf \ - --codec models/qwen-tokenizer-12hz-Q8_0.gguf \ - --host 127.0.0.1 --port "$TTS_PUERTO" \ - --lang spanish --codec-chunk-dur 1.0 \ - > "$LOGS/tts-server.log" 2>&1 & - fi - - esperar "$LLM_PUERTO" llama-server 240 - esperar "$TTS_PUERTO" tts-server 240 -} - -parar() { - # SIGTERM, no SIGKILL: hay que darles ocasión de soltar la GPU. - pkill -TERM -f 'llama-server .*--port '"$LLM_PUERTO" 2>/dev/null && echo " llama-server parado" || true - pkill -TERM -f 'tts-server .*--port '"$TTS_PUERTO" 2>/dev/null && echo " tts-server parado" || true -} - -estado() { - vivo "$LLM_PUERTO" && echo " llama-server arriba :$LLM_PUERTO" || echo " llama-server parado" - vivo "$TTS_PUERTO" && echo " tts-server arriba :$TTS_PUERTO" || echo " tts-server parado" - command -v nvidia-smi >/dev/null && \ - nvidia-smi --query-gpu=memory.used,memory.total --format=csv,noheader | sed 's/^/ GPU: /' -} - -case "${1:-estado}" in - arrancar|start) arrancar ;; - parar|stop) parar ;; - estado|status) estado ;; - *) echo "uso: $0 {arrancar|parar|estado}" >&2; exit 1 ;; -esac |