diff options
-rwxr-xr-x | clippy.py | 156 |
1 files changed, 156 insertions, 0 deletions
diff --git a/clippy.py b/clippy.py new file mode 100755 index 0000000..f287e1b --- /dev/null +++ b/clippy.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 + +import json +import random +import socket +import struct +import time +import numpy +import bz2 +import os + +from PIL import Image + +from pixelterm import pixelterm + +HOST, PORT = "172.23.42.29",2342 +DISPLAY_WIDTH, DISPLAY_HEIGHT = 56*8, 12*19 +CMD_LED_DRAW = 18 + +def resize_image(img, size): + tw, th = size + w, h = img.size + a, b = w/tw, h/th + f = 1/max(a, b) + pos = int((tw-w*f)/2), int((th-h*f)/2) + buf = Image.new('RGBA', (tw, th)) + buf.paste(img.resize((int(w*f), int(h*f))).convert('RGBA'), pos) + buf2 = Image.new('RGBA', (tw, th), (0, 0, 0, 255)) + return Image.alpha_composite(buf2, buf) + +class Display: + def __init__(self): + self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + + def sendframe(self, frame): + buf = numpy.frombuffer(frame.tobytes(), dtype='1b') + print('sendbuf: len', len(buf)) + buf_narfed = numpy.concatenate([ buf[(i*12)*DISPLAY_WIDTH: (i*12+8)*DISPLAY_WIDTH] for i in range(20) ]) +# txdata = numpy.packbits(buf_narfed).tobytes() + txdata = buf_narfed + self.sock.sendto(struct.pack('!HHHHH', CMD_LED_DRAW, 0, + (56*8*(12*20-8))%65536, 0x627a, 0) + # do. not. fucking. ask. + bz2.compress(txdata), (HOST, PORT)) + + @staticmethod + def do_gamma(im, gamma): + """Fast gamma correction with PIL's image.point() method""" + invert_gamma = 1.0/gamma + lut = [pow(x/255., invert_gamma) * 255 for x in range(256)] + lut = lut*4 # need one set of data for each band for RGBA + im = im.point(lut) + return im + +def weightedChoice(choices, default=None): + acc = 0 + r = random.random() + for weight, choice in choices: + if r < (acc + weight): + return choice + acc += weight + return default + +class Agent: + def __init__(self, path: 'pathlib.Path'): + self.config = json.loads((path / 'agent.json').read_text()) + for ani in self.config['animations'].values(): + for f in ani['frames']: + branching, exitBranch = f.get('branching'), f.get('exitBranch') + if 'branching' in f: + f['next'] = lambda f, idx: weightedChoice( + [ (b['weight']/100, b['frameIndex']) for b in f['branching']['branches'] ] + , default=idx+1) + elif 'exitBranch' in f: + f['next'] = lambda f, idx: f['exitBranch'] + else: + f['next'] = lambda f, idx: idx+1 + if 'images' in f: + self.sendframe( + Display.do_gamma( + resize_image(img, (DISPLAY_WIDTH, DISPLAY_HEIGHT)), 0.5).convert('1')) + self.picmap = Image.open(path / 'map.png') + self.path = path + + def __call__(self, action): + print('Playing:', action) + for frame in self._animate(action): + print('frame:', frame) + if 'images' in frame: # some frames contain branch info and sound, but no images + yield self.get_image(*frame['images'][0]) + time.sleep(frame['duration']/1000) + + def _animate(self, action): + anim, idx = self.config['animations'][action]['frames'], 0 + while idx < len(anim): + yield anim[idx] + idx = anim[idx]['next'](anim[idx], idx) + + def get_image(self, x, y): + print('cropbox:', x, y, *self.config['framesize'], 'map:', self.picmap.size) + tw, th = self.config['framesize'] + return self.picmap.crop((x, y, x+tw, y+th)) + + @property + def animations(self): + return list(self.config['animations'].keys()) + +if __name__ == '__main__': + import argparse, pathlib, sys + + parser = argparse.ArgumentParser() + parser.add_argument('-l', '--list', action='store_true') + parser.add_argument('-a', '--agent', default='Clippy') + parser.add_argument('-e', '--endless', action='store_true') + parser.add_argument('-d', '--display', action='store_true') + parser.add_argument('-t', '--terminal', action='store_true') + parser.add_argument('action', default='Greeting', nargs='?') + args = parser.parse_args() + + agent_path = pathlib.Path('agents') / args.agent + if not agent_path.is_dir(): + print('Agent not found. Exiting.') + sys.exit(1) + + if args.list: + print('\n'.join(Agent(agent_path).animations)) + sys.exit(0) + + dsp = Display() + + tx, ty = os.get_terminal_size() + termsize = (tx, ty*2) + if args.endless: + agent = Agent(agent_path) + while True: + if random.random() > 0.2: + for img_dsp, img_term in agent(random.choice(agent.animations)): + if args.terminal: + print('\033[H', end='') + print(pixelterm.termify_pixels( + resize_image(img, termsize))) + if args.display: + dsp.sendframe(img_dsp) + time.sleep(1) + + else: + for img_dsp, img_term in Agent(agent_path)(args.action): + if args.terminal: + print(pixelterm.termify_pixels( + resize_image(img, termsize))) +# print(pixelterm.termify_pixels( +# img.thumbnail((80, 40)) +# .convert('1', dither=Image.FLOYDSTEINBERG) +# .convert('RGBA', dither=Image.FLOYDSTEINBERG))) +# print('display size:', (DISPLAY_WIDTH, DISPLAY_HEIGHT)) + if args.display: + dsp.sendframe(img_dsp) |