polats commited on
Commit
76d7f3c
·
verified ·
1 Parent(s): c9dd954

Deploy Karate Wiener (kimodo kata maker)

Browse files
.comic-cache/kmd_0c84d8afaab845cb.json ADDED
The diff for this file is too large to render. See raw diff
 
.comic-cache/kmd_11c32c612d4243e4.json ADDED
The diff for this file is too large to render. See raw diff
 
.comic-cache/kmd_1afbd86fd3fe46f6.json ADDED
The diff for this file is too large to render. See raw diff
 
.comic-cache/kmd_1d660f04319e4746.json ADDED
The diff for this file is too large to render. See raw diff
 
.comic-cache/kmd_1d6e16cc6e97440c.json ADDED
The diff for this file is too large to render. See raw diff
 
.comic-cache/kmd_1f952002fc964515.json ADDED
The diff for this file is too large to render. See raw diff
 
.comic-cache/kmd_611c1728665acc5b.json ADDED
The diff for this file is too large to render. See raw diff
 
.comic-cache/kmd_96a1e7e3cc6961b3.json ADDED
The diff for this file is too large to render. See raw diff
 
.comic-cache/kmd_9e1940e54f9a8163.json ADDED
The diff for this file is too large to render. See raw diff
 
.comic-cache/kmd_a768b4f3a338424c.json ADDED
The diff for this file is too large to render. See raw diff
 
.comic-cache/kmd_af19653214a96ac1.json ADDED
The diff for this file is too large to render. See raw diff
 
.comic-cache/kmd_da99023f4a484a20.json ADDED
The diff for this file is too large to render. See raw diff
 
.comic-cache/kmd_ea04338a30114bc2.json ADDED
The diff for this file is too large to render. See raw diff
 
.gitattributes CHANGED
@@ -98,3 +98,4 @@ kick_stances_verify.png filter=lfs diff=lfs merge=lfs -text
98
  stance_library_verify.png filter=lfs diff=lfs merge=lfs -text
99
  assets/default_scene.webp filter=lfs diff=lfs merge=lfs -text
100
  assets/citizen_skin_color.png filter=lfs diff=lfs merge=lfs -text
 
 
98
  stance_library_verify.png filter=lfs diff=lfs merge=lfs -text
99
  assets/default_scene.webp filter=lfs diff=lfs merge=lfs -text
100
  assets/citizen_skin_color.png filter=lfs diff=lfs merge=lfs -text
101
+ comic_sheet_v0.png filter=lfs diff=lfs merge=lfs -text
app.py CHANGED
@@ -3601,10 +3601,10 @@ def improve_dojo_prompt(payload_json: str | dict | None) -> str:
3601
  # Prefer the first substantial line (drops any stray preamble the model adds).
3602
  best = ""
3603
  for line in text.splitlines():
3604
- cand = line.strip().strip('"').strip("'").lstrip("-*•0123456789. ").strip()
3605
  if len(cand) > len(best):
3606
  best = cand
3607
- text = " ".join((best or text).split())[:500]
3608
  if not text:
3609
  return json.dumps({"ok": False, "error": "empty suggestion"})
3610
  return json.dumps({"ok": True, "prompt": text})
 
3601
  # Prefer the first substantial line (drops any stray preamble the model adds).
3602
  best = ""
3603
  for line in text.splitlines():
3604
+ cand = line.strip().strip('"\'*`').lstrip("-•0123456789. ").strip('"\'*`').strip()
3605
  if len(cand) > len(best):
3606
  best = cand
3607
+ text = " ".join((best or text).split()).strip('"\'*` ')[:500]
3608
  if not text:
3609
  return json.dumps({"ok": False, "error": "empty suggestion"})
3610
  return json.dumps({"ok": True, "prompt": text})
