aboutsummaryrefslogtreecommitdiffstats
path: root/scripts
diff options
context:
space:
mode:
authorelvis <elvis@claros.ar>2026-09-26 20:20:19 -0300
committerelvis <elvis@claros.ar>2026-09-26 20:20:19 -0300
commit8518a63f55153e7f45fd49ad6caff5555f4e374f (patch)
tree636684eea3fa6f35d78282ab95eb687ef49154af /scripts
parent69de76dc9cbedc6092d1e5ce84094a8030de1470 (diff)
downloadasist-p-8518a63f55153e7f45fd49ad6caff5555f4e374f.tar.gz
asist-p-8518a63f55153e7f45fd49ad6caff5555f4e374f.zip
Translate code, comments, logs and terminal UI to English; add English README; rename scriptsHEADmain
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/bootstrap.sh155
-rwxr-xr-xscripts/buscar-ddgs.sh54
-rwxr-xr-xscripts/capturar-pantalla.sh52
-rwxr-xr-xscripts/capture-screen.sh53
-rwxr-xr-xscripts/clonar-voz.sh47
-rwxr-xr-xscripts/clone-voice.sh47
-rwxr-xr-xscripts/search-ddgs.sh54
-rwxr-xr-xscripts/servers.sh84
-rwxr-xr-xscripts/servidores.sh83
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