File size: 15,861 Bytes
2695555
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
#!/bin/bash
# Configuração do openTARS: instala o que falta pra ele rodar (Ollama,
# ambiente Python isolado com as dependências, modelos do Ollama; nenhum
# ajudante desde a 3.0.5: a helper Murph vem no pacote; desde a 3.1, usa as
# IAs que outros programas já têm; desde a 4.0, baixa o Gargantua, a IA
# oficial; na Endurance Neo, nada de modelo de embeddings) e NUNCA reinstala
# algo que já está presente no PC.
# Roda automaticamente depois do "apt install" (chamado pelo postinst)
# e pode ser rodado de novo a qualquer momento com "opentars --setup".
#
# De propósito, NUNCA sai com erro: se o dpkg visse esse script falhar,
# deixaria o pacote marcado como "quebrado" mesmo com o openTARS
# utilizável. Cada etapa arriscada (rede, permissões) avisa e segue.

set -uo pipefail

# Ajudantes das versões antigas (não são mais baixados nem usados): se já
# estiverem instalados, só não contam como modelo de conversa.
MODELO_AJUDANTE="qwen2.5:0.5b"
MODELO_AJUDANTE_ANTIGO="gemma3:270m"
DIR_OPENTARS="/usr/share/opentars"
VENV="$DIR_OPENTARS/venv"
DEPENDENCIAS=("requests:requests" "psutil:psutil" "pyautogui:pyautogui" "PIL:pillow" "PySide6:PySide6-Essentials" "numpy:numpy" "onnxruntime:onnxruntime")

# Textos no idioma do sistema: vêm de idiomas/*.json, pelo mesmo módulo
# que o openTARS usa (tars_i18n.py). Se algo falhar, aparecem as chaves
# em vez das frases — a instalação nunca quebra por causa disso.
eval "$(python3 "$DIR_OPENTARS/tars_i18n.py" --shell setup. 2>/dev/null)" 2>/dev/null || true

# t chave [campo=valor ...]  ->  frase do idioma, com os {campos} preenchidos
t() {
    local var="T_setup_$1" texto par
    texto="${!var:-$1}"
    shift
    for par in "$@"; do
        texto="${texto//"{${par%%=*}}"/${par#*=}}"
    done
    printf '%s' "$texto"
}

msg()   { echo "[openTARS] $*"; }
aviso() { echo "[openTARS] $(t aviso): $*"; }

echo ""
echo "$(t titulo)"
echo ""

OLLAMA_URL="http://127.0.0.1:11434"
# Respeita OLLAMA_HOST (o mesmo do próprio Ollama): "0.0.0.0", "host:porta" ou URL
if [ -n "${OLLAMA_HOST:-}" ]; then
    OLLAMA_URL="$OLLAMA_HOST"
    case "$OLLAMA_URL" in *://*) ;; *) OLLAMA_URL="http://$OLLAMA_URL" ;; esac
    case "${OLLAMA_URL#*://}" in *:*) ;; *) OLLAMA_URL="$OLLAMA_URL:11434" ;; esac
    OLLAMA_URL="${OLLAMA_URL/\/\/0.0.0.0/\/\/127.0.0.1}"
fi

ollama_responde() {
    curl -fsS --max-time 3 "$OLLAMA_URL/api/version" >/dev/null 2>&1
}

# 4.5: WSL. O Ollama costuma ficar no Windows, que o WSL2 alcança pelo IP dele
# (servidor DNS e gateway do WSL), não pelo 127.0.0.1.
em_wsl() {
    grep -qi microsoft /proc/version 2>/dev/null || [ -n "${WSL_DISTRO_NAME:-}" ]
}

ollama_do_windows() {
    local ip
    for ip in $(awk '/^nameserver/ {print $2}' /etc/resolv.conf 2>/dev/null) $(ip route 2>/dev/null | awk '/^default/ {print $3}'); do
        case "$ip" in 127.*|"") continue ;; esac
        if curl -fsS --max-time 2 "http://$ip:11434/api/version" >/dev/null 2>&1; then
            echo "http://$ip:11434"
            return 0
        fi
    done
    return 1
}