comic_compose.py ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """4-koma compositor for the stance comics. render_panel draws one skeleton frame
2
+ through a named shot (comic_shots) as an ink figure on dark paper; compose_koma
3
+ stacks 4 panels into a phone-proportioned vertical strip with speech bubbles,
4
+ SFX, a reserved Weiner-sticker slot per panel, and a footer stamp. Captions are
5
+ caller-supplied (templates for now, Weiner's LLM later)."""
6
+ import io
7
+ import numpy as np
8
+ import matplotlib
9
+ matplotlib.use("Agg")
10
+ import matplotlib.pyplot as plt
11
+ from matplotlib.patches import Circle
12
+ from PIL import Image, ImageDraw, ImageFont
13
+
14
+ from comic_shots import PARENTS, LEFTJ, RIGHTJ, HEAD, make_camera, project, ground_grid
15
+
16
+ INK = "#f3ead8" # warm ink on dark paper (matches the app's dark viewer)
17
+ PAPER_TOP = (44, 42, 48)
18
+ PAPER_BOT = (24, 23, 27)
19
+ LEFT_TINT = "#9fc2ff"
20
+ RIGHT_TINT = "#ffab9b"
21
+ GRID_COL = "#8a8276"
22
+ BORDER = "#0c0b0d"
23
+ PANEL_W, PANEL_H = 660, 470
24
+
25
+
26
+ def _blend(hex_a, hex_b, t):
27
+ a = np.array([int(hex_a[i:i + 2], 16) for i in (1, 3, 5)], float)
28
+ b = np.array([int(hex_b[i:i + 2], 16) for i in (1, 3, 5)], float)
29
+ c = (a * (1 - t) + b * t).astype(int)
30
+ return "#%02x%02x%02x" % tuple(c)
31
+
32
+
33
+ def render_panel(Pf, shot, W=PANEL_W, H=PANEL_H, speed_lines=False):
34
+ """One skeleton frame [22,3] -> PIL Image, framed by the named shot."""
35
+ Pf = np.asarray(Pf, np.float32)
36
+ cam = make_camera(shot, Pf)
37
+ dpi = 100
38
+ fig = plt.figure(figsize=(W / dpi, H / dpi), dpi=dpi)
39
+ ax = fig.add_axes([0, 0, 1, 1])
40
+ ax.set_xlim(0, W); ax.set_ylim(H, 0); ax.axis("off")
41
+
42
+ # paper: vertical gradient
43
+ grad = np.linspace(0, 1, 64).reshape(-1, 1)
44
+ grad = np.dstack([(PAPER_TOP[i] + (PAPER_BOT[i] - PAPER_TOP[i]) * grad) / 255.0 for i in range(3)])
45
+ ax.imshow(grad, extent=[0, W, H, 0], aspect="auto", zorder=0)
46
+
47
+ # floor grid, faded by depth
48
+ center = (Pf.min(axis=0) + Pf.max(axis=0)) / 2.0
49
+ for a, b in ground_grid(center):
50
+ (p2, z) = project(np.array([a, b]), cam, W, H)
51
+ alpha = float(np.clip(0.30 - 0.035 * z.mean(), 0.05, 0.30))
52
+ ax.plot(p2[:, 0], p2[:, 1], color=GRID_COL, lw=1.0, alpha=alpha, zorder=1)
53
+
54
+ # speed lines (gag/action): radial strokes from the panel edge toward the figure
55
+ if speed_lines:
56
+ j2, _ = project(Pf, cam, W, H)
57
+ cx, cy = j2.mean(axis=0)
58
+ rng = np.random.default_rng(7) # fixed seed: deterministic art
59
+ for ang in np.linspace(0, 2 * np.pi, 26, endpoint=False):
60
+ r0 = max(W, H) * 0.75
61
+ r1 = r0 * rng.uniform(0.55, 0.72)
62
+ x0, y0 = cx + r0 * np.cos(ang), cy + r0 * np.sin(ang)
63
+ x1, y1 = cx + r1 * np.cos(ang), cy + r1 * np.sin(ang)
64
+ ax.plot([x0, x1], [y0, y1], color=INK, lw=1.4, alpha=0.22, zorder=2)
65
+
66
+ j2, jz = project(Pf, cam, W, H)
67
+ # px-per-meter near the head, for sizing the head circle and end caps
68
+ head3 = Pf[HEAD]
69
+ probe, _ = project(np.array([head3, head3 + np.array([0, 0.12, 0])]), cam, W, H)
70
+ head_r = max(6.0, float(np.linalg.norm(probe[1] - probe[0])))
71
+
72
+ # bones far-to-near so nearer limbs overdraw
73
+ order = sorted([(j, p) for j, p in enumerate(PARENTS) if p >= 0],
74
+ key=lambda jp: -(jz[jp[0]] + jz[jp[1]]) / 2.0)
75
+ zmin, zmax = jz.min(), jz.max()
76
+ for j, p in order:
77
+ t = 0.0 if zmax - zmin < 1e-6 else float((jz[j] + jz[p]) / 2.0 - zmin) / (zmax - zmin)
78
+ col = INK
79
+ if j in LEFTJ:
80
+ col = _blend(INK, LEFT_TINT, 0.45)
81
+ elif j in RIGHTJ:
82
+ col = _blend(INK, RIGHT_TINT, 0.45)
83
+ col = _blend(col, "#3a3a40", 0.35 * t) # recede with depth
84
+ lw = 7.0 - 2.2 * t
85
+ ax.plot([j2[p, 0], j2[j, 0]], [j2[p, 1], j2[j, 1]], "-", color=BORDER,
86
+ lw=lw + 3.0, solid_capstyle="round", zorder=3)
87
+ ax.plot([j2[p, 0], j2[j, 0]], [j2[p, 1], j2[j, 1]], "-", color=col,
88
+ lw=lw, solid_capstyle="round", zorder=4)
89
+ # head
90
+ ax.add_patch(Circle((j2[HEAD, 0], j2[HEAD, 1]), head_r * 1.05, facecolor=BORDER, zorder=5))
91
+ ax.add_patch(Circle((j2[HEAD, 0], j2[HEAD, 1]), head_r * 0.85, facecolor=INK, zorder=6))
92
+ # hands / feet nubs
93
+ for j in (20, 21, 10, 11):
94
+ col = _blend(INK, LEFT_TINT if j in LEFTJ else RIGHT_TINT, 0.45)
95
+ ax.add_patch(Circle((j2[j, 0], j2[j, 1]), head_r * 0.32, facecolor=col,
96
+ edgecolor=BORDER, lw=1.5, zorder=6))
97
+
98
+ buf = io.BytesIO()
99
+ fig.savefig(buf, format="png", dpi=dpi)
100
+ plt.close(fig)
101
+ buf.seek(0)
102
+ return Image.open(buf).convert("RGB")
103
+
104
+
105
+ # ---------------------------------------------------------------- strip assembly
106
+
107
+ def _font(size, bold=False):
108
+ for name in (("comicbd.ttf",) if bold else ("comic.ttf",)) + ("arialbd.ttf" if bold else "arial.ttf",):
109
+ try:
110
+ return ImageFont.truetype("C:/Windows/Fonts/" + name, size)
111
+ except OSError:
112
+ continue
113
+ return ImageFont.load_default()
114
+
115
+
116
+ def _wrap(draw, text, font, max_w):
117
+ words, lines, cur = text.split(), [], ""
118
+ for w in words:
119
+ trial = (cur + " " + w).strip()
120
+ if draw.textlength(trial, font=font) <= max_w:
121
+ cur = trial
122
+ else:
123
+ if cur:
124
+ lines.append(cur)
125
+ cur = w
126
+ if cur:
127
+ lines.append(cur)
128
+ return lines
129
+
130
+
131
+ def _bubble(img, draw, text, anchor_xy, panel_box, speaker):
132
+ """Speech bubble in the panel's upper strip; tail points down toward the figure."""
133
+ x0, y0, x1, y1 = panel_box
134
+ font = _font(21)
135
+ pad, max_w = 12, int((x1 - x0) * 0.62)
136
+ lines = _wrap(draw, text, font, max_w - 2 * pad)
137
+ line_h = font.size + 5
138
+ bw = max(draw.textlength(l, font=font) for l in lines) + 2 * pad
139
+ bh = len(lines) * line_h + 2 * pad
140
+ on_left = speaker != "student" # weiner speaks from the left, student from the right
141
+ bx = x0 + 14 if on_left else x1 - 14 - bw
142
+ by = y0 + 12
143
+ fill = "#f6f1e4" if speaker != "student" else "#e8f0fb"
144
+ draw.rounded_rectangle([bx, by, bx + bw, by + bh], radius=13, fill=fill,
145
+ outline=BORDER, width=3)
146
+ tx = bx + bw * (0.28 if on_left else 0.72)
147
+ draw.polygon([(tx - 9, by + bh - 2), (tx + 9, by + bh - 2), (tx + (0 if on_left else 4), by + bh + 16)],
148
+ fill=fill, outline=BORDER)
149
+ for i, l in enumerate(lines):
150
+ draw.text((bx + pad, by + pad + i * line_h), l, font=font, fill="#17151a")
151
+
152
+
153
+ def _sfx(img, draw, text, panel_box):
154
+ x0, y0, x1, y1 = panel_box
155
+ font = _font(54, bold=True)
156
+ tw = draw.textlength(text, font=font)
157
+ # rotated SFX on its own layer so it can tilt like a stamp
158
+ lay = Image.new("RGBA", (int(tw) + 40, 90), (0, 0, 0, 0))
159
+ ld = ImageDraw.Draw(lay)
160
+ for dx in (-3, 3):
161
+ for dy in (-3, 3):
162
+ ld.text((20 + dx, 12 + dy), text, font=font, fill=BORDER)
163
+ ld.text((20, 12), text, font=font, fill="#ffd24a")
164
+ lay = lay.rotate(9, expand=True, resample=Image.BICUBIC)
165
+ img.paste(lay, (int(x1 - lay.width - 8), int(y1 - lay.height - 6)), lay)
166
+
167
+
168
+ def _sticker_slot(draw, panel_box, expression):
169
+ """Reserved corner box for the Karate Weiner sticker (art pending)."""
170
+ x0, y0, x1, y1 = panel_box
171
+ s = 108
172
+ bx, by = x0 + 12, y1 - 12 - s
173
+ draw.rounded_rectangle([bx, by, bx + s, by + s], radius=12, outline="#bba76f", width=2)
174
+ f = _font(40)
175
+ draw.text((bx + s / 2, by + s / 2 - 12), "🌭", font=f, anchor="mm")
176
+ draw.text((bx + s / 2, by + s - 16), expression, font=_font(15), fill="#bba76f", anchor="mm")
177
+
178
+
179
+ def compose_koma(panels, title, footer, out_w=720):
180
+ """panels: 4 dicts {img, caption, speaker, sfx?, weiner} -> one vertical strip."""
181
+ gutter, border = 14, 4
182
+ header_h, footer_h = 86, 56
183
+ pw = out_w - 2 * gutter
184
+ ph = int(pw * PANEL_H / PANEL_W)
185
+ H = header_h + 4 * (ph + gutter) + footer_h
186
+ img = Image.new("RGB", (out_w, H), "#141318")
187
+ draw = ImageDraw.Draw(img)
188
+
189
+ draw.text((out_w / 2, 30), "KARATE WEINER", font=_font(34, bold=True), fill=INK, anchor="mm")
190
+ draw.text((out_w / 2, 64), title, font=_font(19), fill="#bba76f", anchor="mm")
191
+
192
+ y = header_h
193
+ for p in panels:
194
+ pim = p["img"].resize((pw, ph), Image.LANCZOS)
195
+ img.paste(pim, (gutter, y))
196
+ box = (gutter, y, gutter + pw, y + ph)
197
+ draw.rectangle(box, outline=BORDER, width=border)
198
+ draw.rectangle([box[0] - 1, box[1] - 1, box[2] + 1, box[3] + 1], outline="#5a5345", width=1)
199
+ _sticker_slot(draw, box, p.get("weiner", ""))
200
+ if p.get("caption"):
201
+ _bubble(img, draw, p["caption"], None, box, p.get("speaker", "weiner"))
202
+ if p.get("sfx"):
203
+ _sfx(img, draw, p["sfx"], box)
204
+ y += ph + gutter
205
+
206
+ draw.text((out_w / 2, H - footer_h / 2 - 4), footer, font=_font(17), fill="#8d8676", anchor="mm")
207
+ return img
comic_sheet_v0.png ADDED

