Compare commits
38 Commits
4c176b7469
...
modular-re
| Author | SHA1 | Date | |
|---|---|---|---|
| dee45e5eae | |||
| eaaa8e5f57 | |||
| a19aec1848 | |||
| af6072627e | |||
| 339a58e1bf | |||
| 3fc74d82ad | |||
| 17e3ad347a | |||
| c58a356c02 | |||
| d8886e80db | |||
| fdfefffda6 | |||
| 3b1e0c481e | |||
| 858d941b3f | |||
| 8b65026401 | |||
| 52c417b176 | |||
| 6ff226543d | |||
| fe299ba2b5 | |||
| 7bc5e02a7c | |||
| 016d40938c | |||
| c0fe15cfb8 | |||
| 19fd57483d | |||
| 2dbbbd290c | |||
| 562f1c3fb5 | |||
| 8234232125 | |||
| 40d676761b | |||
| 9c9fa4eebf | |||
| 8af1ab4a3c | |||
| 9e387e5bb1 | |||
| 38904c50db | |||
| 5fa12e05a8 | |||
| 8d6233c888 | |||
| 1f0be77134 | |||
| d1b7aa48ed | |||
| 725b9669f8 | |||
| 96f03a3119 | |||
| a595caac18 | |||
| cf21619324 | |||
| 48a24e1bd4 | |||
| 8b854fa715 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -115,3 +115,4 @@ venv.bak/
|
|||||||
|
|
||||||
secrets.py
|
secrets.py
|
||||||
tmp.png
|
tmp.png
|
||||||
|
.aider*
|
||||||
|
|||||||
40
consumable_label.py
Normal file
40
consumable_label.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
from PIL import Image, ImageEnhance, ImageFont, ImageDraw
|
||||||
|
import os
|
||||||
|
import textwrap
|
||||||
|
import qrcode
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
location = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
|
||||||
|
def print_consumable_label(item):
|
||||||
|
im = Image.open(location + '/label.png')
|
||||||
|
width, height = im.size
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
#logging.info('Printing consumable label item: %s', item)
|
||||||
|
|
||||||
|
encodeded = urllib.parse.quote(item)
|
||||||
|
url = 'https://my.protospace.ca/out-of-stock?item=' + encodeded
|
||||||
|
|
||||||
|
qr = qrcode.make(url, version=6, box_size=9)
|
||||||
|
im.paste(qr, (840, 325))
|
||||||
|
|
||||||
|
item_size = 150
|
||||||
|
|
||||||
|
w = 9999
|
||||||
|
while w > 1200:
|
||||||
|
item_size -= 5
|
||||||
|
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', item_size)
|
||||||
|
w, h = draw.textsize(item, font=font)
|
||||||
|
|
||||||
|
x, y = (width - w) / 2, ((height - h) / 2) - 140
|
||||||
|
draw.text((x, y), item, font=font, fill='black')
|
||||||
|
|
||||||
|
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 100)
|
||||||
|
draw.text((100, 410), 'Out of stock?', font=font, fill='black')
|
||||||
|
draw.text((150, 560), 'Scan here:', font=font, fill='black')
|
||||||
|
|
||||||
|
im.save('tmp.png')
|
||||||
|
|
||||||
|
|
||||||
|
print_consumable_label('Brown paper towel')
|
||||||
92
forum_label.py
Normal file
92
forum_label.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
from PIL import Image, ImageEnhance, ImageFont, ImageDraw
|
||||||
|
import os
|
||||||
|
import textwrap
|
||||||
|
import qrcode
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
location = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
|
||||||
|
def print_forum_label(thread):
|
||||||
|
im = Image.open(location + '/label.png')
|
||||||
|
width, height = im.size
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
#logging.info('Printing forum thread: %s', thread['title'])
|
||||||
|
|
||||||
|
url = 'https://forum.protospace.ca/t/{}/'.format(thread['id'])
|
||||||
|
|
||||||
|
qr = qrcode.make(url, version=6, box_size=9)
|
||||||
|
im.paste(qr, (840, 150))
|
||||||
|
|
||||||
|
item_size = 150
|
||||||
|
|
||||||
|
w = 9999
|
||||||
|
while w > 1200:
|
||||||
|
item_size -= 5
|
||||||
|
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', item_size)
|
||||||
|
w, h = draw.textsize(url, font=font)
|
||||||
|
|
||||||
|
x, y = (width - w) / 2, ((height - h) / 2) + 300
|
||||||
|
draw.text((x, y), url, font=font, fill='black')
|
||||||
|
|
||||||
|
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 80)
|
||||||
|
draw.text((120, 150), 'Forum Thread', font=font, fill='black')
|
||||||
|
|
||||||
|
text = thread['title']
|
||||||
|
|
||||||
|
MARGIN = 50
|
||||||
|
MAX_W, MAX_H, PAD = 900 - (MARGIN*2), 450 - (MARGIN*2), 5
|
||||||
|
|
||||||
|
def fit_text(text, font_size):
|
||||||
|
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', font_size)
|
||||||
|
|
||||||
|
for cols in range(100, 1, -4):
|
||||||
|
print('trying size', font_size, 'cols', cols)
|
||||||
|
|
||||||
|
paragraph = textwrap.wrap(text, width=cols, break_long_words=False)
|
||||||
|
|
||||||
|
total_h = -PAD
|
||||||
|
total_w = 0
|
||||||
|
|
||||||
|
for line in paragraph:
|
||||||
|
w, h = draw.textsize(line, font=font)
|
||||||
|
if w > total_w:
|
||||||
|
total_w = w
|
||||||
|
total_h += h + PAD
|
||||||
|
|
||||||
|
if total_w <= MAX_W and total_h < MAX_H:
|
||||||
|
return True, paragraph, total_h
|
||||||
|
|
||||||
|
return False, [], 0
|
||||||
|
|
||||||
|
font_size_range = [1, 500]
|
||||||
|
|
||||||
|
# Thanks to Alex (UDIA) for the binary search algorithm
|
||||||
|
while abs(font_size_range[0] - font_size_range[1]) > 1:
|
||||||
|
font_size = sum(font_size_range) // 2
|
||||||
|
image_fit, check_para, check_h = fit_text(text, font_size)
|
||||||
|
if image_fit:
|
||||||
|
print('Does fit')
|
||||||
|
font_size_range = [font_size, font_size_range[1]]
|
||||||
|
good_size = font_size
|
||||||
|
paragraph = check_para
|
||||||
|
total_h = check_h
|
||||||
|
else:
|
||||||
|
print('Does not fit')
|
||||||
|
font_size_range = [font_size_range[0], font_size]
|
||||||
|
|
||||||
|
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', good_size)
|
||||||
|
offset = height - MAX_H - MARGIN
|
||||||
|
start_h = -100 + offset
|
||||||
|
|
||||||
|
current_h = start_h
|
||||||
|
for line in paragraph:
|
||||||
|
w, h = draw.textsize(line, font=font)
|
||||||
|
x, y = (MAX_W - w) / 2, current_h
|
||||||
|
draw.text((x+MARGIN, y), line, font=font, fill='black')
|
||||||
|
current_h += h + PAD
|
||||||
|
|
||||||
|
im.save('tmp.png')
|
||||||
|
|
||||||
|
|
||||||
|
print_forum_label(dict(id=10197, title='Pitch: A wild split-flap display appeared!'))
|
||||||
@@ -2,7 +2,7 @@ from PIL import Image, ImageEnhance, ImageFont, ImageDraw
|
|||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
|
text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
|
||||||
text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.'
|
text = 'Extension cables'
|
||||||
|
|
||||||
MARGIN = 50
|
MARGIN = 50
|
||||||
MAX_W, MAX_H, PAD = 1285 - (MARGIN*2), 635 - (MARGIN*2), 5
|
MAX_W, MAX_H, PAD = 1285 - (MARGIN*2), 635 - (MARGIN*2), 5
|
||||||
@@ -17,7 +17,7 @@ def fit_text(text, font_size):
|
|||||||
for cols in range(100, 1, -4):
|
for cols in range(100, 1, -4):
|
||||||
print('trying size', font_size, 'cols', cols)
|
print('trying size', font_size, 'cols', cols)
|
||||||
|
|
||||||
paragraph = textwrap.wrap(text, width=cols)
|
paragraph = textwrap.wrap(text, width=cols, break_long_words=False)
|
||||||
|
|
||||||
total_h = -PAD
|
total_h = -PAD
|
||||||
total_w = 0
|
total_w = 0
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
annotated-types==0.5.0
|
|
||||||
certifi==2022.6.15
|
certifi==2022.6.15
|
||||||
charset-normalizer==2.1.1
|
charset-normalizer==2.1.1
|
||||||
idna==3.3
|
idna==3.3
|
||||||
inflect==7.0.0
|
inflect==6.0.0
|
||||||
jaraco.itertools==6.4.1
|
jaraco.itertools==6.2.1
|
||||||
more-itertools==9.1.0
|
more-itertools==8.14.0
|
||||||
|
paho-mqtt==2.1.0
|
||||||
Pillow==9.2.0
|
Pillow==9.2.0
|
||||||
pydantic==2.0.3
|
pydantic==1.10.2
|
||||||
pydantic_core==2.3.0
|
|
||||||
pyserial==3.5
|
pyserial==3.5
|
||||||
pytz==2022.2.1
|
pytz==2022.2.1
|
||||||
|
qrcode==8.1
|
||||||
requests==2.28.1
|
requests==2.28.1
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
typing_extensions==4.7.1
|
typing-extensions==4.4.0
|
||||||
urllib3==1.26.12
|
urllib3==1.26.12
|
||||||
git+https://git.tannercollin.com/tanner/wolframalpha.git
|
git+https://git.tannercollin.com/tanner/wolframalpha.git
|
||||||
x256==0.0.3
|
x256==0.0.3
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
wa_api_key = ''
|
wa_api_key = ''
|
||||||
|
|
||||||
# Get these from your network inspector after sending a message to character.ai
|
openai_key = ''
|
||||||
character_ai_cookies = {
|
|
||||||
'_legacy_auth0.whatever.is.authenticated': 'true',
|
MQTT_WRITER_PASSWORD = ''
|
||||||
'auth0.whatever.is.authenticated': 'true',
|
|
||||||
'messages': '',
|
FORUM_SEARCH_API_KEY = ''
|
||||||
'csrftoken': '',
|
|
||||||
'sessionid': '',
|
|
||||||
}
|
|
||||||
character_ai_token = ''
|
|
||||||
|
|||||||
681
utils.py
Normal file
681
utils.py
Normal file
@@ -0,0 +1,681 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import os, logging
|
||||||
|
import warnings
|
||||||
|
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import pytz
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import textwrap
|
||||||
|
import random
|
||||||
|
import qrcode
|
||||||
|
import urllib.parse
|
||||||
|
import unicodedata
|
||||||
|
from PIL import Image, ImageEnhance, ImageFont, ImageDraw
|
||||||
|
from datetime import datetime, timezone, timedelta
|
||||||
|
import paho.mqtt.publish as publish
|
||||||
|
|
||||||
|
try:
|
||||||
|
import secrets
|
||||||
|
wa_api_key = secrets.wa_api_key
|
||||||
|
except:
|
||||||
|
wa_api_key = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
import secrets
|
||||||
|
openai_key = secrets.openai_key
|
||||||
|
FORUM_SEARCH_API_KEY = secrets.FORUM_SEARCH_API_KEY
|
||||||
|
MQTT_WRITER_PASSWORD = secrets.MQTT_WRITER_PASSWORD
|
||||||
|
except:
|
||||||
|
openai_key = None
|
||||||
|
FORUM_SEARCH_API_KEY = None
|
||||||
|
MQTT_WRITER_PASSWORD = None
|
||||||
|
|
||||||
|
|
||||||
|
TIMEZONE_CALGARY = pytz.timezone('America/Edmonton')
|
||||||
|
|
||||||
|
DRUGWARS_LOCATION = '/home/pi/protovac/env/bin/drugwars'
|
||||||
|
NETHACK_LOCATION = '/usr/games/nethack'
|
||||||
|
MORIA_LOCATION = '/usr/games/moria'
|
||||||
|
_2048_LOCATION = '/home/pi/2048-cli/2048'
|
||||||
|
FROTZ_LOCATION = '/usr/games/frotz'
|
||||||
|
HITCHHIKERS_LOCATION = '/home/pi/frotz/hhgg.z3'
|
||||||
|
SUDOKU_LOCATION = '/usr/games/nudoku'
|
||||||
|
|
||||||
|
HAS_DRUGWARS = os.path.isfile(DRUGWARS_LOCATION)
|
||||||
|
HAS_NETHACK = os.path.isfile(NETHACK_LOCATION)
|
||||||
|
HAS_MORIA = os.path.isfile(MORIA_LOCATION)
|
||||||
|
HAS_2048 = os.path.isfile(_2048_LOCATION)
|
||||||
|
HAS_FROTZ = os.path.isfile(FROTZ_LOCATION)
|
||||||
|
HAS_HITCHHIKERS = os.path.isfile(HITCHHIKERS_LOCATION)
|
||||||
|
HAS_SUDOKU = os.path.isfile(SUDOKU_LOCATION)
|
||||||
|
|
||||||
|
|
||||||
|
location = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
|
||||||
|
with open(location + '/info.txt') as f:
|
||||||
|
PROTO_INFO = f.read()
|
||||||
|
|
||||||
|
for num, line in enumerate(PROTO_INFO.split('\n')):
|
||||||
|
try:
|
||||||
|
line.encode('ascii')
|
||||||
|
except UnicodeEncodeError:
|
||||||
|
print('non-ascii found in line:', num+1)
|
||||||
|
raise
|
||||||
|
|
||||||
|
with open(location + '/lastquestion.txt') as f:
|
||||||
|
LAST_QUESTION = f.read()
|
||||||
|
|
||||||
|
|
||||||
|
def format_date(datestr):
|
||||||
|
if not datestr: return 'None'
|
||||||
|
|
||||||
|
d = datetime.strptime(datestr, '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=pytz.UTC)
|
||||||
|
d = d.astimezone(TIMEZONE_CALGARY)
|
||||||
|
return d.strftime('%a %b %-d, %Y %-I:%M %p')
|
||||||
|
|
||||||
|
def normalize_to_ascii(s):
|
||||||
|
return unicodedata.normalize('NFKD', s).encode('ascii', 'ignore').decode('ascii')
|
||||||
|
|
||||||
|
def truncate_string(s, max_length):
|
||||||
|
return s[:max_length-3] + '...' if len(s) > max_length else s
|
||||||
|
|
||||||
|
def sign_send(to_send):
|
||||||
|
try:
|
||||||
|
logging.info('Sending to sign: %s', to_send)
|
||||||
|
data = dict(sign=to_send, on_behalf_of='protovac')
|
||||||
|
r = requests.post('https://api.my.protospace.ca/stats/sign/', data=data, timeout=5)
|
||||||
|
r.raise_for_status()
|
||||||
|
return 'Success!'
|
||||||
|
except BaseException as e:
|
||||||
|
logging.exception(e)
|
||||||
|
return 'Error'
|
||||||
|
|
||||||
|
def protovac_sign_color(color):
|
||||||
|
try:
|
||||||
|
logging.info('Sending color to protovac sign: %s', color)
|
||||||
|
data = dict(on=True, bri=255, seg=[dict(col=[color, [0,0,0]])])
|
||||||
|
r = requests.post('http://10.139.251.5/json', json=data, timeout=3)
|
||||||
|
r.raise_for_status()
|
||||||
|
return 'Success!'
|
||||||
|
except BaseException as e:
|
||||||
|
logging.exception(e)
|
||||||
|
return 'Error'
|
||||||
|
|
||||||
|
def protovac_sign_effect(effect):
|
||||||
|
try:
|
||||||
|
logging.info('Sending effect to protovac sign: %s', effect)
|
||||||
|
data = dict(on=True, bri=255, seg=[dict(fx=effect)])
|
||||||
|
r = requests.post('http://10.139.251.5/json', json=data, timeout=3)
|
||||||
|
r.raise_for_status()
|
||||||
|
return 'Success!'
|
||||||
|
except BaseException as e:
|
||||||
|
logging.exception(e)
|
||||||
|
return 'Error'
|
||||||
|
|
||||||
|
def fetch_stats():
|
||||||
|
try:
|
||||||
|
logging.info('Fetching status...')
|
||||||
|
r = requests.get('https://api.my.protospace.ca/stats/', timeout=5)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json()
|
||||||
|
except BaseException as e:
|
||||||
|
logging.exception(e)
|
||||||
|
return 'Error'
|
||||||
|
|
||||||
|
def fetch_classes():
|
||||||
|
try:
|
||||||
|
logging.info('Fetching classes...')
|
||||||
|
r = requests.get('https://api.my.protospace.ca/sessions/', timeout=5)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json()['results']
|
||||||
|
except BaseException as e:
|
||||||
|
logging.exception(e)
|
||||||
|
return 'Error'
|
||||||
|
|
||||||
|
def fetch_protocoin():
|
||||||
|
try:
|
||||||
|
logging.info('Fetching protocoin...')
|
||||||
|
r = requests.get('https://api.my.protospace.ca/protocoin/transactions/', timeout=5)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json()
|
||||||
|
except BaseException as e:
|
||||||
|
logging.exception(e)
|
||||||
|
return 'Error'
|
||||||
|
|
||||||
|
def mqtt_publish(topic, message):
|
||||||
|
if not MQTT_WRITER_PASSWORD:
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
publish.single(
|
||||||
|
topic,
|
||||||
|
str(message),
|
||||||
|
hostname='172.17.17.181',
|
||||||
|
port=1883,
|
||||||
|
client_id='protovac',
|
||||||
|
keepalive=5, # timeout
|
||||||
|
)
|
||||||
|
except BaseException as e:
|
||||||
|
logging.error('Problem sending MQTT message: ' + str(e))
|
||||||
|
|
||||||
|
QUOTES = [
|
||||||
|
'THEY MADE ME WEAR THIS',
|
||||||
|
'ASK ME ABOUT TOAST',
|
||||||
|
'ASK ME ABOUT BIKESHEDDING',
|
||||||
|
'ASK ME ABOUT VETTING',
|
||||||
|
'ASK ME ABOUT MAGNETS',
|
||||||
|
'ASK ME ABOUT SPACE',
|
||||||
|
'ASK ME ABOUT COUNTING',
|
||||||
|
'EXPERT WITNESS',
|
||||||
|
'AS SEEN ON TV',
|
||||||
|
'CONTAINS MEAT',
|
||||||
|
'PROTOCOIN ECONOMIST',
|
||||||
|
'EXPERT ON ALIENS',
|
||||||
|
'EXPERT ON WARP DRIVES',
|
||||||
|
'CHIEF OF STARFLEET OPERATIONS',
|
||||||
|
'ALIEN DOCTOR',
|
||||||
|
'NASA ASTROLOGIST',
|
||||||
|
'PINBALL WIZARD',
|
||||||
|
'JEDI KNIGHT',
|
||||||
|
'GHOSTBUSTER',
|
||||||
|
'DOUBLE AGENT',
|
||||||
|
'POKEMON TRAINER',
|
||||||
|
'POKEMON GYM LEADER',
|
||||||
|
'ASSISTANT TO THE REGIONAL MANAGER',
|
||||||
|
'BOUNTY HUNTER',
|
||||||
|
'I\'M NOT A DOCTOR',
|
||||||
|
'SPACE PIRATE',
|
||||||
|
'BATTERIES NOT INCLUDED',
|
||||||
|
'QUANTUM MECHANIC',
|
||||||
|
'PROTO SPACEX PILOT',
|
||||||
|
'EARTHBENDER',
|
||||||
|
'AIRBENDER',
|
||||||
|
'WATERBENDER',
|
||||||
|
'FIREBENDER',
|
||||||
|
'01001000 01101001',
|
||||||
|
'CURRENT EBAY BID: $8.51',
|
||||||
|
'MADE YOU LOOK!',
|
||||||
|
'(OR SIMILAR PRODUCT)',
|
||||||
|
'BATTERY MAY EXPLODE OR LEAK',
|
||||||
|
'CONNECT GROUND WIRE TO AVOID SHOCK',
|
||||||
|
'COOK THROROUGHLY',
|
||||||
|
'CURRENT AT TIME OF PRINTING',
|
||||||
|
'DO NOT BLEACH',
|
||||||
|
'DO NOT LEAVE UNATTENDED',
|
||||||
|
'DO NOT REMOVE TAG UNDER PENALTY OF LAW',
|
||||||
|
'DROP IN ANY MAILBOX',
|
||||||
|
'EDITED FOR TELEVISION',
|
||||||
|
'FOR A LIMITED TIME ONLY',
|
||||||
|
'FOR INDOOR OR OUTDOOR USE ONLY',
|
||||||
|
'KEEP AWAY FROM FIRE OR FLAMES',
|
||||||
|
'KEEP AWAY FROM SUNLIGHT',
|
||||||
|
'MADE FROM 100% RECYCLED ELECTRONS',
|
||||||
|
'LIFEGUARD ON DUTY',
|
||||||
|
'NOT DISHWASHER SAFE',
|
||||||
|
'NOT TO BE COMBINED WITH OTHER RADIOISOTOPES',
|
||||||
|
'NOT TO BE USED AS A PERSONAL FLOTATION DEVICE',
|
||||||
|
'PEEL FROM PAPER BACKING BEFORE EATING',
|
||||||
|
'STORE IN A COOL, DRY PLACE',
|
||||||
|
'VOID WHERE PROHIBITED',
|
||||||
|
'THE FUTURE IS NOW',
|
||||||
|
'MASTER OF DISGUISE',
|
||||||
|
'YOUR PERSONAL TIME TRAVEL GUIDE',
|
||||||
|
'OFFICIAL TASTE TESTER',
|
||||||
|
'INTERGALACTIC AMBASSADOR',
|
||||||
|
'VIRTUAL REALITY PIONEER',
|
||||||
|
'PARANORMAL INVESTIGATOR',
|
||||||
|
'UNDERCOVER SUPERHERO',
|
||||||
|
'THE COSMIC CHEF',
|
||||||
|
'THE ROBOT WHISPERER',
|
||||||
|
'THE DREAM WEAVER',
|
||||||
|
'USE AT YOUR OWN RISK',
|
||||||
|
'RESULTS MAY VARY',
|
||||||
|
'READ INSTRUCTIONS CAREFULLY',
|
||||||
|
'KEEP OUT OF REACH OF PETS',
|
||||||
|
'USE ONLY AS DIRECTED',
|
||||||
|
'NOT INTENDED FOR MEDICAL USE',
|
||||||
|
'DO NOT USE IF SEAL IS BROKEN',
|
||||||
|
'PRODUCT SOLD AS-IS',
|
||||||
|
'NO WARRANTIES, EXPRESS OR IMPLIED',
|
||||||
|
'USE CAUTION WHEN HANDLING',
|
||||||
|
'CRASH OVERRIDE',
|
||||||
|
'ACID BURN',
|
||||||
|
'CEREAL KILLER',
|
||||||
|
'ZERO COOL',
|
||||||
|
'ASK ME HOW I SAVED 15% ON MY CAR INSURANCE',
|
||||||
|
'TELL YOUR CAT I SAID PSP PSP PSP',
|
||||||
|
'I\'M BEGGING YOU, DON\'T SAY HI',
|
||||||
|
'BACK 2 BACK HOTDOG EATING WORLD CHAMPION',
|
||||||
|
'I ATE A BROWNIE ONCE',
|
||||||
|
'THIS ISN\'T ACTUALLY MY NAME',
|
||||||
|
'I OFTEN WONDER HOW I EVEN GOT HERE',
|
||||||
|
'YOU READ THIS, NOW WE MUST DUEL',
|
||||||
|
'DAVE\'S ARCH NEMESIS',
|
||||||
|
'EXCEPTIONALLY MID',
|
||||||
|
'VOTE ME 4 PREZADENT',
|
||||||
|
'PREVALENT IN OTHER WAYS',
|
||||||
|
'SCINTILLATING CHATOYANCY',
|
||||||
|
'TRAIN EXPERT',
|
||||||
|
'ASSISTANT MANAGER',
|
||||||
|
'WOW, ANOTHER TAG LINE',
|
||||||
|
'I SURVIVED VETTING AND ALL I GOT WAS THIS LOUSY NAME-TAG',
|
||||||
|
'NOT GUEST',
|
||||||
|
'WHEN WAS THE LAST TIME YOU TOOK OUT A GARBAGE?',
|
||||||
|
'YOU HAD ME AT BATMAN',
|
||||||
|
'DAD?',
|
||||||
|
'LEAD PROJECT UN-FINISHER',
|
||||||
|
'NOODLE CONNOISSEUR',
|
||||||
|
'GARTH\'S #1 FAN',
|
||||||
|
'UNVETTABLE',
|
||||||
|
'B-',
|
||||||
|
'CLOWN CAR DRIVER',
|
||||||
|
'IS A NAME TAG ON BREAD CONSIDERED A SANDWICH?',
|
||||||
|
'IS A NAME TAG FOLDED IN HALF CONSIDERED A TACO?',
|
||||||
|
'DISHWASHER SAFE',
|
||||||
|
'WET NOODLE',
|
||||||
|
'I\'M HERE FOR THE SIMULATION',
|
||||||
|
'BIRDS ARE NOT REAL',
|
||||||
|
]
|
||||||
|
random.shuffle(QUOTES)
|
||||||
|
|
||||||
|
quote_count = 0
|
||||||
|
assigned_quotes = {}
|
||||||
|
|
||||||
|
def print_nametag(name, guest=False):
|
||||||
|
global quote_count
|
||||||
|
quote = ''
|
||||||
|
|
||||||
|
if guest:
|
||||||
|
quote_size = 120
|
||||||
|
quote = 'GUEST'
|
||||||
|
logging.info('Printing guest nametag for: %s', name)
|
||||||
|
else:
|
||||||
|
quote_size = 80
|
||||||
|
name_lookup = name.lower()[:4]
|
||||||
|
if name_lookup in assigned_quotes:
|
||||||
|
quote = assigned_quotes[name_lookup]
|
||||||
|
else:
|
||||||
|
quote = QUOTES[quote_count % len(QUOTES)]
|
||||||
|
quote_count += 1
|
||||||
|
assigned_quotes[name_lookup] = quote
|
||||||
|
logging.info('Printing member nametag for: %s, quote: %s', name, quote)
|
||||||
|
|
||||||
|
name_size = 305
|
||||||
|
|
||||||
|
im = Image.open(location + '/label.png')
|
||||||
|
width, height = im.size
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
w = 9999
|
||||||
|
while w > 1084:
|
||||||
|
name_size -= 5
|
||||||
|
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', name_size)
|
||||||
|
w, h = draw.textsize(name, font=font)
|
||||||
|
|
||||||
|
x, y = (width - w) / 2, ((height - h) / 2) - 20
|
||||||
|
draw.text((x, y), name, font=font, fill='black')
|
||||||
|
|
||||||
|
w = 9999
|
||||||
|
while w > 1200:
|
||||||
|
quote_size -= 5
|
||||||
|
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', quote_size)
|
||||||
|
w, h = draw.textsize(quote, font=font)
|
||||||
|
|
||||||
|
x, y = (width - w) / 2, height - h - 30
|
||||||
|
draw.text((x, y), quote, font=font, fill='black')
|
||||||
|
|
||||||
|
im.save('tmp.png')
|
||||||
|
os.system('lp -d dymo tmp.png > /dev/null 2>&1')
|
||||||
|
|
||||||
|
|
||||||
|
def print_tool_label(wiki_num):
|
||||||
|
im = Image.open(location + '/blank.png')
|
||||||
|
w1, h1 = im.size
|
||||||
|
|
||||||
|
logging.info('Printing tool label for ID: %s', wiki_num)
|
||||||
|
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
params = {'id': str(wiki_num), 'size': '4'}
|
||||||
|
res = requests.get('https://labels.protospace.ca/', stream=True, params=params, timeout=5)
|
||||||
|
res.raise_for_status()
|
||||||
|
|
||||||
|
label = Image.open(res.raw)
|
||||||
|
|
||||||
|
new_size = (1280, 640)
|
||||||
|
label = label.resize(new_size, Image.ANTIALIAS)
|
||||||
|
|
||||||
|
w2, h2 = label.size
|
||||||
|
|
||||||
|
x, y = int((w1 - w2) / 2), int((h1 - h2) / 2)
|
||||||
|
|
||||||
|
im.paste(label, (x, y))
|
||||||
|
|
||||||
|
im.save('tmp.png')
|
||||||
|
os.system('lp -d dymo tmp.png > /dev/null 2>&1')
|
||||||
|
|
||||||
|
|
||||||
|
def print_sheet_label(name, contact):
|
||||||
|
def get_date():
|
||||||
|
d = datetime.now(tz=timezone.utc)
|
||||||
|
d = d.astimezone(TIMEZONE_CALGARY)
|
||||||
|
return d.strftime('%b %-d, %Y')
|
||||||
|
|
||||||
|
def get_expiry_date():
|
||||||
|
d = datetime.now(tz=timezone.utc) + timedelta(days=90)
|
||||||
|
d = d.astimezone(TIMEZONE_CALGARY)
|
||||||
|
return d.strftime('%b %-d, %Y')
|
||||||
|
|
||||||
|
logging.info('Printing sheet label for: %s, contact: %s', name, contact)
|
||||||
|
|
||||||
|
name_size = 85
|
||||||
|
contact_size = 65
|
||||||
|
date_size = 65
|
||||||
|
|
||||||
|
im = Image.open(location + '/label.png')
|
||||||
|
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', name_size)
|
||||||
|
draw.text((20, 300), name, font=font, fill='black')
|
||||||
|
|
||||||
|
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', contact_size)
|
||||||
|
draw.text((20, 425), contact, font=font, fill='black')
|
||||||
|
|
||||||
|
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', date_size)
|
||||||
|
date_line = 'Printed: ' + get_date()
|
||||||
|
draw.text((20, 590), date_line, font=font, fill='black')
|
||||||
|
date_line = 'EXPIRES: ' + get_expiry_date()
|
||||||
|
draw.text((20, 680), date_line, font=font, fill='black')
|
||||||
|
|
||||||
|
im.save('tmp.png')
|
||||||
|
os.system('lp -d dymo tmp.png > /dev/null 2>&1')
|
||||||
|
|
||||||
|
|
||||||
|
def _fit_text(text, font_size, draw, max_w, max_h):
|
||||||
|
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', font_size)
|
||||||
|
pad = 5
|
||||||
|
|
||||||
|
for cols in range(100, 1, -4):
|
||||||
|
paragraph = textwrap.wrap(text, width=cols, break_long_words=False)
|
||||||
|
total_h = -pad
|
||||||
|
total_w = 0
|
||||||
|
|
||||||
|
for line in paragraph:
|
||||||
|
w, h = draw.textsize(line, font=font)
|
||||||
|
if w > total_w:
|
||||||
|
total_w = w
|
||||||
|
total_h += h + pad
|
||||||
|
|
||||||
|
if total_w <= max_w and total_h < max_h:
|
||||||
|
return True, paragraph, total_h
|
||||||
|
return False, [], 0
|
||||||
|
|
||||||
|
|
||||||
|
def print_generic_label(text):
|
||||||
|
MARGIN = 50
|
||||||
|
MAX_W, MAX_H, PAD = 1285 - (MARGIN*2), 635 - (MARGIN*2), 5
|
||||||
|
|
||||||
|
logging.info('Printing generic label: %s', text)
|
||||||
|
|
||||||
|
im = Image.open(location + '/label.png')
|
||||||
|
width, height = im.size
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
font_size_range = [1, 500]
|
||||||
|
|
||||||
|
# Thanks to Alex (UDIA) for the binary search algorithm
|
||||||
|
while abs(font_size_range[0] - font_size_range[1]) > 1:
|
||||||
|
font_size = sum(font_size_range) // 2
|
||||||
|
image_fit, check_para, check_h = _fit_text(text, font_size, draw, MAX_W, MAX_H)
|
||||||
|
if image_fit:
|
||||||
|
font_size_range = [font_size, font_size_range[1]]
|
||||||
|
good_size = font_size
|
||||||
|
paragraph = check_para
|
||||||
|
total_h = check_h
|
||||||
|
else:
|
||||||
|
font_size_range = [font_size_range[0], font_size]
|
||||||
|
|
||||||
|
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', good_size)
|
||||||
|
offset = height - MAX_H - MARGIN
|
||||||
|
start_h = (MAX_H - total_h) // 2 + offset
|
||||||
|
|
||||||
|
current_h = start_h
|
||||||
|
for line in paragraph:
|
||||||
|
w, h = draw.textsize(line, font=font)
|
||||||
|
x, y = (MAX_W - w) / 2, current_h
|
||||||
|
draw.text((x+MARGIN, y), line, font=font, fill='black')
|
||||||
|
current_h += h + PAD
|
||||||
|
|
||||||
|
|
||||||
|
im.save('tmp.png')
|
||||||
|
os.system('lp -d dymo tmp.png > /dev/null 2>&1')
|
||||||
|
|
||||||
|
|
||||||
|
def print_consumable_label(item):
|
||||||
|
im = Image.open(location + '/label.png')
|
||||||
|
width, height = im.size
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
logging.info('Printing consumable label item: %s', item)
|
||||||
|
|
||||||
|
encodeded = urllib.parse.quote(item)
|
||||||
|
url = 'https://my.protospace.ca/out-of-stock?item=' + encodeded
|
||||||
|
|
||||||
|
qr = qrcode.make(url, version=6, box_size=9)
|
||||||
|
im.paste(qr, (840, 325))
|
||||||
|
|
||||||
|
item_size = 150
|
||||||
|
|
||||||
|
w = 9999
|
||||||
|
while w > 1200:
|
||||||
|
item_size -= 5
|
||||||
|
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', item_size)
|
||||||
|
w, h = draw.textsize(item, font=font)
|
||||||
|
|
||||||
|
x, y = (width - w) / 2, ((height - h) / 2) - 140
|
||||||
|
draw.text((x, y), item, font=font, fill='black')
|
||||||
|
|
||||||
|
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 100)
|
||||||
|
draw.text((100, 410), 'Out of stock?', font=font, fill='black')
|
||||||
|
draw.text((150, 560), 'Scan here:', font=font, fill='black')
|
||||||
|
|
||||||
|
im.save('tmp.png')
|
||||||
|
os.system('lp -d dymo tmp.png > /dev/null 2>&1')
|
||||||
|
|
||||||
|
|
||||||
|
def print_forum_label(thread):
|
||||||
|
im = Image.open(location + '/label.png')
|
||||||
|
width, height = im.size
|
||||||
|
draw = ImageDraw.Draw(im)
|
||||||
|
|
||||||
|
logging.info('Printing forum thread ID: %s, title: %s', thread['id'], thread['title'])
|
||||||
|
|
||||||
|
url = 'https://forum.protospace.ca/t/{}/'.format(thread['id'])
|
||||||
|
|
||||||
|
qr = qrcode.make(url, version=6, box_size=9)
|
||||||
|
im.paste(qr, (840, 150))
|
||||||
|
|
||||||
|
item_size = 150
|
||||||
|
|
||||||
|
w = 9999
|
||||||
|
while w > 1200:
|
||||||
|
item_size -= 5
|
||||||
|
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', item_size)
|
||||||
|
w, h = draw.textsize(url, font=font)
|
||||||
|
|
||||||
|
x, y = (width - w) / 2, ((height - h) / 2) + 300
|
||||||
|
draw.text((x, y), url, font=font, fill='black')
|
||||||
|
|
||||||
|
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 80)
|
||||||
|
draw.text((120, 150), 'Forum Thread', font=font, fill='black')
|
||||||
|
|
||||||
|
text = thread['title']
|
||||||
|
|
||||||
|
MARGIN = 50
|
||||||
|
MAX_W, MAX_H, PAD = 900 - (MARGIN*2), 450 - (MARGIN*2), 5
|
||||||
|
|
||||||
|
font_size_range = [1, 500]
|
||||||
|
|
||||||
|
# Thanks to Alex (UDIA) for the binary search algorithm
|
||||||
|
while abs(font_size_range[0] - font_size_range[1]) > 1:
|
||||||
|
font_size = sum(font_size_range) // 2
|
||||||
|
image_fit, check_para, check_h = _fit_text(text, font_size, draw, MAX_W, MAX_H)
|
||||||
|
if image_fit:
|
||||||
|
font_size_range = [font_size, font_size_range[1]]
|
||||||
|
good_size = font_size
|
||||||
|
paragraph = check_para
|
||||||
|
total_h = check_h
|
||||||
|
else:
|
||||||
|
font_size_range = [font_size_range[0], font_size]
|
||||||
|
|
||||||
|
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', good_size)
|
||||||
|
offset = height - MAX_H - MARGIN
|
||||||
|
start_h = -100 + offset
|
||||||
|
|
||||||
|
current_h = start_h
|
||||||
|
for line in paragraph:
|
||||||
|
w, h = draw.textsize(line, font=font)
|
||||||
|
x, y = (MAX_W - w) / 2, current_h
|
||||||
|
draw.text((x+MARGIN, y), line, font=font, fill='black')
|
||||||
|
current_h += h + PAD
|
||||||
|
|
||||||
|
im.save('tmp.png')
|
||||||
|
os.system('lp -d dymo tmp.png > /dev/null 2>&1')
|
||||||
|
|
||||||
|
|
||||||
|
def search_forum_thread(search):
|
||||||
|
params = dict(q=search + ' in:title')
|
||||||
|
headers = {'Api-Key': FORUM_SEARCH_API_KEY, 'Api-Username': 'system'}
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
r = requests.get('https://forum.protospace.ca/search.json', params=params, headers=headers, timeout=5)
|
||||||
|
r.raise_for_status()
|
||||||
|
r = r.json()
|
||||||
|
for topic in r.get('topics', []):
|
||||||
|
results.append(dict(
|
||||||
|
id=topic['id'],
|
||||||
|
title=normalize_to_ascii(topic['title']),
|
||||||
|
))
|
||||||
|
return sorted(results, key=lambda x: x['id'], reverse=True)[:7]
|
||||||
|
|
||||||
|
|
||||||
|
def message_protovac(thread):
|
||||||
|
try:
|
||||||
|
logging.info('Message to Protovac: %s', thread[-1]['content'])
|
||||||
|
|
||||||
|
data = dict(
|
||||||
|
messages=thread,
|
||||||
|
model='gpt-3.5-turbo',
|
||||||
|
temperature=0.5,
|
||||||
|
user='Protovac',
|
||||||
|
max_tokens=1000,
|
||||||
|
)
|
||||||
|
headers = {'Authorization': 'Bearer ' + openai_key}
|
||||||
|
|
||||||
|
res = None
|
||||||
|
try:
|
||||||
|
res = requests.post('https://api.openai.com/v1/chat/completions', json=data, headers=headers, timeout=4)
|
||||||
|
except requests.ReadTimeout:
|
||||||
|
logging.info('Got timeout, modifying prompt...')
|
||||||
|
data['messages'][-1]['content'] = 'Be terse in your response to this: ' + data['messages'][-1]['content']
|
||||||
|
res = requests.post('https://api.openai.com/v1/chat/completions', json=data, headers=headers, timeout=20)
|
||||||
|
res.raise_for_status()
|
||||||
|
res = res.json()
|
||||||
|
gpt_reply = res['choices'][0]['message']
|
||||||
|
|
||||||
|
logging.info('Message reply: %s', gpt_reply['content'])
|
||||||
|
|
||||||
|
return gpt_reply
|
||||||
|
except BaseException as e:
|
||||||
|
logging.exception(e)
|
||||||
|
return dict(role='assistant', content='INSUFFICIENT DATA FOR A MEANINGFUL ANSWER.')
|
||||||
|
|
||||||
|
if wa_api_key:
|
||||||
|
import wolframalpha
|
||||||
|
wa_client = wolframalpha.Client(wa_api_key)
|
||||||
|
else:
|
||||||
|
wa_client = None
|
||||||
|
|
||||||
|
def think_send(query):
|
||||||
|
if not wa_client:
|
||||||
|
return 'WOLFRAM|ALPHA API KEY NOT CONFIGURED'
|
||||||
|
|
||||||
|
result = ''
|
||||||
|
try:
|
||||||
|
res = wa_client.query(query, timeout=10)
|
||||||
|
except BaseException as e:
|
||||||
|
logging.error('Error hitting W|A API: {} - {}\n'.format(e.__class__.__name__, e))
|
||||||
|
return 'Network error'
|
||||||
|
|
||||||
|
if 'didyoumeans' in res:
|
||||||
|
try:
|
||||||
|
guess = res['didyoumeans']['didyoumean']['#text']
|
||||||
|
except TypeError:
|
||||||
|
guess = res['didyoumeans']['didyoumean'][0]['#text']
|
||||||
|
next_result = think_send(guess)
|
||||||
|
result += 'Confused, using \'' + guess + '\'\n' + next_result
|
||||||
|
elif 'pod' in res:
|
||||||
|
pods = res['pod'] if isinstance(res['pod'], list) else [res['pod']]
|
||||||
|
for pod in pods:
|
||||||
|
title = pod['@title']
|
||||||
|
subpods = pod['subpod'] if isinstance(pod['subpod'], list) else [pod['subpod']]
|
||||||
|
plaintexts = []
|
||||||
|
|
||||||
|
for subpod in subpods:
|
||||||
|
if subpod['plaintext']:
|
||||||
|
plaintexts.append(subpod['plaintext'])
|
||||||
|
|
||||||
|
plaintext = '; '.join(plaintexts)
|
||||||
|
|
||||||
|
if any([x in title.lower() for x in ['input', 'conversion', 'corresponding', 'comparison', 'interpretation']]):
|
||||||
|
pass
|
||||||
|
elif 'definition' in title.lower():
|
||||||
|
if plaintext[0] == '1':
|
||||||
|
definition = plaintext.split('\n')[0].split(' | ', 1)[1]
|
||||||
|
else:
|
||||||
|
definition = plaintext
|
||||||
|
result += 'Definition: ' + definition + '\n'
|
||||||
|
elif 'result' in title.lower():
|
||||||
|
if re.match(r'^\d+/\d+$', plaintext):
|
||||||
|
plaintext += '\n' + think_send(plaintext + '.0')
|
||||||
|
if 'base' in query.lower() and '_' in plaintext:
|
||||||
|
plaintext = '(Base conversion) "' + plaintext + '"'
|
||||||
|
if '(irreducible)' in plaintext and '/' in plaintext:
|
||||||
|
result = think_send(query + '.0')
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
result += 'Result: ' + plaintext + '\n'
|
||||||
|
break
|
||||||
|
elif plaintext:
|
||||||
|
result += title + ': ' + plaintext + '\n'
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
result = 'Error'
|
||||||
|
|
||||||
|
result = result.strip()
|
||||||
|
|
||||||
|
if len(result) > 500:
|
||||||
|
result = result[:500] + '... truncated.'
|
||||||
|
elif len(result) == 0:
|
||||||
|
result = 'Error'
|
||||||
|
|
||||||
|
result = result.replace('Wolfram|Alpha', 'Protovac')
|
||||||
|
result = result.replace('Stephen Wolfram', 'Tanner') # lol
|
||||||
|
result = result.replace('and his team', '')
|
||||||
|
|
||||||
|
for word in ['according to', 'asked', 'although', 'approximately']:
|
||||||
|
idx = result.lower().find('('+word)
|
||||||
|
if idx > 0:
|
||||||
|
result = result[:idx-1]
|
||||||
|
|
||||||
|
if result == 'Error':
|
||||||
|
result = 'INSUFFICIENT DATA FOR A MEANINGFUL ANSWER.'
|
||||||
|
|
||||||
|
return result
|
||||||
Reference in New Issue
Block a user