Spaces:
Running on Zero
Running on Zero
Deploy Karate Wiener (kimodo kata maker)
Browse files- .comic-cache/kmd_0c84d8afaab845cb.json +0 -0
- .comic-cache/kmd_11c32c612d4243e4.json +0 -0
- .comic-cache/kmd_1afbd86fd3fe46f6.json +0 -0
- .comic-cache/kmd_1d660f04319e4746.json +0 -0
- .comic-cache/kmd_1d6e16cc6e97440c.json +0 -0
- .comic-cache/kmd_1f952002fc964515.json +0 -0
- .comic-cache/kmd_611c1728665acc5b.json +0 -0
- .comic-cache/kmd_96a1e7e3cc6961b3.json +0 -0
- .comic-cache/kmd_9e1940e54f9a8163.json +0 -0
- .comic-cache/kmd_a768b4f3a338424c.json +0 -0
- .comic-cache/kmd_af19653214a96ac1.json +0 -0
- .comic-cache/kmd_da99023f4a484a20.json +0 -0
- .comic-cache/kmd_ea04338a30114bc2.json +0 -0
- .gitattributes +1 -0
- app.py +2 -2
- comic_compose.py +207 -0
- comic_sheet_v0.png +3 -0
- comic_shots.py +6 -4
.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('"'
|
| 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
|
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 |
-
|
| 17 |
-
|
| 18 |
-
"
|
| 19 |
-
"
|
|
|
|
|
|
|
| 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 |
|