Download app/nucleo/visao.py from OpenTARS/openTARS: direct link, hf CLI and curl.
- Browser
- Download file 9.23 kB
-
https://huggingface.co/OpenTARS/openTARS/resolve/main/app/nucleo/visao.py
- Command line
-
hf download hf://OpenTARS/openTARS/app/nucleo/visao.py
-
curl -L -o visao.py https://huggingface.co/OpenTARS/openTARS/resolve/main/app/nucleo/visao.py
9.23 kB
| # openTARS — nucleo/visao.py | |
| # A visão da tela. Era o Cooper (descontinuado em 18/10/2026); agora é o próprio Gargantua 2 | |
| # (ollama pull ProjectOpenTARS/Gargantua-2), que responde no mesmo formato. As perguntas e a leitura das | |
| # respostas ficam em tars_visao.py; aqui está a conversa com o Ollama e o | |
| # encaixe no clique (por nome ou ícone) e na descrição da tela. | |
| # | |
| # Parte do núcleo: o tars.py executa este arquivo DENTRO do próprio | |
| # namespace (ver _carregar_nucleo). Usa _chat_unico e _ultimo_erro (ollama), | |
| # catalogar_modelos (hardware) e time/io/base64/re em tempo de execução. | |
| # flake8: noqa | |
| # ============================================================ | |
| # COOPER | |
| # ============================================================ | |
| # Num clique com x/y já dados pela IA, o ponto do Cooper só vale se cair a | |
| # esta distância (px) do que ela indicou; senão, vale o x/y da IA. | |
| DISTANCIA_MAXIMA_VISAO_PX = 220 | |
| # 1ª pergunta (modelo ainda carregando, às vezes trocando de lugar com outro | |
| # na placa de vídeo) tem mais prazo que as seguintes. | |
| # O prazo era 90 s e a resposta não tinha limite de tamanho: numa descrição de | |
| # tela o Cooper (2B, treinado pra respostas curtas) podia escrever sem parar | |
| # até estourar. Agora a resposta tem teto de tokens e o prazo é folgado, como | |
| # o do modelo de visão geral sempre foi. | |
| TIMEOUT_VISAO_FRIO = 300 | |
| TIMEOUT_VISAO_QUENTE = 120 | |
| # Contexto fixo e pequeno: cabe o print + a resposta, gasta pouca VRAM (o | |
| # Cooper fica na placa junto do modelo da conversa) e não recarrega. | |
| CONTEXTO_VISAO = 4096 | |
| TOKENS_VISAO_PONTO = 64 | |
| TOKENS_VISAO_DESCRICAO = 400 | |
| # Se o Cooper estourou o prazo, ele descansa este tempo (s): sem isso cada | |
| # passo da tarefa esperaria o prazo inteiro de novo. | |
| PAUSA_VISAO_LENTO = 240 | |
| _visao_estado = {"quente": False, "pausado_ate": 0.0, "erro": ""} | |
| def visao_instalada(): | |
| """A tag do Cooper no Ollama (ou None se não está instalado ou está desligado).""" | |
| if not visao_lib.ligado(): | |
| return None | |
| try: | |
| return visao_lib.achar_instalado(list(catalogar_modelos().keys())) | |
| except Exception: | |
| return None | |
| def visao_disponivel(): | |
| """A tag do Cooper pronta pra uso agora: instalado, ligado e fora da | |
| pausa que ele leva depois de estourar o prazo.""" | |
| if time.time() < _visao_estado["pausado_ate"]: | |
| return None | |
| return visao_instalada() | |
| def motivo_da_falha_da_visao(): | |
| return _visao_estado["erro"] or "no answer" | |
| def _opcoes_visao(max_tokens, tag=None): | |
| if visao_lib.e_gargantua_2(tag): | |
| # O Gargantua 2 é o mesmo modelo da conversa: o MESMO num_ctx (o _chat_unico já usa contexto_ia(); outro valor faria o | |
| # Ollama recarregar o modelo a cada olhada) e as opções do Modelfile (a prova de visão foi medida com elas). | |
| return {"num_predict": max_tokens} | |
| return {"num_ctx": CONTEXTO_VISAO, "num_predict": max_tokens, "repeat_penalty": 1.15} | |
| def _visao_cabe_junto(tag): | |
| """O Cooper cabe na placa AO LADO do maior modelo de conversa instalado? | |
| Se não cabe, carregar ele ao abrir só expulsa o modelo da conversa (ou é | |
| expulso por ele): o 1º pedido ficava esperando uma recarga à toa.""" | |
| try: | |
| gpu = detectar_gpu() | |
| catalogo = catalogar_modelos() | |
| meu = (catalogo.get(tag) or {}).get("vram_estimado_gb") | |
| outros = [m.get("vram_estimado_gb") for m in catalogo.values() | |
| if not visao_lib.e_cooper(m["tag"]) and m.get("categoria") != "embedding" | |
| and m.get("vram_estimado_gb") is not None] | |
| if not gpu or meu is None or not outros: | |
| return False | |
| return meu + max(outros) + 0.7 <= gpu["vram_total_gb"] | |
| except Exception: | |
| return False | |
| def aquecer_visao(): | |
| """Carrega o Cooper em segundo plano ao abrir o openTARS, pra a 1ª | |
| olhada na tela não pagar a carga do modelo.""" | |
| def _carregar(): | |
| try: | |
| tag = visao_instalada() | |
| if not tag or visao_lib.e_gargantua_2(tag) or not _visao_cabe_junto(tag): | |
| return # o Gargantua 2 já é o modelo da conversa: aquecer_modelo_da_conversa cuida dele | |
| r = _SESSION.post(OLLAMA_GENERATE_URL, timeout=TIMEOUT_VISAO_FRIO, json={ | |
| "model": tag, "prompt": "", "keep_alive": KEEP_ALIVE_VISAO, | |
| "options": {"num_ctx": CONTEXTO_VISAO}}) | |
| if r.status_code == 200: | |
| _visao_estado["quente"] = True | |
| except Exception: | |
| pass | |
| threading.Thread(target=_carregar, daemon=True).start() | |
| def _imagem_em_b64(imagem): | |
| # Tela 4K inteira é lenta no Cooper (e estoura o prazo na placa de 6 GB). A resposta vem em | |
| # milésimos da imagem, então reduzir não muda a posição. | |
| try: | |
| if imagem.width > LARGURA_MAXIMA_SCREENSHOT: | |
| imagem = imagem.resize((LARGURA_MAXIMA_SCREENSHOT, max(1, round(imagem.height * LARGURA_MAXIMA_SCREENSHOT / imagem.width)))) | |
| except Exception: | |
| pass | |
| buffer = io.BytesIO() | |
| imagem.save(buffer, format="PNG", compress_level=1) | |
| return base64.b64encode(buffer.getvalue()).decode("ascii") | |
| def perguntar_visao(tag, pergunta, imagem=None, imagem_b64=None, max_tokens=TOKENS_VISAO_PONTO): | |
| """Resposta do Cooper (texto) ou None se o Ollama não respondeu (o | |
| motivo fica em motivo_da_falha_da_visao). Falha rápida (erro, não prazo | |
| estourado) ganha uma segunda tentativa; prazo estourado põe o Cooper em | |
| pausa pra não travar os passos seguintes.""" | |
| b64 = imagem_b64 or _imagem_em_b64(imagem) | |
| prazo = TIMEOUT_VISAO_QUENTE if _visao_estado["quente"] else TIMEOUT_VISAO_FRIO | |
| for tentativa in range(2): | |
| _ultimo_erro["texto"] = "" | |
| inicio = time.time() | |
| resposta = _chat_unico(tag, pergunta, temperatura=0.0, timeout=prazo, | |
| keep_alive=KEEP_ALIVE if visao_lib.e_gargantua_2(tag) else KEEP_ALIVE_VISAO, imagens=[b64], | |
| opcoes=_opcoes_visao(max_tokens, tag)) | |
| if resposta: | |
| _visao_estado["quente"] = True | |
| _visao_estado["erro"] = "" | |
| return resposta | |
| gasto = time.time() - inicio | |
| _visao_estado["erro"] = _ultimo_erro["texto"] or "empty answer" | |
| if gasto >= prazo * 0.9 or "timed out" in _visao_estado["erro"].lower(): | |
| _visao_estado["quente"] = False | |
| _visao_estado["pausado_ate"] = time.time() + PAUSA_VISAO_LENTO | |
| break # estourou o prazo: não adianta repetir | |
| if gasto > 8: | |
| break | |
| return None | |
| def localizar_com_visao(nome, imagem, tag=None): | |
| """O centro do elemento 'nome' na imagem, em pixels DELA: ((x, y), tag), | |
| ("ausente", tag) se o Cooper diz que não está na tela, ou (None, None).""" | |
| tag = tag or visao_disponivel() | |
| if not tag: | |
| return None, None | |
| try: | |
| resposta = perguntar_visao(tag, visao_lib.pergunta_localizar(nome), imagem=imagem) | |
| except Exception as e: | |
| _visao_estado["erro"] = str(e)[:200] | |
| return None, None | |
| lido = visao_lib.ler_ponto(resposta) | |
| if lido is None: | |
| if resposta: | |
| _visao_estado["erro"] = "unreadable answer" | |
| return None, None | |
| if lido[0] == "ausente": | |
| return "ausente", tag | |
| largura, altura = imagem.size | |
| return visao_lib.para_pixels(lido[1], lido[2], largura, altura), tag | |
| def mirar_com_visao(alvo, imagem, perto_de=None): | |
| """(x, y, como) em pixels da imagem; "ausente" se o Cooper olhou e o | |
| alvo não está na tela; ou None (sem Cooper, falhou ou ficou longe do | |
| ponto que a IA tinha indicado).""" | |
| ponto, tag = localizar_com_visao(alvo, imagem) | |
| if ponto == "ausente": | |
| return "ausente" | |
| if not isinstance(ponto, tuple): | |
| return None | |
| if perto_de is not None: | |
| distancia = ((ponto[0] - perto_de[0]) ** 2 + (ponto[1] - perto_de[1]) ** 2) ** 0.5 | |
| if distancia > DISTANCIA_MAXIMA_VISAO_PX: | |
| return None | |
| return ponto[0], ponto[1], f"located '{alvo}' by looking at the screen" | |
| def descrever_com_visao(imagem_b64, pedido): | |
| """(texto com posições em pixels, tag) feito pelo Cooper, ou (None, None) | |
| se não há Cooper ou ele não respondeu.""" | |
| tag = visao_disponivel() | |
| if not tag: | |
| return None, None | |
| try: | |
| from PIL import Image | |
| with Image.open(io.BytesIO(base64.b64decode(imagem_b64))) as img: | |
| largura, altura = img.size | |
| bruto = perguntar_visao(tag, visao_lib.pergunta_descrever(pedido), imagem_b64=imagem_b64, | |
| max_tokens=TOKENS_VISAO_DESCRICAO) | |
| except Exception as e: | |
| _visao_estado["erro"] = str(e)[:200] | |
| return None, None | |
| if not bruto: | |
| return None, None | |
| if not re.search(r"\(\s*\d+(?:\.\d+)?\s*,\s*\d+(?:\.\d+)?\s*\)|\"x\"\s*:", bruto): | |
| return (f"[The {largura}x{altura} screenshot, described without positions. To click, use click_element(name) " | |
| f"or click_mouse(x, y, target=<what it is>).]\n{bruto.strip()}"), tag | |
| texto = visao_lib.descricao_em_pixels(bruto, largura, altura) | |
| return (f"[Positions are pixel coordinates of this {largura}x{altura} image, origin at the top-left.]\n{texto}"), tag | |