Spaces:
Sleeping
Sleeping
| 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 <forma>, 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":<int>, "h":<int>, "pin":<int>, "brightness":<0..1>}, | |
| {"op":"set_color", "rgb":[R,G,B]}, | |
| {"op":"draw_shape", "name":"smile|giraffe|heart|pacman|happy|duck"}, | |
| {"op":"wait", "ms":<int>}, | |
| {"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.") |