if em_wsl; then
    msg "$(t wsl_detectado)"
    if [ -z "${OLLAMA_HOST:-}" ] && ! ollama_responde && windows_url="$(ollama_do_windows)"; then
        OLLAMA_URL="$windows_url"
        msg "$(t ollama_windows url="$OLLAMA_URL")"
    fi
fi
# O comando "ollama" (pull) fala com o mesmo servidor que o resto deste script
export OLLAMA_HOST="${OLLAMA_URL#*://}"

# Nomes dos modelos baixados, um por linha (via API: funciona mesmo com
# o Ollama rodando em Docker, sem o comando "ollama" no PC).
modelos_instalados() {
    curl -fsS --max-time 5 "$OLLAMA_URL/api/tags" 2>/dev/null \
        | grep -o '"name"[[:space:]]*:[[:space:]]*"[^"]*"' | cut -d'"' -f4
}

# ------------------------------------------------------------------
# 1. Ollama — só instala se ainda não existir.
#
# Um Ollama que já responde na porta padrão (instalado de outro jeito,
# em Docker, via Snap...) conta como instalado: instalar outro por cima
# criaria um segundo serviço brigando pela mesma porta.
# ------------------------------------------------------------------
if command -v ollama >/dev/null 2>&1; then
    msg "$(t ollama_ja)"
elif ollama_responde; then
    msg "$(t ollama_existente url="$OLLAMA_URL")"
else
    msg "$(t ollama_instalando)"
    # Baixa pro disco e só então executa: com "curl | sh" o status do
    # pipe é o do "sh", que "funciona" mesmo se o download falhar.
    instalador="$(mktemp)"
    if curl -fsSL https://ollama.com/install.sh -o "$instalador" && sh "$instalador"; then
        msg "$(t ollama_ok)"
    else
        aviso "$(t ollama_falhou)"
        echo "        $(t ollama_manual)"
    fi
    rm -f "$instalador"
fi

# ------------------------------------------------------------------
# 2. Ambiente Python isolado.
#
# Um venv próprio evita o conflito do pyautogui com o setuptools do
# sistema em distros recentes (Ubuntu 24.04+, Zorin 17+).
# --system-site-packages deixa o venv enxergar o tkinter, que só existe
# como pacote do sistema (python3-tk), nunca via pip.
#
# Se o venv existe mas o Python dele não roda mais (ex: atualização da
# distro trocou a versão do Python), ele é recriado do zero.
# ------------------------------------------------------------------
# Escolhe o Python base: o "python3" padrão, a menos que ele não tenha
# tkinter e outro python3.X instalado tenha (acontece quando o python3
# do PATH foi trocado e o pacote python3-tk é da versão padrão da distro).
escolher_python() {
    local candidato
    for candidato in python3 /usr/bin/python3 $(ls /usr/bin/python3.[0-9]* 2>/dev/null | grep -E 'python3\.[0-9]+$' | sort -t. -k2 -n -r); do
        command -v "$candidato" >/dev/null 2>&1 || continue
        "$candidato" -c "import sys; sys.exit(0 if sys.version_info >= (3, 8) else 1)" 2>/dev/null || continue
        "$candidato" -c "import venv, tkinter" >/dev/null 2>&1 && { echo "$candidato"; return; }
    done
    echo python3
}

venv_ok() {
    [ -x "$VENV/bin/python3" ] && "$VENV/bin/python3" -c "pass" >/dev/null 2>&1
}

# Venv criado com um Python sem tkinter enquanto existe outro com tkinter
# (ex: python3-tk instalado depois): recria pra liberar a interface gráfica.
PYTHON_BASE="$(escolher_python)"
if venv_ok && ! "$VENV/bin/python3" -c "import tkinter" >/dev/null 2>&1 \
        && "$PYTHON_BASE" -c "import tkinter" >/dev/null 2>&1; then
    msg "$(t venv_sem_tk python="$PYTHON_BASE")"
    rm -rf "$VENV"
