Skip to content

Commit 1463b64

Browse files
authored
Merge pull request #2454 from jepler/openai-get-superpower
Openai get superpower
2 parents 098fe94 + 037976e commit 1463b64

File tree

2 files changed

+225
-0
lines changed

2 files changed

+225
-0
lines changed
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
# SPDX-FileCopyrightText: 2023 Jeff Epler for Adafruit Industries
2+
# SPDX-License-Identifier: MIT
3+
import json
4+
import os
5+
import ssl
6+
7+
import board
8+
import displayio
9+
import digitalio
10+
import keypad
11+
import socketpool
12+
from wifi import radio
13+
14+
import adafruit_requests
15+
import adafruit_displayio_ssd1306
16+
from adafruit_bitmap_font.bitmap_font import load_font
17+
from adafruit_display_text import wrap_text_to_pixels
18+
from adafruit_display_text.bitmap_label import Label
19+
from adafruit_ticks import ticks_add, ticks_less, ticks_ms
20+
21+
22+
# Choose your own prompt and wait messages, either by changing it below inside
23+
# the """triple quoted""" string, or by putting it in your settings.toml file,
24+
# like so:
25+
#
26+
# MY_PROMPT="Give me an idea for a gluten free, keto dinner. Write one sentence"
27+
# PLEASE_WAIT="Cooking something up just for you"
28+
#
29+
# Experiementation is best to figure out what works. Usually you'll want to ask
30+
# for just one sentence or paragraph, since the 128x32 pixel screen can't hold
31+
# much text!
32+
33+
# Here are some that the author found worked reasonably well:
34+
35+
# Give me an idea for a plant-based dinner. Write one sentence
36+
#
37+
# Give jepler (they/them) a cliched and flowery description as a comic book
38+
# supervillain. write one sentence.
39+
#
40+
# Invent and describe an alien species. write one sentence
41+
#
42+
# Invent a zany 'as seen on' product that can't possibly work. One sentence
43+
#
44+
# Tell a 1-sentence story about a kitten and a funny mishap
45+
#
46+
# Make up a 1-sentence fortune for me
47+
#
48+
# In first person, write a 1-sentence story about an AI avoiding boredom in a creative way.
49+
#
50+
# Pick an everyday object (don't say what it is) and describe it using only the
51+
# ten hundred most common words.
52+
#
53+
# Invent an alien animal or plant, name it, and vividly describe it in 1
54+
# sentence
55+
#
56+
# Invent and vividly describe an alien species. write one paragraph
57+
58+
prompt=os.getenv("MY_PROMPT", """
59+
Write 1 setence starting "you can" about an unconventional but useful superpower
60+
""").strip()
61+
please_wait=os.getenv("PLEASE_WAIT", """
62+
Finding superpower
63+
""").strip()
64+
65+
openai_api_key = os.getenv("OPENAI_API_KEY")
66+
67+
nice_font = load_font("helvR08.pcf")
68+
line_spacing = 9 # in pixels
69+
70+
# i2c display setup
71+
displayio.release_displays()
72+
oled_reset = board.GP9
73+
74+
# STEMMA I2C on picowbell
75+
i2c = board.STEMMA_I2C()
76+
display_bus = displayio.I2CDisplay(i2c, device_address=0x3D, reset=oled_reset)
77+
78+
WIDTH = 128
79+
HEIGHT = 64
80+
81+
display = adafruit_displayio_ssd1306.SSD1306(
82+
display_bus, width=WIDTH, height=HEIGHT
83+
)
84+
if openai_api_key is None:
85+
input("Place your\nOPENAI_API_KEY\nin settings.toml")
86+
display.auto_refresh = False
87+
88+
class WrappedTextDisplay(displayio.Group):
89+
def __init__(self):
90+
super().__init__()
91+
self.offset = 0
92+
self.max_lines = display.height // line_spacing
93+
for i in range(self.max_lines):
94+
self.make_label("", i * line_spacing)
95+
self.lines = [""]
96+
self.text = ""
97+
98+
def make_label(self, text, y):
99+
result = Label(
100+
font=nice_font,
101+
color=0xFFFFFF,
102+
background_color=0,
103+
line_spacing=line_spacing,
104+
anchor_point=(0, 0),
105+
anchored_position=(0, y),
106+
text=text)
107+
self.append(result)
108+
109+
def add_text(self, new_text):
110+
print(end=new_text)
111+
if self.lines:
112+
text = self.lines[-1] + new_text
113+
else:
114+
text = new_text
115+
self.lines[-1:] = wrap_text_to_pixels(text, display.width, nice_font)
116+
self.scroll_to_end()
117+
118+
def set_text(self, text):
119+
print("\033[H\033[2J", end=text)
120+
self.text = text
121+
self.lines = wrap_text_to_pixels(text, display.width, nice_font)
122+
self.offset = 0
123+
124+
def show(self, text):
125+
self.set_text(text)
126+
self.refresh()
127+
128+
def add_show(self, new_text):
129+
self.add_text(new_text)
130+
self.refresh()
131+
132+
def scroll_to_end(self):
133+
self.offset = self.max_offset()
134+
135+
def scroll_next_line(self):
136+
max_offset = self.max_offset()
137+
self.offset = (self.offset + 1) % (max_offset + 1)
138+
139+
def max_offset(self):
140+
return max(0, len(self.lines) - self.max_lines)
141+
142+
def on_last_line(self):
143+
return self.offset == self.max_offset()
144+
145+
def refresh(self):
146+
lines = self.lines
147+
# update labels from wrapped text, accounting for scroll offset
148+
for i in range(len(self)):
149+
offset_i = i + self.offset
150+
if offset_i >= len(lines):
151+
text = ""
152+
else:
153+
text = lines[offset_i]
154+
if text != self[i].text:
155+
self[i].text = text
156+
157+
# Actually update the display all at once
158+
display.refresh()
159+
160+
display.root_group = wrapped_text = WrappedTextDisplay()
161+
162+
def wait_button_scroll_text():
163+
led.switch_to_output(True)
164+
deadline = ticks_add(ticks_ms(),
165+
5000 if wrapped_text.on_last_line() else 1000)
166+
while True:
167+
if (event := keys.events.get()) and event.pressed:
168+
break
169+
if wrapped_text.max_offset() > 0 and ticks_less(deadline, ticks_ms()):
170+
wrapped_text.scroll_next_line()
171+
wrapped_text.refresh()
172+
deadline = ticks_add(deadline,
173+
5000 if wrapped_text.on_last_line() else 1000)
174+
led.value = False
175+
176+
if radio.ipv4_address is None:
177+
wrapped_text.show(f"connecting to {os.getenv('WIFI_SSID')}")
178+
radio.connect(os.getenv('WIFI_SSID'), os.getenv('WIFI_PASSWORD'))
179+
requests = adafruit_requests.Session(socketpool.SocketPool(radio), ssl.create_default_context())
180+
181+
def iter_lines(resp):
182+
partial_line = []
183+
for c in resp.iter_content():
184+
if c == b'\n':
185+
yield (b"".join(partial_line)).decode('utf-8')
186+
del partial_line[:]
187+
else:
188+
partial_line.append(c)
189+
if partial_line:
190+
yield (b"".join(partial_line)).decode('utf-8')
191+
192+
full_prompt = [
193+
{"role": "user", "content": prompt},
194+
]
195+
196+
keys = keypad.Keys((board.GP14,), value_when_pressed=False)
197+
led = digitalio.DigitalInOut(board.LED)
198+
199+
while True:
200+
wrapped_text.show(please_wait)
201+
202+
with requests.post("https://api.openai.com/v1/chat/completions",
203+
json={"model": "gpt-3.5-turbo", "messages": full_prompt, "stream": True},
204+
headers={
205+
"Authorization": f"Bearer {openai_api_key}",
206+
},
207+
) as response:
208+
209+
wrapped_text.set_text("")
210+
if response.status_code != 200:
211+
wrapped_text.show(f"Uh oh! {response.status_code}: {response.reason}")
212+
else:
213+
wrapped_text.show("")
214+
for line in iter_lines(response):
215+
if line.startswith("data: [DONE]"):
216+
break
217+
if line.startswith("data:"):
218+
content = json.loads(line[5:])
219+
try:
220+
token = content['choices'][0]['delta'].get('content', '')
221+
except (KeyError, IndexError) as e:
222+
token = None
223+
if token:
224+
wrapped_text.add_show(token)
225+
wait_button_scroll_text()
Binary file not shown.

0 commit comments

Comments
 (0)