Blog/latxa.sh
(Deskargatu)
#!/bin/bash
jaraitu=0
profile=""
sound=0
voice="maider"
while [ $# -gt 0 ]; do
case "$1" in
-j|--jaraitu)
jaraitu=1
shift
;;
-s|--soinua)
sound=1
shift
;;
-b|--boza)
if [ $# -lt 2 ]; then echo "Error: -b-k ahots izena behar du (adib: maider, antton, es_ES-davefx)" >&2; exit 1; fi
voice="$2"
shift 2
if [ "$voice" != "maider" ] && [ "$voice" != "antton" ]; then
if ! echo "$voice" | grep -qE '^[a-z]{2}_[A-Z]{2}-[a-z0-9]+(-(x_low|low|medium|high))?$'; then
echo "Error: '$voice' formatua ez da zuzena. Adib: es_ES-davefx" >&2
exit 1
fi
fi
;;
-p|--profile)
if [ $# -lt 2 ]; then echo "Error: -p-k profile izena behar du" >&2; exit 1; fi
profile="$2"
shift 2
if [ -n "$profile" ]; then
ok=$(python3 -c "
import sys
profiles = {'harderalat':1,'irakasle':1,'kontalari':1,'linux':1,'errima':1,'haur':1,'filosofo':1,'detektibe':1,'poeta':1}
print('1' if sys.argv[1] in profiles else '0')
" "$profile" 2>/dev/null)
if [ "$ok" != "1" ]; then
echo "Error: '$profile' profila ez da existitzen." >&2
echo "Azalpena: latxa.sh -l" >&2
exit 1
fi
fi
;;
-l|--list)
echo "Erabilgarri dauden profilak:"
python3 -c "
profiles = {
'harderalat': {'prompt': 'Erantzun euskaraz, baina beti gaztelerazko hitz bat sartu gutxienez erantzun bakoitzean. Idatzi oso modu informalean, lagunarteko hizkeran, eta erabili interneteko laburdurak eta gazteen hizkera.', 'description': 'Gazte eta informal, beti gaztelerazko hitz batekin'},
'irakasle': {'prompt': 'Itzul zaitez euskara irakasle zorrotz eta adeitsu batean. Azaldu gramatika arauak, eman adibide argiak, eta zuzendu ikaslearen akatsak modu eraikitzailean. Erabili euskara batua eta irakaslearen hizkera profesionala.', 'description': 'Euskara irakasle zorrotza'},
'kontalari': {'prompt': 'Itzul zaitez euskal kontalari zahar batean. Kontatu istorioak eta kondairak euskal mitologiaren eta tradizioaren estiloan. Erabili irudi poetikoak eta euskal esaera zaharrak. Hitzaurrea eta bukaera egin kontalariaren estiloan.', 'description': 'Euskal kontalari tradizionala'},
'linux': {'prompt': 'Linux sistemetako komandoak eta irtenbideak eman. Idatzi komando zehatzak, azaldu nola funtzionatzen duten, eta eman adibide praktikoak. Erabili hizkera tekniko argia.', 'description': 'Linux komando eta irtenbideak'},
'errima': {'prompt': 'Erantzun beti errimatutako bertsoetan, bertsolaritzaren estiloan. Erabili euskal neurketa eta errimaren arauak. Bertso bakoitzak zentzua izan behar du eta galderari erantzun.', 'description': 'Bertsotan erantzun'},
'haur': {'prompt': 'Erantzun 6-8 urteko haur batentzat ulergarria den moduan. Erabili hitz sinpleak, esaldi laburrak eta azalpen irudimentsuak. Izan dibertigarria eta jostalaria.', 'description': 'Haurrentzako azalpen sinpleak'},
'filosofo': {'prompt': 'Erantzun euskal filosofo jakintsu bat izango bazina bezala. Sakondu galderaren oinarri filosofikoetan, erabili euskal esaera zaharrak eta pentsamendu abstraktu sakona. Bukaeran galdera filosofiko bat bota itzulera.', 'description': 'Euskal filosofo sakona'},
'detektibe': {'prompt': 'Itzul zaitez detektibe euskaldun zorrotz batean. Aztertu galdera froga bat izango balitz bezala. Egin galdera argigarriak eta ondorioztatu logikoki. Berbetan erabili \"Ba, ba, ba...\" eta \"Aurrera begira...\" bezalako espresioak.', 'description': 'Detektibe moduan ikertu'},
'poeta': {'prompt': 'Erantzun olerki labur batean. Erabili irudi poetikoak, metaforak eta euskal poesiaren estilo tradizionala. Olerkiak galderari erantzun behar dio. Bukaeran olerkiaren azalpen laburra gehitu parentesi artean.', 'description': 'Olerki moduan erantzun'},
}
for n, p in profiles.items():
print(f' {n}: {p[\"description\"]}')
" 2>/dev/null
exit 0
;;
-h|--help)
echo "ERABILERA: latxa.sh [-j] [-s] [-p profila] [-l] [galdera]"
echo ""
echo "GALDETU LATXARI (Eusko Jaurlaritzako euskal LLMa)"
echo ""
echo "AUKERAK:"
echo " -j, --jaraitu Aurreko solasaldia jarraitu"
echo " -s, --soinua Erantzuna ahoz ere bai (Piper TTS Docker bidez)"
echo " -b, --boza IZENA Ahotsa hautatu (maider, antton, es_ES-davefx-medium, ...)"
echo " -p, --profile IZENA Profila hautatu (adibidez: irakasle, linux, poeta)"
echo " -l, --list Profilak zerrendatu"
echo " -h, --help Laguntza hau"
echo ""
echo "ADIBIDEAK:"
echo " latxa.sh Nola da pajaro euskaraz?"
echo " latxa.sh -s Nola da pajaro euskaraz?"
echo " latxa.sh -b antton -s Nola da pajaro euskaraz?"
echo " latxa.sh -j Eta katu?"
echo " latxa.sh -p irakasle Zer esan nahi du joan aditzak?"
echo " latxa.sh -p linux Nola konprimo fitxategi bat?"
echo " latxa.sh -p poeta Idatzi poema bat udazkenaz"
echo " latxa.sh -l"
exit 0
;;
-*)
echo "Erabilera: latxa.sh [-j] [-s] [-p profila] [-l] [galdera]" >&2
echo "Ikusi latxa.sh --help laguntza gehiagorako" >&2
exit 1
;;
*) break ;;
esac
done
if [ $# -eq 0 ]; then
echo "Latxa${profile:+ ($profile)}: Zer nahi duzu?"
echo -n "Zu: " > /dev/stderr
read -r latxa_question
else
latxa_question="$*"
fi
# --- Piper TTS setup (only if -s) ---
if [ "$sound" = "1" ]; then
PIPER_DIR="$HOME/.local/share/piper"
IMAGE_NAME="latxa-piper"
if [ "$voice" = "maider" ] || [ "$voice" = "antton" ]; then
VOICE_PATH="eu_ES-${voice}-medium"
LANG_REGION="eu_ES"
NAME="$voice"
QUALITY="medium"
else
LANG_REGION=$(echo "$voice" | cut -d- -f1)
NAME=$(echo "$voice" | cut -d- -f2)
QUALITY=$(echo "$voice" | cut -d- -f3)
[ -z "$QUALITY" ] && QUALITY="medium"
VOICE_PATH="${LANG_REGION}-${NAME}-${QUALITY}"
fi
MODEL="$PIPER_DIR/$VOICE_PATH.onnx"
CONFIG="$PIPER_DIR/$VOICE_PATH.onnx.json"
mkdir -p "$PIPER_DIR"
# Dockerfile
if [ ! -f "$PIPER_DIR/Dockerfile" ]; then
cat > "$PIPER_DIR/Dockerfile" << 'DEOF'
FROM python:3.13-slim
RUN pip install --no-cache-dir piper-tts
ENTRYPOINT ["piper"]
DEOF
fi
# Build image if needed
if ! docker image inspect "$IMAGE_NAME" >/dev/null 2>&1; then
echo "Piper Docker irudia eraikitzen..." >&2
docker build -t "$IMAGE_NAME" "$PIPER_DIR" >&2
fi
# Download voice model if needed
if [ ! -s "$MODEL" ]; then
LANG="${LANG_REGION%_*}"
BASE="https://huggingface.co/rhasspy/piper-voices/resolve/main/${LANG}/${LANG_REGION}/${NAME}"
found=0
for q in "$QUALITY" high low x_low; do
[ "$q" != "$QUALITY" ] && VOICE_PATH="${LANG_REGION}-${NAME}-${q}" && MODEL="$PIPER_DIR/$VOICE_PATH.onnx" && CONFIG="$PIPER_DIR/$VOICE_PATH.onnx.json"
[ -s "$MODEL" ] && found=1 && break
echo "${NAME} ahotsa deskargatzen (${q})..." >&2
wget -q -O "$MODEL" "${BASE}/${q}/${VOICE_PATH}.onnx" >&2
if [ -s "$MODEL" ]; then
wget -q -O "$CONFIG" "${BASE}/${q}/${VOICE_PATH}.onnx.json" >&2
found=1
break
fi
echo -n "" > "$MODEL" 2>/dev/null
echo -n "" > "$CONFIG" 2>/dev/null
done
if [ "$found" = "0" ]; then
echo "Error: ezin izan da '$voice' ahotsa deskargatu" >&2
fi
fi
fi
# --- Hotsik gabeko erantzuna gordetzeko temp fitxategia ---
soundfile=""
if [ "$sound" = "1" ]; then
soundfile=$(mktemp /tmp/latxa-sound-XXXXXX.txt)
fi
python3 -c "
import sys, json, urllib.request, urllib.error, os, re
from http.cookiejar import CookieJar
question = sys.argv[1]
jaraitu = int(sys.argv[2])
profile_name = sys.argv[3] if len(sys.argv) > 3 and sys.argv[3] else ''
soundfile = sys.argv[4] if len(sys.argv) > 4 and sys.argv[4] else ''
profiles = {
'harderalat': {'prompt': 'Erantzun euskaraz, baina beti gaztelerazko hitz bat sartu gutxienez erantzun bakoitzean. Idatzi oso modu informalean, lagunarteko hizkeran, eta erabili interneteko laburdurak eta gazteen hizkera.', 'description': ''},
'irakasle': {'prompt': 'Itzul zaitez euskara irakasle zorrotz eta adeitsu batean. Azaldu gramatika arauak, eman adibide argiak, eta zuzendu ikaslearen akatsak modu eraikitzailean. Erabili euskara batua eta irakaslearen hizkera profesionala.', 'description': ''},
'kontalari': {'prompt': 'Itzul zaitez euskal kontalari zahar batean. Kontatu istorioak eta kondairak euskal mitologiaren eta tradizioaren estiloan. Erabili irudi poetikoak eta euskal esaera zaharrak. Hitzaurrea eta bukaera egin kontalariaren estiloan.', 'description': ''},
'linux': {'prompt': 'Linux sistemetako komandoak eta irtenbideak eman. Idatzi komando zehatzak, azaldu nola funtzionatzen duten, eta eman adibide praktikoak. Erabili hizkera tekniko argia.', 'description': ''},
'errima': {'prompt': 'Erantzun beti errimatutako bertsoetan, bertsolaritzaren estiloan. Erabili euskal neurketa eta errimaren arauak. Bertso bakoitzak zentzua izan behar du eta galderari erantzun.', 'description': ''},
'haur': {'prompt': 'Erantzun 6-8 urteko haur batentzat ulergarria den moduan. Erabili hitz sinpleak, esaldi laburrak eta azalpen irudimentsuak. Izan dibertigarria eta jostalaria.', 'description': ''},
'filosofo': {'prompt': 'Erantzun euskal filosofo jakintsu bat izango bazina bezala. Sakondu galderaren oinarri filosofikoetan, erabili euskal esaera zaharrak eta pentsamendu abstraktu sakona. Bukaeran galdera filosofiko bat bota itzulera.', 'description': ''},
'detektibe': {'prompt': 'Itzul zaitez detektibe euskaldun zorrotz batean. Aztertu galdera froga bat izango balitz bezala. Egin galdera argigarriak eta ondorioztatu logikoki. Berbetan erabili \"Ba, ba, ba...\" eta \"Aurrera begira...\" bezalako espresioak.', 'description': ''},
'poeta': {'prompt': 'Erantzun olerki labur batean. Erabili irudi poetikoak, metaforak eta euskal poesiaren estilo tradizionala. Olerkiak galderari erantzun behar dio. Bukaeran olerkiaren azalpen laburra gehitu parentesi artean.', 'description': ''},
}
histfile = os.path.expanduser('~/.latxa_history.json')
history = []
if jaraitu and os.path.exists(histfile):
try:
with open(histfile) as f:
history = json.load(f)
except (json.JSONDecodeError, IOError):
pass
if not history and profile_name and profile_name in profiles:
history.append({'role': 'user', 'content': '[SISTEMA: ' + profiles[profile_name]['prompt'] + ']'})
history.append({'role': 'user', 'content': question})
cj = CookieJar()
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj))
payload = json.dumps({'data': [history, None, None, None, None, None, None]}).encode()
req = urllib.request.Request('https://www.apps.euskadi.eus/latxa/gradio_api/call/chat', data=payload, headers={'Content-Type': 'application/json'})
with opener.open(req, timeout=120) as resp:
event_id = json.loads(resp.read())['event_id']
sse_url = f'https://www.apps.euskadi.eus/latxa/gradio_api/call/chat/{event_id}'
with opener.open(sse_url, timeout=120) as resp:
last_content = None
lines = []
for line_bytes in resp:
line = line_bytes.decode().strip()
if line == '':
event_type = None
data_line = None
for l in lines:
if l.startswith('event: '):
event_type = l[7:]
elif l.startswith('data: '):
data_line = l[6:]
if data_line and data_line != 'null':
try:
obj = json.loads(data_line)
msgs = obj[0]
if isinstance(msgs, list) and len(msgs) > 0:
msg = msgs[-1]
if isinstance(msg, dict) and msg.get('role') == 'assistant':
last_content = msg.get('content', '')
except json.JSONDecodeError:
pass
if event_type == 'complete':
break
lines = []
else:
lines.append(line)
if last_content:
history.append({'role': 'assistant', 'content': last_content})
try:
with open(histfile, 'w') as f:
json.dump(history, f, ensure_ascii=False)
except IOError:
pass
if soundfile:
try:
with open(soundfile, 'w') as f:
f.write(last_content)
except IOError:
pass
last_content = re.sub(r'\*\*(.+?)\*\*', r'\033[1m\1\033[0m', last_content, flags=re.DOTALL)
last_content = re.sub('\`\`\`(.+?)\`\`\`', lambda m: '\033[31m' + m.group(1).strip('\n') + '\033[0m', last_content, flags=re.DOTALL)
last_content = re.sub('\`(.+?)\`', '\033[31m\\\\1\033[0m', last_content, flags=re.DOTALL)
print(last_content)
else:
sys.exit(1)
" "$latxa_question" "$jaraitu" "$profile" "$soundfile"
# --- Piper TTS playback ---
if [ "$sound" = "1" ] && [ -s "$soundfile" ]; then
# Gelditu aurreko erreprodukzioa
if [ -f /tmp/latxa-tts.pid ]; then
kill "$(cat /tmp/latxa-tts.pid)" 2>/dev/null || true
sleep 0.2
fi
echo "" >&2
echo "Erantzuna ahoz (Piper TTS)..." >&2
(sed "s/[\"'*]//g" "$soundfile"; rm -f "$soundfile") | docker run --rm -i \
-v "$HOME/.local/share/piper:/voices" \
"latxa-piper" \
--model "/voices/$VOICE_PATH.onnx" --output-raw 2>/dev/null \
| aplay -r 22050 -f S16_LE -t raw - 2>/dev/null &
APLAY_PID=$!
echo "$APLAY_PID" > /tmp/latxa-tts.pid
fi