fi

if venv_ok; then
    msg "$(t venv_ja)"
else
    [ -e "$VENV" ] && msg "$(t venv_quebrado)"
    rm -rf "$VENV"
    msg "$(t venv_criando versao="$("$PYTHON_BASE" --version 2>&1)")"
    if "$PYTHON_BASE" -m venv --system-site-packages "$VENV"; then
        "$VENV/bin/pip" install --quiet --upgrade pip setuptools wheel \
            || aviso "$(t pip_falhou)"
        msg "$(t venv_ok)"
    else
        aviso "$(t venv_falhou)"
        rm -rf "$VENV"
    fi
fi

if [ -x "$VENV/bin/python3" ]; then
    msg "$(t deps_verificando)"

    faltando=()
    for par in "${DEPENDENCIAS[@]}"; do
        modulo="${par%%:*}"
        pacote="${par##*:}"
        # find_spec só checa se o módulo existe, sem executá-lo. Um
        # "import" real do pyautogui lê $DISPLAY na hora e dá KeyError
        # sem sessão gráfica (como durante o próprio postinst).
        if ! "$VENV/bin/python3" -c "import importlib.util,sys; sys.exit(0 if importlib.util.find_spec('$modulo') else 1)" >/dev/null 2>&1; then
            faltando+=("$pacote")
        fi
    done

    if [ "${#faltando[@]}" -eq 0 ]; then
        msg "$(t deps_ok)"
    else
        msg "$(t deps_instalando pacotes="${faltando[*]}")"
        if ! "$VENV/bin/pip" install --quiet "${faltando[@]}"; then
            aviso "$(t deps_falhou)"
        fi
    fi

    if "$VENV/bin/python3" -c "import tkinter" >/dev/null 2>&1; then
        msg "$(t tk_ok)"
    else
        aviso "$(t tk_falta)"
        echo "        $(t tk_instale)"
        echo "        $(t tk_terminal)"
    fi

    # Clicar nos botões pelo nome (árvore de acessibilidade, AT-SPI).
    if "$VENV/bin/python3" -c "import gi; gi.require_version('Atspi', '2.0'); from gi.repository import Atspi" >/dev/null 2>&1; then
        msg "$(t acess_ok)"
    else
        aviso "$(t acess_falta)"
        echo "        $(t acess_instale)"
    fi
else
    aviso "$(t sem_venv)"
fi

# ------------------------------------------------------------------
# 3. Serviço do Ollama pronto (a helper Murph já vem no pacote).
# ------------------------------------------------------------------
servico_pronto=0

# Recém-instalado, o serviço leva um instante pra subir. Se o Ollama
# existe mas o serviço está parado (sem systemd, por exemplo), tenta
# iniciar.
if ! ollama_responde && command -v systemctl >/dev/null 2>&1 \
        && systemctl list-unit-files ollama.service >/dev/null 2>&1; then
    systemctl start ollama >/dev/null 2>&1 || true
fi

for _ in $(seq 1 10); do
    if ollama_responde; then
        servico_pronto=1
        break
    fi
    sleep 2
done

# 3.0.5: nenhum modelo ajudante é baixado. Quem decide o tipo de cada
# pedido é a helper Murph, que já vem no pacote (modelos/murph.json).
msg "$(t helper_murph versao="1.0")"
if [ "$servico_pronto" -eq 0 ]; then
    aviso "$(t ollama_nao_respondeu url="$OLLAMA_URL")"
    # 4.5: WSL (e contêiner) costuma não ter systemd: o serviço não sobe sozinho
    if em_wsl || ! command -v systemctl >/dev/null 2>&1; then
        echo "        $(t ollama_serve_manual)"
    fi
fi

