FluxcodeRasp / src /streamlit_app.py
profdanielvieira95's picture
update
5616e12 verified
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.")