Tfe

Ongi etorri tfe-ren webgunera...

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