# ------------------------------------------------------------------
# 3d. Gargantua (4.0): a IA oficial do openTARS, treinada pra usar as ferramentas
#     dele. Desde 18/10 é o Gargantua 2 (Qwen3.5 2B, ~1,6 GB, enxerga a tela: o Cooper,
#     modelo à parte, foi descontinuado). Baixa só se ainda não tiver.
#     Com ele instalado, o passo 4 não baixa outro modelo de conversa.
#     TARS_SEM_GARGANTUA=1 pula; TARS_MODELO_GARGANTUA=<nome> troca.
# ------------------------------------------------------------------
MODELO_GARGANTUA="${TARS_MODELO_GARGANTUA:-ProjectOpenTARS/Gargantua-2}"

# 5.1: máquina fraca (sem placa NVIDIA com 4 GB E com até 6 GB de RAM OU menos de 4 threads) recebe o Gargantua Lite
# (Qwen3.5 0,8B com visão, 0,6 GB, Gargantua-2:lite). TARS_GARGANTUA=lite|completo força. A mesma regra do primeiro uso (tars_primeiro_uso.py).
maquina_fraca() {
    case "${TARS_GARGANTUA:-}" in lite) return 0 ;; completo) return 1 ;; esac
    local vram=0 ram threads
    if command -v nvidia-smi >/dev/null 2>&1; then
        vram="$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits 2>/dev/null | sort -n | tail -1 | cut -d. -f1)"
    fi
    [ "${vram:-0}" -ge 4000 ] 2>/dev/null && return 1
    ram="$(awk '/^MemTotal:/ {print int($2/1024)}' /proc/meminfo 2>/dev/null)"
    threads="$(nproc 2>/dev/null || echo 4)"
    [ "${ram:-99999}" -le 6200 ] || [ "${threads:-4}" -lt 4 ]
}
if [ -z "${TARS_MODELO_GARGANTUA:-}" ] && maquina_fraca; then
    MODELO_GARGANTUA="ProjectOpenTARS/Gargantua-2:lite"
fi

gargantua_instalado() {
    # Qualquer "gargantua" conta (o oficial ou um treinado no próprio PC).
    modelos_instalados | awk -F/ '{print tolower($NF)}' | grep -q '^gargantua'
}

if [ "$servico_pronto" -eq 1 ]; then
    if [ "${TARS_SEM_GARGANTUA:-0}" = "1" ]; then
        msg "$(t gargantua_pulado)"
    elif gargantua_instalado; then
        msg "$(t gargantua_ja modelo="$(modelos_instalados | grep -i 'gargantua' | head -1)")"
    else
        msg "$(t gargantua_baixando modelo="$MODELO_GARGANTUA")"
        if [ -t 1 ]; then
            # num terminal ("dpkg -i", "apt install", "opentars --setup"): baixa aqui, com a barra do Ollama
            msg "$(t pode_demorar)"
            if command -v ollama >/dev/null 2>&1; then
                ollama pull "$MODELO_GARGANTUA" || true
            else
                curl -fsS --max-time 7200 "$OLLAMA_URL/api/pull" \
                    -d "{\"model\":\"$MODELO_GARGANTUA\",\"stream\":false}" >/dev/null || true
            fi
            if gargantua_instalado; then
                msg "$(t baixado modelo="$MODELO_GARGANTUA")"
            else
                aviso "$(t nao_baixou modelo="$MODELO_GARGANTUA")"
            fi
        else
            # SEM terminal (loja de aplicativos, instalação automática, atualização pelo próprio openTARS): um pull aqui
            # segura a instalação por minutos sem mostrar nada, e a barra da loja fica parada em 0% enquanto o
            # Ollama já está em 50%. Baixa em segundo plano, solto da instalação; o assistente de primeiro uso do
            # openTARS acompanha o mesmo download (o Ollama junta os pedidos) e mostra a barra de verdade.
            msg "$(t gargantua_segundo_plano)"
            if command -v ollama >/dev/null 2>&1; then
                ( setsid nohup ollama pull "$MODELO_GARGANTUA" >/dev/null 2>&1 < /dev/null & )
            else
                ( setsid nohup curl -fsS --max-time 7200 "$OLLAMA_URL/api/pull" \
                    -d "{\"model\":\"$MODELO_GARGANTUA\",\"stream\":false}" >/dev/null 2>&1 < /dev/null & )
            fi
        fi
    fi
