profdanielvieira95 commited on
Commit
5616e12
·
verified ·
1 Parent(s): 0c2b53a
Files changed (1) hide show
  1. src/streamlit_app.py +413 -34
src/streamlit_app.py CHANGED
@@ -1,40 +1,419 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
 
 
 
4
  import streamlit as st
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
 
 
 
 
12
 
13
- In the meantime, below is an example of what you can do with just a few lines of code:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
1
+ import base64
2
+ import io
3
+ import os
4
+ import re
5
+ from typing import Tuple
6
+
7
  import streamlit as st
8
+ from PIL import Image
9
+ from openai import OpenAI
10
+ from dotenv import load_dotenv
11
 
12
+ # Tentativa de importar OpenCV/Numpy (usado apenas no modo Local)
13
+ try:
14
+ import cv2
15
+ import numpy as np
16
+ OPENCV_OK = True
17
+ except Exception:
18
+ OPENCV_OK = False
19
+
20
+ # Carregar variáveis do .env
21
+ load_dotenv()
22
+
23
+ # ---------------------------
24
+ # Config & Helpers
25
+ # ---------------------------
26
+ st.set_page_config(
27
+ page_title="Interpretação de Fluxogramas • BitDogLab",
28
+ page_icon="🤖",
29
+ )
30
+
31
+ def get_openai_client() -> OpenAI:
32
+ api_key = os.getenv("OPENAI_API_KEY")
33
+ if not api_key:
34
+ st.error("❌ Chave da OpenAI não encontrada no .env")
35
+ st.stop()
36
+ return OpenAI(api_key=api_key)
37
+
38
+ def limpar_codigo(texto: str) -> str:
39
+ """
40
+ Remove marcadores de markdown e outros adornos, mantendo a indentação.
41
+ """
42
+ linhas = texto.splitlines()
43
+ resultado = []
44
+ dentro_do_bloco = False
45
+
46
+ for linha in linhas:
47
+ if linha.strip().startswith("```"):
48
+ dentro_do_bloco = not dentro_do_bloco
49
+ continue
50
+ if not dentro_do_bloco:
51
+ linha_limpa = linha.replace("**", "").replace("__", "").replace(">>>", "")
52
+ resultado.append(linha_limpa)
53
+ return "\n".join(resultado)
54
+
55
+ def interpretar_fluxograma(img: Image.Image, model: str = "gpt-4o", max_tokens: int = 1500, temperature: float = 0.2) -> Tuple[str, str]:
56
+ """
57
+ Envia a imagem do fluxograma para o modelo multimodal e retorna
58
+ (pseudocódigo, código MicroPython).
59
+ """
60
+ client = get_openai_client()
61
+
62
+ # Converter imagem para base64
63
+ buffered = io.BytesIO()
64
+ img.save(buffered, format="PNG")
65
+ image_base64 = base64.b64encode(buffered.getvalue()).decode("utf-8")
66
+
67
+ # Montar prompt
68
+ prompt = [
69
+ {
70
+ "role": "system",
71
+ "content": """
72
+ Você é um assistente especializado que interpreta fluxogramas infantis e gera pseudocódigo seguido por código equivalente em MicroPython para a placa BitDogLab V7.
73
+ A BitDogLab (Projeto Escola 4.0/Unicamp) é baseada na Raspberry Pi Pico H/W e possui:
74
+ - LED RGB (cátodo comum): R=GPIO13, G=GPIO11, B=GPIO12
75
+ - Botões com pull-up: A=GPIO5, B=GPIO6 (pressionado = nível LOW)
76
+ - Buzzer passivo: GPIO21
77
+ - Matriz WS2812B: GPIO7
78
+ - Joystick: VRx=GPIO27, VRy=GPIO26, botão=GPIO22 (pull-up)
79
+ - Display SH1107: SDA=GPIO2, SCL=GPIO3 (I2C1 ou SoftI2C)
80
+ - Microfone analógico: GPIO28
81
+
82
+ REGRAS GERAIS
83
+ - Saída sempre em texto simples, sem blocos de markdown, sem cercas ``` e sem negrito.
84
+ - A resposta deve ter exatamente duas seções, nessa ordem:
85
+ 1) Pseudocódigo:
86
+ 2) Código MicroPython:
87
+ - O pseudocódigo deve refletir os blocos do fluxograma em linguagem simples (Início, Configurar matriz, Cor atual, Desenhar <forma>, Esperar, Limpar, Fim, etc.).
88
+ - O código deve ser completo e executável na BitDogLab, apenas com bibliotecas padrão do MicroPython (machine, time, neopixel, etc.).
89
+ - Para desenhos na matriz WS2812, use os bitmaps definidos abaixo (não invente novos formatos se já houver mapeamento).
90
+
91
+ VOCABULÁRIO DE BLOCOS (exemplos)
92
+ - Início / Fim
93
+ - Configurar matriz (largura, altura, pino, brilho)
94
+ - Cor atual (R,G,B) → define color = (R,G,B)
95
+ - Desenhar carinha feliz (smile)
96
+ - Desenhar girafa (giraffe)
97
+ - Desenhar coração (heart)
98
+ - Desenhar pacman (pacman)
99
+ - Desenhar happy (happy)
100
+ - Desenhar pato (duck)
101
+ - Limpar matriz
102
+ - Esperar (ms)
103
+
104
+ MINI-DSL INTERNA (orientação; não imprimir)
105
+ [
106
+ {"op":"setup_matrix", "w":<int>, "h":<int>, "pin":<int>, "brightness":<0..1>},
107
+ {"op":"set_color", "rgb":[R,G,B]},
108
+ {"op":"draw_shape", "name":"smile|giraffe|heart|pacman|happy|duck"},
109
+ {"op":"wait", "ms":<int>},
110
+ {"op":"clear"}
111
+ ]
112
+
113
+ REGRAS PARA A MATRIZ WS2812
114
+ - Pino padrão: GPIO7.
115
+ - Tamanho padrão: 5x5 (a menos que o fluxograma especifique 8x8).
116
+ - Mapeamento serpentina por linha: linhas pares da esquerda→direita; linhas ímpares da direita→esquerda.
117
+ - Inclua no código as funções: xy_to_i(x,y), clear(), set_pixel(x,y,color), draw_bitmap(bitmap,color).
118
+ - Controle de brilho multiplicando os canais por um fator 0..1.
119
+ - Sempre chamar np.write() após atualizar pixels.
120
 
121
+ BITMAPS 5x5 (1 = aceso, 0 = apagado)
122
+ - smile:
123
+ 01110
124
+ 10101
125
+ 10001
126
+ 10001
127
+ 01110
128
 
129
+ - giraffe:
130
+ 00100
131
+ 01110
132
+ 01010
133
+ 11100
134
+ 01000
135
+
136
+ - heart:
137
+ 00100
138
+ 01110
139
+ 11111
140
+ 11111
141
+ 01010
142
+
143
+ - pacman:
144
+ 01110
145
+ 00011
146
+ 00111
147
+ 00011
148
+ 01110
149
+
150
+ - happy:
151
+ 01110
152
+ 10001
153
+ 10101
154
+ 10001
155
+ 01110
156
+
157
+ - duck:
158
+ 01111
159
+ 01110
160
+ 11100
161
+ 01110
162
+ 00100
163
+
164
+ BITMAPS 8x8 (usar se a matriz for 8x8)
165
+ - smile:
166
+ 00111100
167
+ 01000010
168
+ 10011001
169
+ 10100101
170
+ 10000001
171
+ 10100101
172
+ 01000010
173
+ 00111100
174
+
175
+ - giraffe:
176
+ 00001000
177
+ 00011000
178
+ 00011000
179
+ 00111000
180
+ 01111100
181
+ 00101000
182
+ 00111100
183
+ 00011000
184
+
185
+ - heart:
186
+ 00011000
187
+ 00111100
188
+ 01111110
189
+ 11111111
190
+ 11111111
191
+ 01111110
192
+ 00111100
193
+ 00011000
194
+
195
+ - pacman:
196
+ 00000000
197
+ 00111100
198
+ 01111110
199
+ 11110000
200
+ 11100000
201
+ 11110000
202
+ 01111110
203
+ 00111100
204
+
205
+ - happy:
206
+ 00111100
207
+ 01000010
208
+ 10011001
209
+ 10100101
210
+ 10000001
211
+ 10100101
212
+ 01000010
213
+ 00111100
214
+
215
+ - duck:
216
+ 00011000
217
+ 00111100
218
+ 01111110
219
+ 11111110
220
+ 11110000
221
+ 01111110
222
+ 00111100
223
+ 00011000
224
+
225
+ FORMATO DA RESPOSTA (OBRIGATÓRIO)
226
+ Pseudocódigo:
227
+ [descrever a sequência interpretada, linha a linha, com eventuais padrões assumidos, ex.: cores]
228
+
229
+ Código MicroPython:
230
+ [entregar programa completo e pronto para rodar; incluir setup WS2812, bitmaps, utilitários e a sequência do fluxograma]
231
+
232
+ RESTRIÇÕES
233
+ - Não usar markdown, não usar cercas de código, não usar negrito.
234
+ - 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)”.
235
+ - Evitar laços infinitos sem necessidade.
236
  """
237
+ },
238
+ {
239
+ "role": "user",
240
+ "content": [
241
+ {"type": "text", "text": "Interprete o fluxograma abaixo, gere o pseudocódigo e depois o código equivalente em MicroPython:"},
242
+ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{image_base64}"}}
243
+ ]
244
+ }
245
+ ]
246
+
247
+
248
+
249
+ response = client.chat.completions.create(
250
+ model=model,
251
+ messages=prompt,
252
+ max_tokens=max_tokens,
253
+ temperature=temperature
254
+ )
255
+
256
+ content = response.choices[0].message.content.strip()
257
+
258
+ # Regex para extrair as seções
259
+ match = re.search(
260
+ r"Pseudocódigo:\s*(.*?)\s*(?:Código\s+MicroPython:|\*\*Código\s+MicroPython\*\*)\s*(.*)",
261
+ content,
262
+ re.DOTALL
263
+ )
264
+
265
+ if match:
266
+ pseudocodigo = match.group(1).strip()
267
+ micropython = limpar_codigo(match.group(2).strip())
268
+ else:
269
+ pseudocodigo = "❌ Não foi possível separar as seções corretamente.\n\n" + content
270
+ micropython = ""
271
+
272
+ return pseudocodigo, micropython
273
+
274
+ # ---------------------------
275
+ # Captura local via OpenCV (apenas quando executando na Raspberry)
276
+ # ---------------------------
277
+ def capturar_foto_webcam(device_index: int = 2, width: int = 1280, height: int = 720) -> Image.Image:
278
+ """
279
+ Captura 1 frame da webcam USB (lado servidor/Raspberry) e retorna PIL.Image.
280
+ """
281
+ if not OPENCV_OK:
282
+ raise RuntimeError("OpenCV não está disponível. Para modo local, instale: pip install opencv-python==4.8.1.78 numpy")
283
+
284
+ cap = cv2.VideoCapture(device_index)
285
+ if not cap.isOpened():
286
+ raise RuntimeError(f"Não foi possível abrir a câmera (índice {device_index}).")
287
+
288
+ # Tenta setar resolução
289
+ cap.set(cv2.CAP_PROP_FRAME_WIDTH, float(width))
290
+ cap.set(cv2.CAP_PROP_FRAME_HEIGHT, float(height))
291
+
292
+ # Lê alguns frames para estabilizar exposição/balanço
293
+ for _ in range(3):
294
+ _ = cap.read()
295
+
296
+ ok, frame = cap.read()
297
+ cap.release()
298
+ if not ok or frame is None:
299
+ raise RuntimeError("Falha ao capturar imagem da webcam.")
300
+
301
+ # OpenCV é BGR → converte para RGB
302
+ frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
303
+ img = Image.fromarray(frame_rgb)
304
+ return img
305
+
306
+ # ---------------------------
307
+ # UI
308
+ # ---------------------------
309
+ st.title("🤖 Interpretação de Fluxogramas • BitDogLab")
310
+ st.caption("Envie um fluxograma para gerar pseudocódigo e código MicroPython ajustado à placa BitDogLab.")
311
+
312
+ # Estados
313
+ if "pseudocodigo" not in st.session_state:
314
+ st.session_state.pseudocodigo = ""
315
+ if "micropython" not in st.session_state:
316
+ st.session_state.micropython = ""
317
+ if "captured_image" not in st.session_state:
318
+ st.session_state.captured_image = None
319
+
320
+ uploaded = st.file_uploader("📷 Envie uma imagem de fluxograma (PNG/JPG)", type=["png", "jpg", "jpeg", "jfif"])
321
+
322
+ col_preview, col_actions = st.columns([2, 1])
323
+
324
+ with col_actions:
325
+ st.markdown("#### Fonte da câmera")
326
+ modo = st.radio(
327
+ "Escolha como capturar a imagem",
328
+ ["Navegador (Streamlit Cloud)", "Raspberry (OpenCV)"],
329
+ index=0,
330
+ 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."
331
+ )
332
+
333
+ img_capturada = None
334
+
335
+ if modo == "Navegador (Streamlit Cloud)":
336
+ cam = st.camera_input("📸 Capturar foto (navegador)", help="Funciona no Streamlit Cloud", key="cam_nav")
337
+ if cam is not None:
338
+ try:
339
+ img_capturada = Image.open(cam).convert("RGB")
340
+ st.session_state.captured_image = img_capturada
341
+ st.success("✅ Foto capturada (navegador)!")
342
+ except Exception as e:
343
+ st.error(f"Erro ao ler imagem do navegador: {e}")
344
+ else:
345
+ cam_idx = st.number_input("Índice da câmera", min_value=0, value=0, step=1, help="Geralmente 0 = /dev/video0")
346
+ col_w, col_h = st.columns(2)
347
+ with col_w:
348
+ w = st.selectbox("Largura", [640, 800, 1024, 1280, 1920], index=3)
349
+ with col_h:
350
+ h = st.selectbox("Altura", [480, 600, 720, 1080], index=2)
351
+ if st.button("📸 Tirar foto (webcam USB via OpenCV)", use_container_width=True):
352
+ try:
353
+ img_capturada = capturar_foto_webcam(int(cam_idx), int(w), int(h))
354
+ st.session_state.captured_image = img_capturada
355
+ st.success("✅ Foto capturada (OpenCV)!")
356
+ except Exception as e:
357
+ st.error(f"Erro ao capturar da webcam: {e}")
358
+
359
+ st.divider()
360
+ executar = st.button("🚀 Interpretar fluxograma", type="primary", use_container_width=True)
361
+
362
+ with col_preview:
363
+ # Prioridade: upload > imagem capturada (navegador/rasp)
364
+ if uploaded is not None:
365
+ image = Image.open(uploaded).convert("RGB")
366
+ st.image(image, caption="Pré-visualização (upload)", use_container_width=True)
367
+ elif st.session_state.captured_image is not None:
368
+ image = st.session_state.captured_image
369
+ st.image(image, caption=f"Pré-visualização ({'navegador' if modo.startswith('Navegador') else 'webcam USB'})", use_container_width=True)
370
+ else:
371
+ image = None
372
+ st.info("Carregue uma imagem, capture pelo navegador ou pela webcam USB.")
373
+
374
+ # Execução
375
+ if executar:
376
+ if uploaded is not None:
377
+ img_to_process = Image.open(uploaded).convert("RGB")
378
+ else:
379
+ img_to_process = st.session_state.captured_image
380
+
381
+ if img_to_process is None:
382
+ st.warning("Envie ou capture uma imagem antes de interpretar.")
383
+ st.stop()
384
+
385
+ with st.spinner("Chamando o modelo e gerando resultados..."):
386
+ try:
387
+ pseudo, micro = interpretar_fluxograma(img_to_process, model="gpt-4o", max_tokens=1500, temperature=0)
388
+ st.session_state.pseudocodigo = pseudo
389
+ st.session_state.micropython = micro
390
+ except Exception as e:
391
+ st.error(f"Erro ao interpretar o fluxograma: {e}")
392
+ st.stop()
393
+
394
+ st.subheader("🧩 Pseudocódigo")
395
+ st.text_area("Saída (pode editar se quiser)", value=st.session_state.pseudocodigo, height=220, key="pseudo_area")
396
+
397
+ st.subheader("🐍 Código MicroPython (BitDogLab)")
398
+ st.code(st.session_state.micropython or "# O código aparecerá aqui após a interpretação.", language="python")
399
+
400
+ # Botões de download
401
+ col_d1, col_d2 = st.columns(2)
402
+ with col_d1:
403
+ st.download_button(
404
+ label="📄 Baixar pseudocódigo (.txt)",
405
+ data=(st.session_state.pseudocodigo or "").encode("utf-8"),
406
+ file_name="pseudocodigo.txt",
407
+ mime="text/plain",
408
+ use_container_width=True,
409
+ )
410
+ with col_d2:
411
+ st.download_button(
412
+ label="💾 Baixar código MicroPython (.py)",
413
+ data=(st.session_state.micropython or "").encode("utf-8"),
414
+ file_name="codigo_bitdoglab.py",
415
+ mime="text/x-python",
416
+ use_container_width=True,
417
+ )
418
 
419
+ st.caption("Dica: no Cloud use 'Navegador'. Localmente na RPi use 'Raspberry (OpenCV)'. GPIOs padrão para BitDogLab V6/V7, ajuste conforme seu hardware.")