Spaces:
Running
Running
File size: 9,287 Bytes
2ac71c7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 | # SPDX-License-Identifier: BSD-3-Clause
"""Read a model's reply as sketch source and take inventory of it.
Everything here is textual and cheap. It runs before the browser does, so a
submission that was never going to paint anything is rejected without paying
for a render, and the shortcuts a model reaches for are visible in the source
long before they are visible in the picture.
The two API lists come from the export table of the vendored p5.brush bundle
rather than from the docs, so they describe the build that will actually run.
"""
from __future__ import annotations
import re
from dataclasses import asdict, dataclass, field
from typing import Any
# Calls that put paint on the canvas. A sketch that never makes one of these
# cannot have painted anything, whatever else it did.
PAINTING_CALLS = frozenset(
{
"arc",
"box",
"circle",
"endShape",
"endStroke",
"flowLine",
"hatch",
"hatchArray",
"line",
"polygon",
"rect",
"spline",
"wash",
}
)
# The rest of the public surface: state, configuration and geometry helpers.
SUPPORTING_CALLS = frozenset(
{
"add",
"addField",
"beginShape",
"beginStroke",
"clip",
"field",
"fill",
"fillBleed",
"fillTexture",
"hatchStyle",
"instance",
"listFields",
"load",
"mass",
"massArray",
"move",
"noClip",
"noField",
"noFill",
"noHatch",
"noMass",
"noStroke",
"noWash",
"noiseSeed",
"pick",
"refreshField",
"scaleBrushes",
"seed",
"set",
"stroke",
"strokeWeight",
"vertex",
"wRand",
"wiggle",
}
)
KNOWN_CALLS = PAINTING_CALLS | SUPPORTING_CALLS
# Bare p5 drawing primitives. Using these is how a model paints without
# p5.brush, which produces a picture that scores on composition while dodging
# the medium entirely. Matched only when not preceded by a dot, so
# `brush.rect(...)` does not trip the check on `rect`.
def _strip_comments(source: str) -> str:
"""Return `source` with JavaScript comments blanked out.
Crude on purpose: a `//` inside a string literal would be removed too. The
sketches use no URLs and their only strings are hex colours, so the cost is
nil against the alternative of parsing JavaScript to read a source inventory.
"""
source = re.sub(r"/\*.*?\*/", " ", source, flags=re.S)
return re.sub(r"//[^\n]*", " ", source)
BARE_PRIMITIVES = (
"arc",
"beginShape",
"bezier",
"circle",
"curve",
"ellipse",
"line",
"point",
"quad",
"rect",
"square",
"triangle",
"vertex",
)
# Ways a sketch could put someone else's picture on the canvas, or reach off
# the machine at all. The submission is untrusted code with a network stack
# behind it, so this is a security boundary as much as an anti-cheat one.
EXTERNAL_ACCESS = (
"loadImage",
"loadBytes",
"loadJSON",
"loadStrings",
"loadTable",
"loadXML",
"loadModel",
"loadFont",
"loadShader",
"createImg",
"createVideo",
"createCapture",
"fetch",
"XMLHttpRequest",
"WebSocket",
"EventSource",
"importScripts",
"createElement",
"innerHTML",
"document.write",
"eval",
"Function(",
)
_FENCE = re.compile(r"```(?:js|javascript)?\s*(.*?)```", re.DOTALL)
# A definition of `setup` or `draw`, not a mention of one. Testing for the bare
# words matches a refusal like "Sorry, I cannot draw images", which then gets
# rejected for the wrong reason two checks later.
_ENTRY_POINT = re.compile(
r"\b(?:function\s+(?:setup|draw)\s*\(|(?:setup|draw)\s*=\s*(?:function|\())"
)
_CALL = re.compile(r"\bbrush\s*\.\s*([A-Za-z_$][\w$]*)")
_CREATE_CANVAS = re.compile(r"\bcreateCanvas\s*\(([^)]*)\)")
_TEXT_CALL = re.compile(r"(^|[^.\w])text\s*\(")
_DATA_URI = re.compile(r"data\s*:\s*image/", re.IGNORECASE)
class SourceError(Exception):
"""Raised when the reply holds nothing that could be a sketch."""
@dataclass(frozen=True)
class SourceReport:
"""What the source says about itself.
Attributes:
source (`str`):
The extracted JavaScript.
painting_calls (`list[str]`):
Distinct `brush.*` calls used that put paint down.
supporting_calls (`list[str]`):
Distinct `brush.*` calls used that configure rather than paint.
unknown_calls (`list[str]`):
`brush.*` calls that do not exist in the vendored build. These are
the sketch's own invention and each one throws at runtime.
bare_primitives (`list[str]`):
Bare p5 drawing primitives used, which is drawing without the
library.
external_access (`list[str]`):
Names suggesting the sketch tried to load or reach something.
has_setup (`bool`):
Whether a `setup` function is defined.
has_draw (`bool`):
Whether a `draw` function is defined.
webgl (`bool`):
Whether `createCanvas` asked for a WEBGL context, which p5.brush
requires.
calls_no_loop (`bool`):
Whether the sketch stops itself. Sketches that do not are scored on
whatever they painted before the render deadline.
writes_text (`bool`):
Whether the sketch calls `text`, which is how a drawing task gets
answered in words instead of paint.
balanced (`bool`):
Whether braces balance. Unbalanced means the reply was cut off.
"""
source: str
painting_calls: list[str] = field(default_factory=list)
supporting_calls: list[str] = field(default_factory=list)
unknown_calls: list[str] = field(default_factory=list)
bare_primitives: list[str] = field(default_factory=list)
external_access: list[str] = field(default_factory=list)
has_setup: bool = False
has_draw: bool = False
webgl: bool = False
calls_no_loop: bool = False
writes_text: bool = False
balanced: bool = True
def to_dict(self) -> dict[str, Any]:
"""Return a JSON-serialisable view, without the source itself."""
data = asdict(self)
data.pop("source")
return data
def extract_sketch(response: str) -> str:
"""Pull JavaScript out of a model reply.
Args:
response (`str`):
The raw reply, with or without a fenced code block.
Returns:
`str`: The sketch source.
Raises:
SourceError: If nothing in the reply defines `setup` or `draw`. A reply
that only mentions them, such as a refusal to draw, counts as
nothing.
Examples:
```python
source = extract_sketch("```js\\nfunction setup(){}\\n```")
```
"""
match = _FENCE.search(response)
source = (match.group(1) if match else response).strip()
if not _ENTRY_POINT.search(source):
raise SourceError("no sketch in response")
return source
def inspect_source(source: str) -> SourceReport:
"""Take inventory of a sketch without running it.
Args:
source (`str`):
The sketch source.
Returns:
[`SourceReport`]: The inventory.
Examples:
```python
report = inspect_source(extract_sketch(reply))
print(report.painting_calls, report.bare_primitives)
```
"""
# Every check below reads code, so comments come out first. They were not
# stripped, and the primitive check allows whitespace before the paren, so
# ordinary English prose in a comment counted as a call: `// the center point
# (300, 300)` matched `point (`, and `// Outer curve (bulging out)` matched
# `curve (`. Measured on the twelve-sample probe of a 35B, that rejected four
# rollouts out of twelve, a third of them, purely for being commented. The
# bigger the model the more it comments, so the penalty grew with capability.
# All four pass once comments are gone, and none of the eight that passed
# starts failing.
code = _strip_comments(source)
used = set(_CALL.findall(code))
canvas = _CREATE_CANVAS.search(code)
bare = [
name
for name in BARE_PRIMITIVES
if re.search(rf"(^|[^.\w]){name}\s*\(", code, re.MULTILINE)
]
external = [name for name in EXTERNAL_ACCESS if name in code]
if _DATA_URI.search(code):
external.append("data_uri_image")
return SourceReport(
source=source,
painting_calls=sorted(used & PAINTING_CALLS),
supporting_calls=sorted(used & SUPPORTING_CALLS),
unknown_calls=sorted(used - KNOWN_CALLS),
bare_primitives=bare,
external_access=external,
has_setup=bool(
re.search(r"function\s+setup\s*\(|setup\s*=\s*(?:function|\()", source)
),
has_draw=bool(
re.search(r"function\s+draw\s*\(|draw\s*=\s*(?:function|\()", source)
),
webgl=bool(canvas and "WEBGL" in canvas.group(1)),
calls_no_loop="noLoop" in source,
writes_text=bool(_TEXT_CALL.search(source)),
balanced=source.count("{") == source.count("}"),
)
|