fi

# ------------------------------------------------------------------
# 4. Modelo de conversa: sem nenhum, o openTARS abre mas não tem quem
#    responda. Baixa UM, do tamanho certo pro PC, só se não houver
#    nenhum ainda (quem já tem modelos escolheu os seus).
#    TARS_MODELO_CONVERSA=<modelo> escolhe outro; TARS_SEM_MODELO=1 pula.
# ------------------------------------------------------------------
vram_gb() {
    local total=0
    if command -v nvidia-smi >/dev/null 2>&1; then
        total=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits 2>/dev/null \
            | awk '{s+=$1} END {printf "%d", s/1024}')
    fi
    if [ "${total:-0}" -eq 0 ]; then
        for d in /sys/class/drm/card[0-9]*/device; do
            [ "$(cat "$d/vendor" 2>/dev/null)" = "0x1002" ] || continue
            local b; b=$(cat "$d/mem_info_vram_total" 2>/dev/null || echo 0)
            [ "$b" -ge 2147483648 ] && total=$((total + b / 1073741824))
        done
    fi
    echo "${total:-0}"
}

ram_gb() {
    awk '/MemTotal/ {printf "%d", $2/1048576}' /proc/meminfo 2>/dev/null || echo 0
}

modelo_de_conversa_sugerido() {
    local vram ram
    vram=$(vram_gb); ram=$(ram_gb)
    if [ "$vram" -ge 6 ]; then
        echo "qwen3:8b"        # ~5 GB, cabe na placa
    elif [ "$ram" -ge 12 ]; then
        echo "qwen3:4b"        # ~2.5 GB, roda bem na CPU
    else
        echo "qwen3:1.7b"      # ~1.4 GB, pra PC com pouca memória
    fi
}

contar_modelos_de_conversa() {
    modelos_instalados | grep -vx "$MODELO_AJUDANTE" | grep -vx "$MODELO_AJUDANTE_ANTIGO" \
        | grep -viE 'embed|minilm|bge-|cooper' | wc -l
}

if [ "$servico_pronto" -eq 1 ]; then
    outros=$(contar_modelos_de_conversa)
    if [ "$outros" -gt 0 ]; then
        msg "$(t ja_tem_modelos n="$outros")"
    elif [ "${TARS_SEM_MODELO:-0}" = "1" ]; then
        aviso "$(t sem_modelo_pedido)"
    else
        MODELO_CONVERSA="${TARS_MODELO_CONVERSA:-$(modelo_de_conversa_sugerido)}"
        msg "$(t baixando_conversa modelo="$MODELO_CONVERSA" vram="$(vram_gb)" ram="$(ram_gb)")"
        msg "$(t pode_demorar)"
        if command -v ollama >/dev/null 2>&1; then
            ollama pull "$MODELO_CONVERSA" || true
        else
            curl -fsS --max-time 7200 "$OLLAMA_URL/api/pull" \
                -d "{\"model\":\"$MODELO_CONVERSA\",\"stream\":false}" >/dev/null || true
        fi
        if modelos_instalados | grep -qx "$MODELO_CONVERSA"; then
            msg "$(t baixado modelo="$MODELO_CONVERSA")"
        else
            aviso "$(t nao_baixou modelo="$MODELO_CONVERSA")"
        fi
    fi
else
    aviso "$(t sem_ollama_modelo)"
fi

echo ""
echo "$(t pronto)"
echo "$(t dica_atalho)"
echo ""

exit 0