overlay-factory-worker 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +215 -0
- package/package.json +64 -0
- package/public/fonts/Handjet-variable.woff2 +0 -0
- package/remotion.config.ts +14 -0
- package/scripts/check-legibility.ts +284 -0
- package/scripts/check-safe-area.ts +148 -0
- package/scripts/custom-fonts.ts +114 -0
- package/scripts/export.sh +31 -0
- package/scripts/field-images.ts +93 -0
- package/scripts/ig-probe.ts +83 -0
- package/scripts/ig-sync.ts +204 -0
- package/scripts/ingest.sh +34 -0
- package/scripts/library.ts +143 -0
- package/scripts/look-card.ts +107 -0
- package/scripts/look-store.ts +176 -0
- package/scripts/make-card.ts +237 -0
- package/scripts/merge-index.ts +74 -0
- package/scripts/new-episode.ts +149 -0
- package/scripts/overlay-worker.ts +1119 -0
- package/scripts/place-overlay.ts +359 -0
- package/scripts/prep-card.ts +73 -0
- package/scripts/quality.ts +0 -0
- package/scripts/render-overlay.ts +150 -0
- package/scripts/report.ts +127 -0
- package/scripts/rerender-cards.ts +116 -0
- package/scripts/series.ts +816 -0
- package/scripts/set-difficulty.ts +62 -0
- package/scripts/state-dir.ts +102 -0
- package/scripts/stock.ts +254 -0
- package/scripts/verify.ts +149 -0
- package/scripts/wp-restock.ts +281 -0
- package/src/Root.tsx +112 -0
- package/src/index.css +1 -0
- package/src/index.ts +4 -0
- package/src/lab/FontLab.tsx +50 -0
- package/src/lab/FontSheet.tsx +188 -0
- package/src/lab/PillLab.tsx +121 -0
- package/src/overlay/Composition.tsx +297 -0
- package/src/overlay/DifficultyMeter.tsx +86 -0
- package/src/overlay/PixelText.tsx +134 -0
- package/src/overlay/Title.tsx +75 -0
- package/src/overlay/brandFonts.ts +58 -0
- package/src/overlay/cardLayout.ts +94 -0
- package/src/overlay/fonts.ts +19 -0
- package/src/overlay/look.ts +155 -0
- package/src/overlay/safeArea.ts +89 -0
- package/src/overlay/types.ts +134 -0
- package/src/series/what-prints/CodeCard.tsx +107 -0
- package/src/series/what-prints/Composition.tsx +106 -0
- package/src/series/what-prints/codeCardTypes.ts +105 -0
- package/src/series/what-prints/types.ts +22 -0
- package/tsconfig.json +17 -0
- package/worker/cli.mjs +151 -0
- package/worker/service.mjs +404 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the What Prints episodes actually did, and what to change next.
|
|
3
|
+
*
|
|
4
|
+
* npx tsx scripts/ig-sync.ts && npx tsx scripts/report.ts
|
|
5
|
+
*
|
|
6
|
+
* Ranks published episodes by comments per 1,000 reach against the account's
|
|
7
|
+
* own baseline. Likes are shown but not ranked on: reach varies by orders of
|
|
8
|
+
* magnitude and likes follow reach, so a like count says more about
|
|
9
|
+
* distribution than about whether the puzzle landed.
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
const ROOT = join(import.meta.dirname, "..");
|
|
15
|
+
|
|
16
|
+
type Episode = {
|
|
17
|
+
card: string;
|
|
18
|
+
name: string;
|
|
19
|
+
difficulty?: string;
|
|
20
|
+
concept?: string;
|
|
21
|
+
format?: string;
|
|
22
|
+
caption: string | null;
|
|
23
|
+
permalink?: string;
|
|
24
|
+
postedAt?: string;
|
|
25
|
+
metrics?: Record<string, number>;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type Post = { insights?: Record<string, number>; comments: number };
|
|
29
|
+
|
|
30
|
+
/** Comments per 1,000 accounts reached. */
|
|
31
|
+
const rate = (comments: number, reach: number) =>
|
|
32
|
+
reach ? (comments / reach) * 1000 : 0;
|
|
33
|
+
|
|
34
|
+
const median = (n: number[]) => {
|
|
35
|
+
if (!n.length) return 0;
|
|
36
|
+
const s = [...n].sort((a, b) => a - b);
|
|
37
|
+
const m = s.length >> 1;
|
|
38
|
+
return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const pad = (v: string | number, n: number) => String(v).padStart(n);
|
|
42
|
+
|
|
43
|
+
const main = () => {
|
|
44
|
+
const index = JSON.parse(
|
|
45
|
+
readFileSync(join(ROOT, "episodes/index.json"), "utf8"),
|
|
46
|
+
) as Episode[];
|
|
47
|
+
const posts = JSON.parse(
|
|
48
|
+
readFileSync(join(ROOT, "episodes/performance.json"), "utf8"),
|
|
49
|
+
) as Post[];
|
|
50
|
+
|
|
51
|
+
// Baseline: every post we have reach for, series or not.
|
|
52
|
+
const withReach = posts.filter((p) => p.insights?.reach);
|
|
53
|
+
const baseline = median(
|
|
54
|
+
withReach.map((p) => rate(p.comments, p.insights!.reach)),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const quiz = index.filter((e) => e.format !== "debate");
|
|
58
|
+
const live = quiz
|
|
59
|
+
.filter((e) => e.metrics?.reach)
|
|
60
|
+
.map((e) => ({
|
|
61
|
+
...e,
|
|
62
|
+
cpr: rate(e.metrics!.comments ?? 0, e.metrics!.reach),
|
|
63
|
+
}))
|
|
64
|
+
.sort((a, b) => b.cpr - a.cpr);
|
|
65
|
+
|
|
66
|
+
console.log(
|
|
67
|
+
`\nWHAT PRINTS — ${live.length} published of ${quiz.length} cards in the library`,
|
|
68
|
+
);
|
|
69
|
+
console.log(
|
|
70
|
+
`baseline: ${baseline.toFixed(2)} comments per 1k reach (median of ${withReach.length} posts)\n`,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (!live.length) {
|
|
74
|
+
console.log("Nothing published yet with reach data — nothing to learn from.");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log(
|
|
79
|
+
" c/1k vs base reach cmts saves diff posted card",
|
|
80
|
+
);
|
|
81
|
+
for (const e of live) {
|
|
82
|
+
const vs = baseline ? `${(e.cpr / baseline).toFixed(1)}x` : "-";
|
|
83
|
+
console.log(
|
|
84
|
+
`${pad(e.cpr.toFixed(1), 6)} ${pad(vs, 8)} ${pad(e.metrics!.reach, 7)} ` +
|
|
85
|
+
`${pad(e.metrics!.comments ?? 0, 6)} ${pad(e.metrics!.saved ?? 0, 6)} ` +
|
|
86
|
+
`${pad(e.difficulty ?? "?", 6)} ${e.postedAt?.slice(0, 10)} ${e.name}`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Grouped read-outs only once there's enough to be worth reading.
|
|
91
|
+
const MIN_PER_GROUP = 3;
|
|
92
|
+
const groups = (key: "difficulty" | "concept") => {
|
|
93
|
+
const m = new Map<string, number[]>();
|
|
94
|
+
for (const e of live) {
|
|
95
|
+
const k = e[key];
|
|
96
|
+
if (!k) continue;
|
|
97
|
+
m.set(k, [...(m.get(k) ?? []), e.cpr]);
|
|
98
|
+
}
|
|
99
|
+
const usable = [...m].filter(([, v]) => v.length >= MIN_PER_GROUP);
|
|
100
|
+
if (!usable.length) return;
|
|
101
|
+
console.log(`\nby ${key}:`);
|
|
102
|
+
for (const [k, v] of usable.sort(
|
|
103
|
+
(a, b) => median(b[1]) - median(a[1]),
|
|
104
|
+
))
|
|
105
|
+
console.log(` ${k.padEnd(10)} ${median(v).toFixed(1)} c/1k (n=${v.length})`);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
if (live.length >= 5) {
|
|
109
|
+
groups("difficulty");
|
|
110
|
+
groups("concept");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log("");
|
|
114
|
+
if (live.length < 5)
|
|
115
|
+
console.log(
|
|
116
|
+
`Only ${live.length} published — too few to tell a pattern from noise.\n` +
|
|
117
|
+
`Ship ~5 more before reading anything into the ranking. The library has\n` +
|
|
118
|
+
`${quiz.length - live.length} cards ready to go.`,
|
|
119
|
+
);
|
|
120
|
+
else
|
|
121
|
+
console.log(
|
|
122
|
+
"Feed the top performers' shared traits back into the rubric in the\n" +
|
|
123
|
+
"what-prints-card skill, and retire concepts that sit below baseline.",
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
main();
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-render the card library in the current design.
|
|
3
|
+
*
|
|
4
|
+
* The cards are baked PNGs, so a change to CodeCard.tsx only affects new ones —
|
|
5
|
+
* every puzzle already in the library keeps whatever it was rendered (or
|
|
6
|
+
* screenshotted) with. This regenerates them from the code stored in
|
|
7
|
+
* episodes/index.json, so one design change reaches everything.
|
|
8
|
+
*
|
|
9
|
+
* npx tsx scripts/rerender-cards.ts --dry-run # list what would change
|
|
10
|
+
* npx tsx scripts/rerender-cards.ts # do it
|
|
11
|
+
*
|
|
12
|
+
* The originals are moved to public/assets/cards-original/ rather than
|
|
13
|
+
* overwritten — they're the reference for what the design is imitating, and a
|
|
14
|
+
* few of them are screenshots that can't be reproduced at all.
|
|
15
|
+
*
|
|
16
|
+
* Verification and the quality gates are SKIPPED here on purpose: these
|
|
17
|
+
* snippets already passed them when they entered the library, and re-running
|
|
18
|
+
* every snippet through a real interpreter turns a re-skin into a long and
|
|
19
|
+
* flaky job. Use scripts/quality.ts to audit content.
|
|
20
|
+
*/
|
|
21
|
+
import { execFileSync } from "node:child_process";
|
|
22
|
+
import {
|
|
23
|
+
copyFileSync,
|
|
24
|
+
existsSync,
|
|
25
|
+
mkdirSync,
|
|
26
|
+
readFileSync,
|
|
27
|
+
writeFileSync,
|
|
28
|
+
} from "node:fs";
|
|
29
|
+
import { join } from "node:path";
|
|
30
|
+
|
|
31
|
+
const ROOT = join(__dirname, "..");
|
|
32
|
+
const EPISODES = join(ROOT, "episodes");
|
|
33
|
+
const CARDS = join(ROOT, "public/assets/cards");
|
|
34
|
+
const ORIGINALS = join(ROOT, "public/assets/cards-original");
|
|
35
|
+
|
|
36
|
+
type Entry = {
|
|
37
|
+
name?: string;
|
|
38
|
+
code?: string;
|
|
39
|
+
language?: string;
|
|
40
|
+
card?: string;
|
|
41
|
+
difficulty?: string;
|
|
42
|
+
retired?: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const asPng = (card: string) => card.replace(/\.(jpe?g|png)$/i, ".png");
|
|
46
|
+
|
|
47
|
+
const main = () => {
|
|
48
|
+
const dryRun = process.argv.includes("--dry-run");
|
|
49
|
+
const index = JSON.parse(
|
|
50
|
+
readFileSync(join(EPISODES, "index.json"), "utf8"),
|
|
51
|
+
) as Entry[];
|
|
52
|
+
const cards = JSON.parse(
|
|
53
|
+
readFileSync(join(EPISODES, "cards.json"), "utf8"),
|
|
54
|
+
) as { card: string; language?: string }[];
|
|
55
|
+
const langOf = new Map(cards.map((c) => [c.card, c.language]));
|
|
56
|
+
|
|
57
|
+
const live = index.filter((e) => !e.retired && e.card);
|
|
58
|
+
const doable = live.filter((e) => e.code);
|
|
59
|
+
const stuck = live.filter((e) => !e.code);
|
|
60
|
+
|
|
61
|
+
console.log(
|
|
62
|
+
`${live.length} live cards · ${doable.length} re-renderable · ${stuck.length} without stored code`,
|
|
63
|
+
);
|
|
64
|
+
for (const e of stuck) {
|
|
65
|
+
console.log(` ! ${e.name ?? e.card}: screenshot only, left as-is`);
|
|
66
|
+
}
|
|
67
|
+
if (dryRun) {
|
|
68
|
+
for (const e of doable) console.log(` would re-render ${asPng(e.card!)}`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
mkdirSync(ORIGINALS, { recursive: true });
|
|
73
|
+
let done = 0;
|
|
74
|
+
|
|
75
|
+
for (const e of doable) {
|
|
76
|
+
const png = asPng(e.card!);
|
|
77
|
+
const slug = png.replace(/\.png$/, "");
|
|
78
|
+
const src = join(CARDS, png);
|
|
79
|
+
|
|
80
|
+
// Keep the original once. A second run must not overwrite the archive with
|
|
81
|
+
// an already-re-rendered card.
|
|
82
|
+
const archived = join(ORIGINALS, png);
|
|
83
|
+
if (existsSync(src) && !existsSync(archived)) copyFileSync(src, archived);
|
|
84
|
+
|
|
85
|
+
const puzzle = {
|
|
86
|
+
slug,
|
|
87
|
+
name: e.name ?? slug,
|
|
88
|
+
language: e.language ?? langOf.get(e.card!) ?? "Python",
|
|
89
|
+
code: e.code,
|
|
90
|
+
difficulty: e.difficulty ?? "medium",
|
|
91
|
+
};
|
|
92
|
+
const tmp = join(ROOT, `.rerender-${slug}.json`);
|
|
93
|
+
writeFileSync(tmp, JSON.stringify(puzzle));
|
|
94
|
+
try {
|
|
95
|
+
execFileSync(
|
|
96
|
+
"npx",
|
|
97
|
+
["tsx", "scripts/make-card.ts", tmp, "--no-verify", "--no-gates"],
|
|
98
|
+
{ cwd: ROOT, stdio: ["ignore", "pipe", "pipe"], encoding: "utf8" },
|
|
99
|
+
);
|
|
100
|
+
done++;
|
|
101
|
+
console.log(` ✓ ${png}`);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
const e2 = err as { stdout?: string; stderr?: string };
|
|
104
|
+
const why = `${e2.stdout ?? ""}${e2.stderr ?? ""}`
|
|
105
|
+
.split("\n")
|
|
106
|
+
.find((l) => /Error/.test(l));
|
|
107
|
+
console.error(` ✗ ${png}: ${why ?? "render failed"}`);
|
|
108
|
+
} finally {
|
|
109
|
+
execFileSync("rm", ["-f", tmp]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log(`\n${done}/${doable.length} re-rendered. Originals: ${ORIGINALS}`);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
main();
|