Spaces:
Running on Zero
Running on Zero
fix: render share png through server endpoint
Browse filesCo-authored-by: Codex <noreply@openai.com>
- README.md +2 -0
- app.py +14 -1
- hackathon_advisor/png_export.py +1 -1
- hackathon_advisor/submission_packet.py +1 -1
- static/app.js +24 -164
- tests/test_app.py +13 -0
- tests/test_frontend_copy.py +10 -0
- tests/test_submission_packet.py +1 -0
README.md
CHANGED
|
@@ -143,6 +143,8 @@ the user-facing app stays centered on idea evaluation. The main `/api/bootstrap`
|
|
| 143 |
Every scored fate page now carries a deterministic `wood_map` artifact: background dots for inked Spaces, red dots for
|
| 144 |
the closest cited echoes, and a green/red "you" dot for the current idea. The live UI and PNG export render the same
|
| 145 |
map, so the share artifact visually proves whether the page sits in an empty margin or near existing work.
|
|
|
|
|
|
|
| 146 |
|
| 147 |
## Latency Watchdog
|
| 148 |
|
|
|
|
| 143 |
Every scored fate page now carries a deterministic `wood_map` artifact: background dots for inked Spaces, red dots for
|
| 144 |
the closest cited echoes, and a green/red "you" dot for the current idea. The live UI and PNG export render the same
|
| 145 |
map, so the share artifact visually proves whether the page sits in an empty margin or near existing work.
|
| 146 |
+
The `PNG` button posts the current artifact to `/api/artifact.png`, which uses the same Pillow renderer as
|
| 147 |
+
`/api/demo-bundle.zip`, so browser downloads and bundled evidence cannot drift into different layouts.
|
| 148 |
|
| 149 |
## Latency Watchdog
|
| 150 |
|
app.py
CHANGED
|
@@ -3,8 +3,9 @@ from __future__ import annotations
|
|
| 3 |
import json
|
| 4 |
import os
|
| 5 |
from pathlib import Path
|
| 6 |
-
from typing import Iterator
|
| 7 |
|
|
|
|
| 8 |
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, Response
|
| 9 |
from gradio import Server
|
| 10 |
|
|
@@ -16,6 +17,7 @@ from hackathon_advisor.demo_rehearsal import build_demo_rehearsal
|
|
| 16 |
from hackathon_advisor.field_notes import build_field_notes_markdown
|
| 17 |
from hackathon_advisor.lora_dataset import build_lora_dataset_jsonl
|
| 18 |
from hackathon_advisor.lora_training_kit import TRAINING_KIT_FILENAME, build_lora_training_kit_zip
|
|
|
|
| 19 |
from hackathon_advisor.prize_ledger import prize_ledger
|
| 20 |
from hackathon_advisor.submission_packet import build_submission_packet_markdown
|
| 21 |
from hackathon_advisor.tool_contracts import resolve_tool_call, tool_schemas
|
|
@@ -116,6 +118,17 @@ def demo_bundle() -> Response:
|
|
| 116 |
)
|
| 117 |
|
| 118 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
@app.get("/api/lora-training-kit.zip")
|
| 120 |
def lora_training_kit() -> Response:
|
| 121 |
runtime_status = engine.runtime_status()
|
|
|
|
| 3 |
import json
|
| 4 |
import os
|
| 5 |
from pathlib import Path
|
| 6 |
+
from typing import Any, Iterator
|
| 7 |
|
| 8 |
+
from fastapi import Body
|
| 9 |
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, Response
|
| 10 |
from gradio import Server
|
| 11 |
|
|
|
|
| 17 |
from hackathon_advisor.field_notes import build_field_notes_markdown
|
| 18 |
from hackathon_advisor.lora_dataset import build_lora_dataset_jsonl
|
| 19 |
from hackathon_advisor.lora_training_kit import TRAINING_KIT_FILENAME, build_lora_training_kit_zip
|
| 20 |
+
from hackathon_advisor.png_export import artifact_png_filename, render_artifact_png
|
| 21 |
from hackathon_advisor.prize_ledger import prize_ledger
|
| 22 |
from hackathon_advisor.submission_packet import build_submission_packet_markdown
|
| 23 |
from hackathon_advisor.tool_contracts import resolve_tool_call, tool_schemas
|
|
|
|
| 118 |
)
|
| 119 |
|
| 120 |
|
| 121 |
+
@app.post("/api/artifact.png")
|
| 122 |
+
def artifact_png(artifact: dict[str, Any] | None = Body(default=None)) -> Response:
|
| 123 |
+
artifact = artifact or {}
|
| 124 |
+
filename = artifact_png_filename(artifact)
|
| 125 |
+
return Response(
|
| 126 |
+
content=render_artifact_png(artifact),
|
| 127 |
+
media_type="image/png",
|
| 128 |
+
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
@app.get("/api/lora-training-kit.zip")
|
| 133 |
def lora_training_kit() -> Response:
|
| 134 |
runtime_status = engine.runtime_status()
|
hackathon_advisor/png_export.py
CHANGED
|
@@ -148,7 +148,7 @@ def _draw_wood_map(
|
|
| 148 |
outline=(80, 47, 22),
|
| 149 |
width=2,
|
| 150 |
)
|
| 151 |
-
draw.text((x, y - 26), "
|
| 152 |
|
| 153 |
for dot in dots:
|
| 154 |
if not isinstance(dot, dict):
|
|
|
|
| 148 |
outline=(80, 47, 22),
|
| 149 |
width=2,
|
| 150 |
)
|
| 151 |
+
draw.text((x, y - 26), "IDEA MAP", font=label_font, fill=INK_SOFT)
|
| 152 |
|
| 153 |
for dot in dots:
|
| 154 |
if not isinstance(dot, dict):
|
hackathon_advisor/submission_packet.py
CHANGED
|
@@ -102,7 +102,7 @@ def _artifact_checklist(trace: list[dict[str, Any]], ideas: list[dict[str, Any]]
|
|
| 102 |
("Almanac chapter markdown", has_ideas, "chapter"),
|
| 103 |
("MiniCPM5 LoRA SFT JSONL", has_trace, "lora_dataset"),
|
| 104 |
("Submission packet markdown", True, "submission_packet"),
|
| 105 |
-
("Fate page PNG", has_artifact, "
|
| 106 |
]
|
| 107 |
lines = ["## Artifact Checklist", "", "| Artifact | Status | Source |", "| --- | --- | --- |"]
|
| 108 |
for name, ready, source in rows:
|
|
|
|
| 102 |
("Almanac chapter markdown", has_ideas, "chapter"),
|
| 103 |
("MiniCPM5 LoRA SFT JSONL", has_trace, "lora_dataset"),
|
| 104 |
("Submission packet markdown", True, "submission_packet"),
|
| 105 |
+
("Fate page PNG", has_artifact, "/api/artifact.png"),
|
| 106 |
]
|
| 107 |
lines = ["## Artifact Checklist", "", "| Artifact | Status | Source |", "| --- | --- | --- |"]
|
| 108 |
for name, ready, source in rows:
|
static/app.js
CHANGED
|
@@ -1036,86 +1036,45 @@ async function exportMarkdown({ endpoint, filename, button, busyLabel, pendingLa
|
|
| 1036 |
}
|
| 1037 |
}
|
| 1038 |
|
| 1039 |
-
function exportArtifact(artifact) {
|
| 1040 |
const idleLabel = actionButtonLabel(exportButton);
|
|
|
|
| 1041 |
exportButton.disabled = true;
|
| 1042 |
setActionButtonLabel(exportButton, "PNG...");
|
| 1043 |
session.ui_status = "Drawing PNG.";
|
| 1044 |
corrections.textContent = session.ui_status;
|
| 1045 |
saveSession();
|
| 1046 |
try {
|
| 1047 |
-
const
|
| 1048 |
-
|
| 1049 |
-
|
| 1050 |
-
|
| 1051 |
-
|
| 1052 |
-
|
| 1053 |
-
|
| 1054 |
-
|
|
|
|
|
|
|
|
|
|
| 1055 |
session.ui_status = `PNG saved: ${filename}`;
|
| 1056 |
corrections.textContent = session.ui_status;
|
| 1057 |
} catch (error) {
|
|
|
|
| 1058 |
session.ui_status = `Export failed: ${error.message}`;
|
| 1059 |
corrections.textContent = session.ui_status;
|
| 1060 |
} finally {
|
| 1061 |
-
saveSession();
|
| 1062 |
setActionButtonLabel(exportButton, idleLabel || PNG_EXPORT_LABEL);
|
| 1063 |
setCommandDisabled(false);
|
| 1064 |
}
|
| 1065 |
}
|
| 1066 |
|
| 1067 |
-
function
|
| 1068 |
-
const
|
| 1069 |
-
|
| 1070 |
-
|
| 1071 |
-
|
| 1072 |
-
|
| 1073 |
-
drawParchment(ctx, canvas.width, canvas.height);
|
| 1074 |
-
const seal = artifact.seal || {};
|
| 1075 |
-
ctx.fillStyle = "#25160e";
|
| 1076 |
-
ctx.font = "700 58px Georgia, serif";
|
| 1077 |
-
wrapText(ctx, artifact.title, 78, 112, 760, 66);
|
| 1078 |
-
ctx.font = "28px Georgia, serif";
|
| 1079 |
-
ctx.fillStyle = "#6b4e35";
|
| 1080 |
-
wrapText(ctx, artifact.caption || "", 82, 252, 720, 36);
|
| 1081 |
-
drawCitationList(ctx, seal.echoes || [], 742, 330, 330);
|
| 1082 |
-
|
| 1083 |
-
ctx.save();
|
| 1084 |
-
ctx.translate(930, 226);
|
| 1085 |
-
ctx.rotate(-0.08);
|
| 1086 |
-
ctx.fillStyle = artifact.verdict?.startsWith("UNWRITTEN") ? "#b68a12" : "#8d2d26";
|
| 1087 |
-
ctx.beginPath();
|
| 1088 |
-
ctx.arc(0, 0, 120, 0, Math.PI * 2);
|
| 1089 |
-
ctx.fill();
|
| 1090 |
-
ctx.fillStyle = "#fff0b5";
|
| 1091 |
-
ctx.textAlign = "center";
|
| 1092 |
-
ctx.font = "800 27px Inter, sans-serif";
|
| 1093 |
-
wrapText(ctx, artifact.verdict || "UNWRITTEN", -92, -28, 184, 32, "center");
|
| 1094 |
-
ctx.font = "700 58px Georgia, serif";
|
| 1095 |
-
ctx.fillText(Number(artifact.overall || seal.overall || 0).toFixed(1), 0, 48);
|
| 1096 |
-
ctx.restore();
|
| 1097 |
-
|
| 1098 |
-
const rows = [
|
| 1099 |
-
["Originality", seal.originality || 0],
|
| 1100 |
-
["Delight", seal.delight || 0],
|
| 1101 |
-
["AI Need", seal.ai_necessity || 0],
|
| 1102 |
-
["Feasible", seal.feasibility || 0],
|
| 1103 |
-
["Goal Fit", seal.goal_fit || 0],
|
| 1104 |
-
];
|
| 1105 |
-
rows.forEach(([label, value], index) => {
|
| 1106 |
-
const y = 418 + index * 34;
|
| 1107 |
-
ctx.fillStyle = "#6b4e35";
|
| 1108 |
-
ctx.font = "700 20px Inter, sans-serif";
|
| 1109 |
-
ctx.fillText(label, 82, y);
|
| 1110 |
-
ctx.fillStyle = "rgba(80, 47, 22, 0.22)";
|
| 1111 |
-
ctx.fillRect(240, y - 17, 320, 16);
|
| 1112 |
-
ctx.fillStyle = artifact.verdict?.startsWith("UNWRITTEN") ? "#2f7a49" : "#8d2d26";
|
| 1113 |
-
ctx.fillRect(240, y - 17, 32 * Number(value), 16);
|
| 1114 |
-
ctx.fillStyle = "#25160e";
|
| 1115 |
-
ctx.fillText(String(value), 582, y);
|
| 1116 |
-
});
|
| 1117 |
-
drawWoodMap(ctx, artifact.wood_map, 742, 396, 330, 184, artifact.verdict);
|
| 1118 |
-
return canvas;
|
| 1119 |
}
|
| 1120 |
|
| 1121 |
function downloadText(filename, text, type = "application/jsonl;charset=utf-8") {
|
|
@@ -1127,108 +1086,9 @@ function downloadText(filename, text, type = "application/jsonl;charset=utf-8")
|
|
| 1127 |
setTimeout(() => URL.revokeObjectURL(link.href), 0);
|
| 1128 |
}
|
| 1129 |
|
| 1130 |
-
function
|
| 1131 |
-
const
|
| 1132 |
-
|
| 1133 |
-
gradient.addColorStop(0.55, "#d4b476");
|
| 1134 |
-
gradient.addColorStop(1, "#b98a4c");
|
| 1135 |
-
ctx.fillStyle = gradient;
|
| 1136 |
-
ctx.fillRect(0, 0, width, height);
|
| 1137 |
-
ctx.fillStyle = "rgba(59, 33, 15, 0.16)";
|
| 1138 |
-
for (let i = 0; i < 360; i += 1) {
|
| 1139 |
-
const x = (i * 73) % width;
|
| 1140 |
-
const y = (i * 37) % height;
|
| 1141 |
-
ctx.fillRect(x, y, 2 + (i % 7), 1);
|
| 1142 |
-
}
|
| 1143 |
-
ctx.strokeStyle = "rgba(72, 39, 18, 0.42)";
|
| 1144 |
-
ctx.lineWidth = 16;
|
| 1145 |
-
ctx.strokeRect(28, 28, width - 56, height - 56);
|
| 1146 |
-
}
|
| 1147 |
-
|
| 1148 |
-
function drawWoodMap(ctx, map, x, y, width, height, verdict) {
|
| 1149 |
-
if (!map?.dots?.length) return;
|
| 1150 |
-
ctx.save();
|
| 1151 |
-
ctx.fillStyle = "rgba(255, 241, 196, 0.38)";
|
| 1152 |
-
ctx.strokeStyle = "rgba(80, 47, 22, 0.34)";
|
| 1153 |
-
ctx.lineWidth = 2;
|
| 1154 |
-
ctx.beginPath();
|
| 1155 |
-
ctx.roundRect(x, y, width, height, 8);
|
| 1156 |
-
ctx.fill();
|
| 1157 |
-
ctx.stroke();
|
| 1158 |
-
|
| 1159 |
-
ctx.fillStyle = "#6b4e35";
|
| 1160 |
-
ctx.font = "800 18px Inter, sans-serif";
|
| 1161 |
-
ctx.fillText("IDEA MAP", x, y - 14);
|
| 1162 |
-
|
| 1163 |
-
for (const dot of map.dots) {
|
| 1164 |
-
const px = x + (width * boundedPercent(dot.x)) / 100;
|
| 1165 |
-
const py = y + (height * boundedPercent(dot.y)) / 100;
|
| 1166 |
-
const radius = Math.max(3, Math.min(10, Number(dot.radius || 4)));
|
| 1167 |
-
if (dot.kind === "idea") {
|
| 1168 |
-
ctx.fillStyle = verdict?.startsWith("UNWRITTEN") ? "#2f7a49" : "#8d2d26";
|
| 1169 |
-
ctx.strokeStyle = "#fff0b5";
|
| 1170 |
-
ctx.lineWidth = 3;
|
| 1171 |
-
} else if (dot.kind === "echo") {
|
| 1172 |
-
ctx.fillStyle = "#8d2d26";
|
| 1173 |
-
ctx.strokeStyle = "rgba(255, 240, 181, 0.72)";
|
| 1174 |
-
ctx.lineWidth = 1.5;
|
| 1175 |
-
} else {
|
| 1176 |
-
ctx.fillStyle = "rgba(80, 47, 22, 0.34)";
|
| 1177 |
-
ctx.strokeStyle = "transparent";
|
| 1178 |
-
ctx.lineWidth = 0;
|
| 1179 |
-
}
|
| 1180 |
-
ctx.beginPath();
|
| 1181 |
-
ctx.arc(px, py, radius, 0, Math.PI * 2);
|
| 1182 |
-
ctx.fill();
|
| 1183 |
-
if (ctx.lineWidth) ctx.stroke();
|
| 1184 |
-
}
|
| 1185 |
-
|
| 1186 |
-
ctx.fillStyle = "#6b4e35";
|
| 1187 |
-
ctx.font = "700 15px Inter, sans-serif";
|
| 1188 |
-
wrapText(ctx, map.caption || "", x, y + height + 24, width, 20);
|
| 1189 |
-
ctx.restore();
|
| 1190 |
-
}
|
| 1191 |
-
|
| 1192 |
-
function drawCitationList(ctx, echoes, x, y, maxWidth) {
|
| 1193 |
-
if (!echoes.length) return;
|
| 1194 |
-
ctx.save();
|
| 1195 |
-
ctx.fillStyle = "#6b4e35";
|
| 1196 |
-
ctx.font = "800 18px Inter, sans-serif";
|
| 1197 |
-
ctx.fillText("CLOSEST PAGES", x, y);
|
| 1198 |
-
ctx.font = "700 15px Inter, sans-serif";
|
| 1199 |
-
echoes.slice(0, 3).forEach((echo, index) => {
|
| 1200 |
-
const project = echo.project || {};
|
| 1201 |
-
const label = `Page ${echo.page_number || "?"}: ${project.title || project.id || "Untitled"}`;
|
| 1202 |
-
wrapText(ctx, label, x, y + 24 + index * 26, maxWidth, 18);
|
| 1203 |
-
});
|
| 1204 |
-
ctx.restore();
|
| 1205 |
-
}
|
| 1206 |
-
|
| 1207 |
-
function wrapText(ctx, text, x, y, maxWidth, lineHeight, align = "left") {
|
| 1208 |
-
const words = String(text).split(/\s+/);
|
| 1209 |
-
let line = "";
|
| 1210 |
-
const originalAlign = ctx.textAlign;
|
| 1211 |
-
ctx.textAlign = align;
|
| 1212 |
-
for (const word of words) {
|
| 1213 |
-
const next = line ? `${line} ${word}` : word;
|
| 1214 |
-
if (ctx.measureText(next).width > maxWidth && line) {
|
| 1215 |
-
ctx.fillText(line, align === "center" ? x + maxWidth / 2 : x, y);
|
| 1216 |
-
line = word;
|
| 1217 |
-
y += lineHeight;
|
| 1218 |
-
} else {
|
| 1219 |
-
line = next;
|
| 1220 |
-
}
|
| 1221 |
-
}
|
| 1222 |
-
if (line) ctx.fillText(line, align === "center" ? x + maxWidth / 2 : x, y);
|
| 1223 |
-
ctx.textAlign = originalAlign;
|
| 1224 |
-
}
|
| 1225 |
-
|
| 1226 |
-
function slugify(value) {
|
| 1227 |
-
return String(value)
|
| 1228 |
-
.toLowerCase()
|
| 1229 |
-
.replace(/[^a-z0-9]+/g, "-")
|
| 1230 |
-
.replace(/^-|-$/g, "")
|
| 1231 |
-
.slice(0, 60);
|
| 1232 |
}
|
| 1233 |
|
| 1234 |
function escapeHtml(value) {
|
|
|
|
| 1036 |
}
|
| 1037 |
}
|
| 1038 |
|
| 1039 |
+
async function exportArtifact(artifact) {
|
| 1040 |
const idleLabel = actionButtonLabel(exportButton);
|
| 1041 |
+
const revision = sessionRevision;
|
| 1042 |
exportButton.disabled = true;
|
| 1043 |
setActionButtonLabel(exportButton, "PNG...");
|
| 1044 |
session.ui_status = "Drawing PNG.";
|
| 1045 |
corrections.textContent = session.ui_status;
|
| 1046 |
saveSession();
|
| 1047 |
try {
|
| 1048 |
+
const response = await fetch("/api/artifact.png", {
|
| 1049 |
+
method: "POST",
|
| 1050 |
+
headers: { "Content-Type": "application/json" },
|
| 1051 |
+
body: JSON.stringify(artifact),
|
| 1052 |
+
});
|
| 1053 |
+
if (!response.ok) throw new Error(`PNG rendering failed with ${response.status}`);
|
| 1054 |
+
const blob = await response.blob();
|
| 1055 |
+
if (!blob.size || !blob.type.includes("png")) throw new Error("PNG rendering failed");
|
| 1056 |
+
if (!isCurrentSessionRevision(revision)) return;
|
| 1057 |
+
const filename = filenameFromContentDisposition(response.headers.get("content-disposition")) || "unwritten-page.png";
|
| 1058 |
+
downloadBlob(filename, blob);
|
| 1059 |
session.ui_status = `PNG saved: ${filename}`;
|
| 1060 |
corrections.textContent = session.ui_status;
|
| 1061 |
} catch (error) {
|
| 1062 |
+
if (!isCurrentSessionRevision(revision)) return;
|
| 1063 |
session.ui_status = `Export failed: ${error.message}`;
|
| 1064 |
corrections.textContent = session.ui_status;
|
| 1065 |
} finally {
|
| 1066 |
+
if (isCurrentSessionRevision(revision)) saveSession();
|
| 1067 |
setActionButtonLabel(exportButton, idleLabel || PNG_EXPORT_LABEL);
|
| 1068 |
setCommandDisabled(false);
|
| 1069 |
}
|
| 1070 |
}
|
| 1071 |
|
| 1072 |
+
function downloadBlob(filename, blob) {
|
| 1073 |
+
const link = document.createElement("a");
|
| 1074 |
+
link.download = filename;
|
| 1075 |
+
link.href = URL.createObjectURL(blob);
|
| 1076 |
+
link.click();
|
| 1077 |
+
setTimeout(() => URL.revokeObjectURL(link.href), 0);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1078 |
}
|
| 1079 |
|
| 1080 |
function downloadText(filename, text, type = "application/jsonl;charset=utf-8") {
|
|
|
|
| 1086 |
setTimeout(() => URL.revokeObjectURL(link.href), 0);
|
| 1087 |
}
|
| 1088 |
|
| 1089 |
+
function filenameFromContentDisposition(value) {
|
| 1090 |
+
const match = String(value || "").match(/filename="([^"]+)"/i);
|
| 1091 |
+
return match ? match[1] : "";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1092 |
}
|
| 1093 |
|
| 1094 |
function escapeHtml(value) {
|
tests/test_app.py
CHANGED
|
@@ -3,6 +3,7 @@ from io import BytesIO
|
|
| 3 |
from zipfile import ZipFile
|
| 4 |
|
| 5 |
from app import (
|
|
|
|
| 6 |
bootstrap,
|
| 7 |
chapter_artifact,
|
| 8 |
demo_bundle,
|
|
@@ -156,6 +157,18 @@ def test_demo_bundle_endpoint_returns_zip_attachment() -> None:
|
|
| 156 |
assert manifest["turn_count"] == 2
|
| 157 |
|
| 158 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
def test_lora_training_kit_endpoint_returns_zip_attachment() -> None:
|
| 160 |
response = lora_training_kit()
|
| 161 |
|
|
|
|
| 3 |
from zipfile import ZipFile
|
| 4 |
|
| 5 |
from app import (
|
| 6 |
+
artifact_png,
|
| 7 |
bootstrap,
|
| 8 |
chapter_artifact,
|
| 9 |
demo_bundle,
|
|
|
|
| 157 |
assert manifest["turn_count"] == 2
|
| 158 |
|
| 159 |
|
| 160 |
+
def test_artifact_png_endpoint_returns_png_attachment() -> None:
|
| 161 |
+
state = engine.turn("A local-first archive cartographer for family photos", {}).state
|
| 162 |
+
response = artifact_png(state["last_artifact"])
|
| 163 |
+
|
| 164 |
+
assert response.media_type == "image/png"
|
| 165 |
+
assert 'filename="a-local-first-archive-cartographer-for-family-photos.png"' in response.headers[
|
| 166 |
+
"content-disposition"
|
| 167 |
+
]
|
| 168 |
+
assert response.body.startswith(b"\x89PNG\r\n\x1a\n")
|
| 169 |
+
assert len(response.body) > 10_000
|
| 170 |
+
|
| 171 |
+
|
| 172 |
def test_lora_training_kit_endpoint_returns_zip_attachment() -> None:
|
| 173 |
response = lora_training_kit()
|
| 174 |
|
tests/test_frontend_copy.py
CHANGED
|
@@ -10,6 +10,9 @@ def test_main_interface_copy_is_builder_facing() -> None:
|
|
| 10 |
assert "Closest project echoes" in html
|
| 11 |
assert "Press Plan to draft build steps for the selected idea." in app_js
|
| 12 |
assert "Loading an example idea board." in app_js
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
stale_jargon = [
|
| 15 |
"No wax path pressed.",
|
|
@@ -40,3 +43,10 @@ def test_visible_static_shell_does_not_promote_submission_evidence() -> None:
|
|
| 40 |
]
|
| 41 |
for term in promotional_terms:
|
| 42 |
assert term not in html
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
assert "Closest project echoes" in html
|
| 11 |
assert "Press Plan to draft build steps for the selected idea." in app_js
|
| 12 |
assert "Loading an example idea board." in app_js
|
| 13 |
+
assert "/api/artifact.png" in app_js
|
| 14 |
+
assert "renderArtifactCanvas" not in app_js
|
| 15 |
+
assert "canvas.toDataURL" not in app_js
|
| 16 |
|
| 17 |
stale_jargon = [
|
| 18 |
"No wax path pressed.",
|
|
|
|
| 43 |
]
|
| 44 |
for term in promotional_terms:
|
| 45 |
assert term not in html
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def test_server_png_copy_uses_interface_language() -> None:
|
| 49 |
+
source = Path("hackathon_advisor/png_export.py").read_text(encoding="utf-8")
|
| 50 |
+
|
| 51 |
+
assert "IDEA MAP" in source
|
| 52 |
+
assert "YOU VS THE WOOD" not in source
|
tests/test_submission_packet.py
CHANGED
|
@@ -32,6 +32,7 @@ def test_submission_packet_contains_demo_and_prize_evidence() -> None:
|
|
| 32 |
assert "Hackathon Advisor" in markdown
|
| 33 |
assert "Well-Tuned | training-kit-ready" in markdown
|
| 34 |
assert "MiniCPM5 LoRA SFT JSONL | ready | lora_dataset" in markdown
|
|
|
|
| 35 |
assert "Notes, Chapter, and PNG are available in the app" in markdown
|
| 36 |
assert "`/api/prize-ledger` separates ready and planned badge states" in markdown
|
| 37 |
assert "A local-first archive cartographer for family photos" in markdown
|
|
|
|
| 32 |
assert "Hackathon Advisor" in markdown
|
| 33 |
assert "Well-Tuned | training-kit-ready" in markdown
|
| 34 |
assert "MiniCPM5 LoRA SFT JSONL | ready | lora_dataset" in markdown
|
| 35 |
+
assert "Fate page PNG | ready | /api/artifact.png" in markdown
|
| 36 |
assert "Notes, Chapter, and PNG are available in the app" in markdown
|
| 37 |
assert "`/api/prize-ledger` separates ready and planned badge states" in markdown
|
| 38 |
assert "A local-first archive cartographer for family photos" in markdown
|