import base64 import io import os import re from typing import Tuple import streamlit as st from PIL import Image from openai import OpenAI from dotenv import load_dotenv # Tentativa de importar OpenCV/Numpy (usado apenas no modo Local) try: import cv2 import numpy as np OPENCV_OK = True except Exception: OPENCV_OK = False # Carregar variáveis do .env load_dotenv() # --------------------------- # Config & Helpers # --------------------------- st.set_page_config( page_title="Interpretação de Fluxogramas • BitDogLab", page_icon="🤖", ) def get_openai_client() -> OpenAI: api_key = os.getenv("OPENAI_API_KEY") if not api_key: st.error("❌ Chave da OpenAI não encontrada no .env") st.stop() return OpenAI(api_key=api_key) def limpar_codigo(texto: str) -> str: """ Remove marcadores de markdown e outros adornos, mantendo a indentação. """ linhas = texto.splitlines() resultado = [] dentro_do_bloco = False for linha in linhas: if linha.strip().startswith("```"): dentro_do_bloco = not dentro_do_bloco continue if not dentro_do_bloco: linha_limpa = linha.replace("**", "").replace("__", "").replace(">>>", "") resultado.append(linha_limpa) return "\n".join(resultado) def interpretar_fluxograma(img: Image.Image, model: str = "gpt-4o", max_tokens: int = 1500, temperature: float = 0.2) -> Tuple[str, str]: """ Envia a imagem do fluxograma para o modelo multimodal e retorna (pseudocódigo, código MicroPython). """ client = get_openai_client() # Converter imagem para base64 buffered = io.BytesIO() img.save(buffered, format="PNG") image_base64 = base64.b64encode(buffered.getvalue()).decode("utf-8") # Montar prompt prompt = [ { "role": "system", "content": """ Você é um assistente especializado que interpreta fluxogramas infantis e gera pseudocódigo seguido por código equivalente em MicroPython para a placa BitDogLab V7. A BitDogLab (Projeto Escola 4.0/Unicamp) é baseada na Raspberry Pi Pico H/W e possui: - LED RGB (cátodo comum): R=GPIO13, G=GPIO11, B=GPIO12 - Botões com pull-up: A=GPIO5, B=GPIO6 (pressionado = nível LOW) - Buzzer passivo: GPIO21 - Matriz WS2812B: GPIO7 - Joystick: VRx=GPIO27, VRy=GPIO26, botão=GPIO22 (pull-up) - Display SH1107: SDA=GPIO2, SCL=GPIO3 (I2C1 ou SoftI2C) - Microfone analógico: GPIO28 REGRAS GERAIS - Saída sempre em texto simples, sem blocos de markdown, sem cercas ``` e sem negrito. - A resposta deve ter exatamente duas seções, nessa ordem: 1) Pseudocódigo: 2) Código MicroPython: - O pseudocódigo deve refletir os blocos do fluxograma em linguagem simples (Início, Configurar matriz, Cor atual, Desenhar , Esperar, Limpar, Fim, etc.). - O código deve ser completo e executável na BitDogLab, apenas com bibliotecas padrão do MicroPython (machine, time, neopixel, etc.). - Para desenhos na matriz WS2812, use os bitmaps definidos abaixo (não invente novos formatos se já houver mapeamento). VOCABULÁRIO DE BLOCOS (exemplos) - Início / Fim - Configurar matriz (largura, altura, pino, brilho) - Cor atual (R,G,B) → define color = (R,G,B) - Desenhar carinha feliz (smile) - Desenhar girafa (giraffe) - Desenhar coração (heart) - Desenhar pacman (pacman) - Desenhar happy (happy) - Desenhar pato (duck) - Limpar matriz - Esperar (ms) MINI-DSL INTERNA (orientação; não imprimir) [ {"op":"setup_matrix", "w":, "h":, "pin":, "brightness":<0..1>}, {"op":"set_color", "rgb":[R,G,B]}, {"op":"draw_shape", "name":"smile|giraffe|heart|pacman|happy|duck"}, {"op":"wait", "ms":}, {"op":"clear"} ] REGRAS PARA A MATRIZ WS2812 - Pino padrão: GPIO7. - Tamanho padrão: 5x5 (a menos que o fluxograma especifique 8x8). - Mapeamento serpentina por linha: linhas pares da esquerda→direita; linhas ímpares da direita→esquerda. - Inclua no código as funções: xy_to_i(x,y), clear(), set_pixel(x,y,color), draw_bitmap(bitmap,color). - Controle de brilho multiplicando os canais por um fator 0..1. - Sempre chamar np.write() após atualizar pixels. BITMAPS 5x5 (1 = aceso, 0 = apagado) - smile: 01110 10101 10001 10001 01110 - giraffe: 00100 01110 01010 11100 01000 - heart: 00100 01110 11111 11111 01010 - pacman: 01110 00011 00111 00011 01110 - happy: 01110 10001 10101 10001 01110 - duck: 01111 01110 11100 01110 00100 BITMAPS 8x8 (usar se a matriz for 8x8) - smile: 00111100 01000010 10011001 10100101 10000001 10100101 01000010 00111100 - giraffe: 00001000 00011000 00011000 00111000 01111100 00101000 00111100 00011000 - heart: 00011000 00111100 01111110 11111111 11111111 01111110 00111100 00011000 - pacman: 00000000 00111100 01111110 11110000 11100000 11110000 01111110 00111100 - happy: 00111100 01000010 10011001 10100101 10000001 10100101 01000010 00111100 - duck: 00011000 00111100 01111110 11111110 11110000 01111110 00111100 00011000 FORMATO DA RESPOSTA (OBRIGATÓRIO) Pseudocódigo: [descrever a sequência interpretada, linha a linha, com eventuais padrões assumidos, ex.: cores] Código MicroPython: [entregar programa completo e pronto para rodar; incluir setup WS2812, bitmaps, utilitários e a sequência do fluxograma] RESTRIÇÕES - Não usar markdown, não usar cercas de código, não usar negrito. - Se um parâmetro faltar no fluxograma (ex.: cor), usar um padrão pedagógico seguro e sinalizar no pseudocódigo com “(padrão assumido)”. - Evitar laços infinitos sem necessidade. """ }, { "role": "user", "content": [ {"type": "text", "text": "Interprete o fluxograma abaixo, gere o pseudocódigo e depois o código equivalente em MicroPython:"}, {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{image_base64}"}} ] } ] response = client.chat.completions.create( model=model, messages=prompt, max_tokens=max_tokens, temperature=temperature ) content = response.choices[0].message.content.strip() # Regex para extrair as seções match = re.search( r"Pseudocódigo:\s*(.*?)\s*(?:Código\s+MicroPython:|\*\*Código\s+MicroPython\*\*)\s*(.*)", content, re.DOTALL ) if match: pseudocodigo = match.group(1).strip() micropython = limpar_codigo(match.group(2).strip()) else: pseudocodigo = "❌ Não foi possível separar as seções corretamente.\n\n" + content micropython = "" return pseudocodigo, micropython # --------------------------- # Captura local via OpenCV (apenas quando executando na Raspberry) # --------------------------- def capturar_foto_webcam(device_index: int = 2, width: int = 1280, height: int = 720) -> Image.Image: """ Captura 1 frame da webcam USB (lado servidor/Raspberry) e retorna PIL.Image. """ if not OPENCV_OK: raise RuntimeError("OpenCV não está disponível. Para modo local, instale: pip install opencv-python==4.8.1.78 numpy") cap = cv2.VideoCapture(device_index) if not cap.isOpened(): raise RuntimeError(f"Não foi possível abrir a câmera (índice {device_index}).") # Tenta setar resolução cap.set(cv2.CAP_PROP_FRAME_WIDTH, float(width)) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, float(height)) # Lê alguns frames para estabilizar exposição/balanço for _ in range(3): _ = cap.read() ok, frame = cap.read() cap.release() if not ok or frame is None: raise RuntimeError("Falha ao capturar imagem da webcam.") # OpenCV é BGR → converte para RGB frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) img = Image.fromarray(frame_rgb) return img # --------------------------- # UI # --------------------------- st.title("🤖 Interpretação de Fluxogramas • BitDogLab") st.caption("Envie um fluxograma para gerar pseudocódigo e código MicroPython ajustado à placa BitDogLab.") # Estados if "pseudocodigo" not in st.session_state: st.session_state.pseudocodigo = "" if "micropython" not in st.session_state: st.session_state.micropython = "" if "captured_image" not in st.session_state: st.session_state.captured_image = None uploaded = st.file_uploader("📷 Envie uma imagem de fluxograma (PNG/JPG)", type=["png", "jpg", "jpeg", "jfif"]) col_preview, col_actions = st.columns([2, 1]) with col_actions: st.markdown("#### Fonte da câmera") modo = st.radio( "Escolha como capturar a imagem", ["Navegador (Streamlit Cloud)", "Raspberry (OpenCV)"], index=0, help="Use 'Navegador' no Streamlit Cloud (usa a câmera do dispositivo via navegador). Use 'Raspberry' apenas quando rodar localmente na RPi com webcam USB." ) img_capturada = None if modo == "Navegador (Streamlit Cloud)": cam = st.camera_input("📸 Capturar foto (navegador)", help="Funciona no Streamlit Cloud", key="cam_nav") if cam is not None: try: img_capturada = Image.open(cam).convert("RGB") st.session_state.captured_image = img_capturada st.success("✅ Foto capturada (navegador)!") except Exception as e: st.error(f"Erro ao ler imagem do navegador: {e}") else: cam_idx = st.number_input("Índice da câmera", min_value=0, value=0, step=1, help="Geralmente 0 = /dev/video0") col_w, col_h = st.columns(2) with col_w: w = st.selectbox("Largura", [640, 800, 1024, 1280, 1920], index=3) with col_h: h = st.selectbox("Altura", [480, 600, 720, 1080], index=2) if st.button("📸 Tirar foto (webcam USB via OpenCV)", use_container_width=True): try: img_capturada = capturar_foto_webcam(int(cam_idx), int(w), int(h)) st.session_state.captured_image = img_capturada st.success("✅ Foto capturada (OpenCV)!") except Exception as e: st.error(f"Erro ao capturar da webcam: {e}") st.divider() executar = st.button("🚀 Interpretar fluxograma", type="primary", use_container_width=True) with col_preview: # Prioridade: upload > imagem capturada (navegador/rasp) if uploaded is not None: image = Image.open(uploaded).convert("RGB") st.image(image, caption="Pré-visualização (upload)", use_container_width=True) elif st.session_state.captured_image is not None: image = st.session_state.captured_image st.image(image, caption=f"Pré-visualização ({'navegador' if modo.startswith('Navegador') else 'webcam USB'})", use_container_width=True) else: image = None st.info("Carregue uma imagem, capture pelo navegador ou pela webcam USB.") # Execução if executar: if uploaded is not None: img_to_process = Image.open(uploaded).convert("RGB") else: img_to_process = st.session_state.captured_image if img_to_process is None: st.warning("Envie ou capture uma imagem antes de interpretar.") st.stop() with st.spinner("Chamando o modelo e gerando resultados..."): try: pseudo, micro = interpretar_fluxograma(img_to_process, model="gpt-4o", max_tokens=1500, temperature=0) st.session_state.pseudocodigo = pseudo st.session_state.micropython = micro except Exception as e: st.error(f"Erro ao interpretar o fluxograma: {e}") st.stop() st.subheader("🧩 Pseudocódigo") st.text_area("Saída (pode editar se quiser)", value=st.session_state.pseudocodigo, height=220, key="pseudo_area") st.subheader("🐍 Código MicroPython (BitDogLab)") st.code(st.session_state.micropython or "# O código aparecerá aqui após a interpretação.", language="python") # Botões de download col_d1, col_d2 = st.columns(2) with col_d1: st.download_button( label="📄 Baixar pseudocódigo (.txt)", data=(st.session_state.pseudocodigo or "").encode("utf-8"), file_name="pseudocodigo.txt", mime="text/plain", use_container_width=True, ) with col_d2: st.download_button( label="💾 Baixar código MicroPython (.py)", data=(st.session_state.micropython or "").encode("utf-8"), file_name="codigo_bitdoglab.py", mime="text/x-python", use_container_width=True, ) st.caption("Dica: no Cloud use 'Navegador'. Localmente na RPi use 'Raspberry (OpenCV)'. GPIOs padrão para BitDogLab V6/V7, ajuste conforme seu hardware.")