Compare commits
19 Commits
19fd57483d
...
modular-re
| Author | SHA1 | Date | |
|---|---|---|---|
| dee45e5eae | |||
| eaaa8e5f57 | |||
| a19aec1848 | |||
| af6072627e | |||
| 339a58e1bf | |||
| 3fc74d82ad | |||
| 17e3ad347a | |||
| c58a356c02 | |||
| d8886e80db | |||
| fdfefffda6 | |||
| 3b1e0c481e | |||
| 858d941b3f | |||
| 8b65026401 | |||
| 52c417b176 | |||
| 6ff226543d | |||
| fe299ba2b5 | |||
| 7bc5e02a7c | |||
| 016d40938c | |||
| c0fe15cfb8 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -115,3 +115,4 @@ venv.bak/
|
||||
|
||||
secrets.py
|
||||
tmp.png
|
||||
.aider*
|
||||
|
||||
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!'))
|
||||
@@ -1,11 +1,8 @@
|
||||
wa_api_key = ''
|
||||
|
||||
# Get these from your network inspector after sending a message to character.ai
|
||||
character_ai_cookies = {
|
||||
'_legacy_auth0.whatever.is.authenticated': 'true',
|
||||
'auth0.whatever.is.authenticated': 'true',
|
||||
'messages': '',
|
||||
'csrftoken': '',
|
||||
'sessionid': '',
|
||||
}
|
||||
character_ai_token = ''
|
||||
openai_key = ''
|
||||
|
||||
MQTT_WRITER_PASSWORD = ''
|
||||
|
||||
FORUM_SEARCH_API_KEY = ''
|
||||
|
||||
|
||||
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