Download server.py from Vishwasaidev/quill: direct link, hf CLI and curl.
- Browser
- Download file 6.92 kB
-
https://huggingface.co/Vishwasaidev/quill/resolve/main/server.py
- Command line
-
hf download hf://Vishwasaidev/quill/server.py
-
curl -L -o server.py https://huggingface.co/Vishwasaidev/quill/resolve/main/server.py
6.92 kB
| """Local test server for Quill. | |
| python server.py -> http://127.0.0.1:8792 | |
| """ | |
| import json | |
| import os | |
| import sys | |
| from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer | |
| from urllib.parse import urlparse | |
| from api import render_query | |
| from quill import INKS, STYLES | |
| PORT = int(os.environ.get("PORT") or (sys.argv[1] if len(sys.argv) > 1 else 8792)) | |
| HOST = "0.0.0.0" if "PORT" in os.environ else "127.0.0.1" # Heroku sets PORT | |
| PAGE = """<!doctype html> | |
| <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>Quill</title> | |
| <style> | |
| :root { --bg:#f6f5f1; --fg:#1d1d1f; --muted:#6b6b70; --card:#fff; --line:#dcdad4; --accent:#7a3b1e; } | |
| @media (prefers-color-scheme: dark) { :root { --bg:#16161a; --fg:#ececf0; --muted:#9a9aa3; --card:#202026; --line:#33333b; --accent:#d08a5c; } } | |
| body { margin:0; background:var(--bg); color:var(--fg); font:15px/1.5 system-ui, sans-serif; } | |
| main { max-width:1000px; margin:0 auto; padding:24px 16px 48px; } | |
| h1 { font-size:22px; margin:0 0 4px; } .sub { color:var(--muted); margin:0 0 16px; } | |
| textarea, select { padding:10px 12px; font:inherit; border:1px solid var(--line); border-radius:8px; | |
| background:var(--card); color:var(--fg); } | |
| textarea { width:100%; box-sizing:border-box; min-height:84px; resize:vertical; } | |
| .controls { display:flex; flex-wrap:wrap; gap:12px; align-items:center; margin:10px 0 0; } | |
| label { display:flex; gap:6px; align-items:center; color:var(--muted); } | |
| button { padding:10px 18px; font:inherit; border:0; border-radius:8px; background:var(--accent); color:#fff; cursor:pointer; } | |
| button.ghost { background:var(--card); color:var(--fg); border:1px solid var(--line); } | |
| button:disabled { opacity:.5; cursor:wait; } | |
| .chips { display:flex; flex-wrap:wrap; gap:6px; margin-top:12px; } | |
| .chip { padding:4px 10px; border:1px solid var(--line); border-radius:999px; background:var(--card); cursor:pointer; font-size:13px; } | |
| #out { margin-top:20px; display:grid; gap:16px; } | |
| figure { margin:0; } figure img { max-width:100%; display:block; border-radius:10px; border:1px solid var(--line); } | |
| figcaption { color:var(--muted); font-size:13px; margin-top:4px; display:flex; gap:12px; } | |
| figcaption a { color:var(--accent); } | |
| #status { color:var(--muted); margin-top:12px; min-height:1.5em; } | |
| </style></head><body><main> | |
| <h1>Quill</h1> | |
| <p class="sub">Calligraphy with hand-set pens over public stroke data (Hershey letters, KanjiVG kanji and kana). No training. Kanji or kana switch to the shodo brush automatically.</p> | |
| <textarea id="t">Ars longa, vita brevis</textarea> | |
| <div class="controls"> | |
| <label>Style <select id="style">__STYLES__</select></label> | |
| <label>Ink <select id="ink"><option value="">style default</option>__INKS__</select></label> | |
| <label>Size <input id="size" type="range" min="32" max="140" value="72"><span id="sv">72</span>px</label> | |
| <label><input id="fl" type="checkbox"> Flourish</label> | |
| <label><input id="ini" type="checkbox"> Red initial</label> | |
| <label>Seal <input id="seal" maxlength="4" size="4" placeholder="和 / VK"></label> | |
| <label><input id="anim" type="checkbox"> Animate</label> | |
| <button id="go">Write</button> | |
| <button id="all" class="ghost">All styles</button> | |
| </div> | |
| <div class="chips" id="chips"></div> | |
| <div id="status"></div> | |
| <div id="out"></div> | |
| </main><script> | |
| const $ = id => document.getElementById(id); | |
| const demos = ["Ars longa, vita brevis", "Happy Birthday\\nwith love from all of us", "Here beginneth the tale", | |
| "With kind regards", "Quiet rain on the lake", "一期一会\\n花鳥風月", "永", "夢", "ありがとう"]; | |
| demos.forEach(d => { const c = document.createElement('span'); c.className = 'chip'; c.textContent = d.replace('\\n', ' / '); | |
| c.onclick = () => { $('t').value = d; write([$('style').value]); }; $('chips').appendChild(c); }); | |
| $('size').oninput = () => $('sv').textContent = $('size').value; | |
| async function write(styles) { | |
| $('go').disabled = $('all').disabled = true; $('out').innerHTML = ''; | |
| for (const s of styles) { | |
| $('status').textContent = 'Writing ' + s + '…'; | |
| const q = new URLSearchParams({ text: $('t').value, style: s, ink: $('ink').value, size: $('size').value, | |
| flourish: $('fl').checked ? '1' : '', initial: $('ini').checked ? '1' : '', | |
| seal: $('seal').value, animate: $('anim').checked ? '1' : '' }); | |
| const t0 = performance.now(); | |
| const r = await fetch('/render?' + q); | |
| const j = await r.json(); | |
| if (!r.ok) { $('status').textContent = j.error; break; } | |
| const f = document.createElement('figure'); | |
| f.innerHTML = '<img alt=""><figcaption><span></span><a download>Download PNG</a></figcaption>'; | |
| f.querySelector('img').src = f.querySelector('a').href = j.src; | |
| const ext = j.src.slice(11, j.src.indexOf(';')); | |
| f.querySelector('a').download = 'quill_' + s + '.' + ext; | |
| f.querySelector('a').textContent = ext === 'png' ? 'Download PNG' : 'Download animation'; | |
| f.querySelector('span').textContent = (j.style || s) + ' · ' + ((performance.now() - t0) / 1000).toFixed(1) + ' s'; | |
| $('out').appendChild(f); | |
| } | |
| $('status').textContent = ''; $('go').disabled = $('all').disabled = false; | |
| } | |
| $('go').onclick = () => write([$('style').value]); | |
| $('all').onclick = () => write(/[\\u3040-\\u9fff]/.test($('t').value) ? ['shodo'] : __ALL__.filter(s => s !== 'shodo')); | |
| write([$('style').value]); | |
| </script></body></html>""" | |
| def page(): | |
| opts = lambda xs: "".join(f'<option value="{x}">{x}</option>' for x in xs) | |
| return (PAGE.replace("__STYLES__", opts(STYLES)).replace("__INKS__", opts(INKS)) | |
| .replace("__ALL__", json.dumps(list(STYLES)))) | |
| class Handler(BaseHTTPRequestHandler): | |
| protocol_version = "HTTP/1.1" # HTTP/1.0 closes right after writing; on this Windows box that RSTs larger bodies | |
| def send(self, body, ctype, code=200): | |
| self.send_response(code) | |
| self.send_header("Content-Type", ctype) | |
| self.send_header("Content-Length", str(len(body))) | |
| self.send_header("Cache-Control", "no-store") | |
| self.end_headers() | |
| self.wfile.write(body) | |
| def do_GET(self): | |
| url = urlparse(self.path) | |
| if url.path == "/": | |
| self.send(page().encode(), "text/html; charset=utf-8") | |
| elif url.path == "/render": | |
| code, body = render_query(url.query) | |
| self.send(json.dumps(body).encode(), "application/json", code) | |
| else: | |
| self.send(b"not found", "text/plain", 404) | |
| def log_message(self, *a): | |
| pass | |
| if __name__ == "__main__": | |
| print(f"Quill on http://{HOST}:{PORT}", flush=True) | |
| ThreadingHTTPServer((HOST, PORT), Handler).serve_forever() | |