Git LFS Details

  • SHA256: f060ef75bef0590346160dfeb9f1aa942ce36d486f08c6b00f027aeef39a9791
  • Pointer size: 132 Bytes
  • Size of remote file: 1.04 MB
comic_shots.py CHANGED
@@ -13,10 +13,12 @@ RIGHTJ = {2, 5, 8, 11, 14, 17, 19, 21}
13
  # (0 = head-on). el = elevation in degrees (negative = camera low, looking up).
14
  # margin = how much air around the joint bbox. roll = dutch tilt.
15
  SHOTS = {
16
- "establishing": dict(az=38.0, el=10.0, margin=1.55, roll=0.0, fov=40.0),
17
- "action": dict(az=55.0, el=4.0, margin=1.22, roll=-4.0, fov=46.0),
18
- "gag": dict(az=22.0, el=16.0, margin=1.12, roll=13.0, fov=50.0),
19
- "hero": dict(az=32.0, el=-16.0, margin=1.42, roll=0.0, fov=38.0),
 
 
20
  }
21
 
22
 
 
13
  # (0 = head-on). el = elevation in degrees (negative = camera low, looking up).
14
  # margin = how much air around the joint bbox. roll = dutch tilt.
15
  SHOTS = {
16
+ # aim_up biases the look-at above the bbox center so the figure sits LOW in
17
+ # the frame — the upper strip belongs to the speech bubble.
18
+ "establishing": dict(az=38.0, el=10.0, margin=1.16, roll=0.0, fov=40.0, aim_up=0.22),
19
+ "action": dict(az=55.0, el=4.0, margin=1.02, roll=-4.0, fov=46.0, aim_up=0.16),
20
+ "gag": dict(az=22.0, el=16.0, margin=0.98, roll=13.0, fov=50.0, aim_up=0.10),
21
+ "hero": dict(az=32.0, el=-16.0, margin=1.04, roll=0.0, fov=38.0, aim_up=0.14),
22
  }
23
 
24