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.
Files changed (54) hide show
  1. package/README.md +215 -0
  2. package/package.json +64 -0
  3. package/public/fonts/Handjet-variable.woff2 +0 -0
  4. package/remotion.config.ts +14 -0
  5. package/scripts/check-legibility.ts +284 -0
  6. package/scripts/check-safe-area.ts +148 -0
  7. package/scripts/custom-fonts.ts +114 -0
  8. package/scripts/export.sh +31 -0
  9. package/scripts/field-images.ts +93 -0
  10. package/scripts/ig-probe.ts +83 -0
  11. package/scripts/ig-sync.ts +204 -0
  12. package/scripts/ingest.sh +34 -0
  13. package/scripts/library.ts +143 -0
  14. package/scripts/look-card.ts +107 -0
  15. package/scripts/look-store.ts +176 -0
  16. package/scripts/make-card.ts +237 -0
  17. package/scripts/merge-index.ts +74 -0
  18. package/scripts/new-episode.ts +149 -0
  19. package/scripts/overlay-worker.ts +1119 -0
  20. package/scripts/place-overlay.ts +359 -0
  21. package/scripts/prep-card.ts +73 -0
  22. package/scripts/quality.ts +0 -0
  23. package/scripts/render-overlay.ts +150 -0
  24. package/scripts/report.ts +127 -0
  25. package/scripts/rerender-cards.ts +116 -0
  26. package/scripts/series.ts +816 -0
  27. package/scripts/set-difficulty.ts +62 -0
  28. package/scripts/state-dir.ts +102 -0
  29. package/scripts/stock.ts +254 -0
  30. package/scripts/verify.ts +149 -0
  31. package/scripts/wp-restock.ts +281 -0
  32. package/src/Root.tsx +112 -0
  33. package/src/index.css +1 -0
  34. package/src/index.ts +4 -0
  35. package/src/lab/FontLab.tsx +50 -0
  36. package/src/lab/FontSheet.tsx +188 -0
  37. package/src/lab/PillLab.tsx +121 -0
  38. package/src/overlay/Composition.tsx +297 -0
  39. package/src/overlay/DifficultyMeter.tsx +86 -0
  40. package/src/overlay/PixelText.tsx +134 -0
  41. package/src/overlay/Title.tsx +75 -0
  42. package/src/overlay/brandFonts.ts +58 -0
  43. package/src/overlay/cardLayout.ts +94 -0
  44. package/src/overlay/fonts.ts +19 -0
  45. package/src/overlay/look.ts +155 -0
  46. package/src/overlay/safeArea.ts +89 -0
  47. package/src/overlay/types.ts +134 -0
  48. package/src/series/what-prints/CodeCard.tsx +107 -0
  49. package/src/series/what-prints/Composition.tsx +106 -0
  50. package/src/series/what-prints/codeCardTypes.ts +105 -0
  51. package/src/series/what-prints/types.ts +22 -0
  52. package/tsconfig.json +17 -0
  53. package/worker/cli.mjs +151 -0
  54. 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();