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,74 @@
1
+ /**
2
+ * Merge the caption batches + card transcriptions into episodes/index.json.
3
+ *
4
+ * Batches come from reading the caption screenshots; cards.json from reading the
5
+ * code-card images. Episodes are matched to cards by normalized code text.
6
+ *
7
+ * Usage: npx tsx scripts/merge-index.ts
8
+ */
9
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
10
+ import { join } from "node:path";
11
+
12
+ const EPISODES = join(import.meta.dirname, "../episodes");
13
+
14
+ type Batch = {
15
+ episode: number | null;
16
+ name: string;
17
+ code: string | null;
18
+ answer: string | null;
19
+ why: string | null;
20
+ caption: string | null;
21
+ sourceScreenshots: string[];
22
+ };
23
+ type Card = { card: string; language: string; code: string };
24
+
25
+ const load = <T>(f: string): T[] => {
26
+ const p = join(EPISODES, f);
27
+ return existsSync(p) ? (JSON.parse(readFileSync(p, "utf8")) as T[]) : [];
28
+ };
29
+
30
+ // Normalize code for matching: strip whitespace, quotes, and comments so
31
+ // transcription differences between the two passes don't block a match.
32
+ const norm = (s: string | null) =>
33
+ (s ?? "")
34
+ .toLowerCase()
35
+ .replace(/["'`]/g, "")
36
+ .replace(/\s+/g, "")
37
+ .replace(/#.*$/gm, "");
38
+
39
+ const episodes = [
40
+ ...load<Batch>("batch1.json"),
41
+ ...load<Batch>("batch2.json"),
42
+ ...load<Batch>("batch3.json"),
43
+ ];
44
+ const cards = load<Card>("cards.json");
45
+
46
+ const used = new Set<string>();
47
+ const merged = episodes.map((e) => {
48
+ const key = norm(e.code);
49
+ let card: string | null = null;
50
+ if (key.length > 12) {
51
+ const exact = cards.find((c) => !used.has(c.card) && norm(c.code) === key);
52
+ const partial =
53
+ exact ??
54
+ cards.find(
55
+ (c) =>
56
+ !used.has(c.card) &&
57
+ (norm(c.code).includes(key) || key.includes(norm(c.code))),
58
+ );
59
+ if (partial) {
60
+ card = partial.card;
61
+ used.add(partial.card);
62
+ }
63
+ }
64
+ return { ...e, card };
65
+ });
66
+
67
+ const unmatchedCards = cards.filter((c) => !used.has(c.card)).map((c) => c.card);
68
+
69
+ writeFileSync(join(EPISODES, "index.json"), JSON.stringify(merged, null, 2) + "\n");
70
+
71
+ console.log(`Merged ${merged.length} episodes from ${episodes.length} batch entries.`);
72
+ console.log(` matched to cards: ${merged.filter((e) => e.card).length}`);
73
+ console.log(` episodes w/o card: ${merged.filter((e) => !e.card).map((e) => e.name).join(", ") || "none"}`);
74
+ console.log(` cards w/o episode: ${unmatchedCards.join(", ") || "none"}`);
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Create the next episode: pick the first unused card (and b-roll), write an
3
+ * episode JSON plus a ready-to-paste caption file.
4
+ *
5
+ * Usage:
6
+ * npx tsx scripts/new-episode.ts # next unused card
7
+ * npx tsx scripts/new-episode.ts IMG_4041 # a specific card
8
+ */
9
+ import { readFileSync, writeFileSync, readdirSync, existsSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { execFileSync } from "node:child_process";
12
+
13
+ const ROOT = join(import.meta.dirname, "..");
14
+ const EPISODES = join(ROOT, "episodes");
15
+ const CARDS = join(ROOT, "public/assets/cards");
16
+ const BROLL = join(ROOT, "public/assets/b-roll");
17
+
18
+ type IndexEntry = {
19
+ episode: number | null;
20
+ name: string;
21
+ card: string | null; // raw filename, e.g. "IMG_4030.jpg"
22
+ answer?: string;
23
+ why?: string;
24
+ difficulty?: "easy" | "medium" | "hard";
25
+ caption?: string | null;
26
+ retired?: string; // set by the quality gates; never pick these
27
+ };
28
+
29
+ const readJson = <T>(p: string, fallback: T): T =>
30
+ existsSync(p) ? (JSON.parse(readFileSync(p, "utf8")) as T) : fallback;
31
+
32
+ const index = readJson<IndexEntry[]>(join(EPISODES, "index.json"), []);
33
+ const used = readJson<{ cards: Record<string, string>; broll: Record<string, string> }>(
34
+ join(EPISODES, "used.json"),
35
+ { cards: {}, broll: {} },
36
+ );
37
+
38
+ // Cards retired by scripts/quality.ts stay on disk but are never picked again.
39
+ const retired = new Set(
40
+ index
41
+ .filter((e) => e.retired && e.card)
42
+ .map((e) => e.card!.replace(/\.(png|jpe?g)$/i, "") + ".png"),
43
+ );
44
+
45
+ const cardPngs = readdirSync(CARDS)
46
+ .filter((f) => f.endsWith(".png") && !retired.has(f))
47
+ .sort();
48
+ const requested = process.argv[2];
49
+
50
+ const cardPng = requested
51
+ ? cardPngs.find((f) => f.startsWith(requested.replace(/\.(png|jpe?g)$/i, "")))
52
+ : cardPngs.find((f) => !used.cards[f]);
53
+
54
+ if (!cardPng) {
55
+ console.error(requested ? `No card matching ${requested}` : "All cards used.");
56
+ process.exit(1);
57
+ }
58
+
59
+ const stem = cardPng.replace(/\.png$/, "");
60
+ const meta = index.find((e) => e.card?.replace(/\.jpe?g$/i, "") === stem);
61
+
62
+ // Prefer an unused b-roll clip; otherwise reuse the least-recently-used one.
63
+ const brollFiles = readdirSync(BROLL).filter((f) => /\.(mov|mp4)$/i.test(f)).sort();
64
+ const broll = brollFiles.find((f) => !used.broll[f]) ?? brollFiles[0];
65
+
66
+ const brollDuration = Number(
67
+ execFileSync("ffprobe", [
68
+ "-v", "quiet", "-show_entries", "format=duration",
69
+ "-of", "default=noprint_wrappers=1:nokey=1",
70
+ join(BROLL, broll),
71
+ ]).toString().trim(),
72
+ );
73
+
74
+ const nextNumber = readdirSync(EPISODES)
75
+ .map((f) => /^what-prints-(\d+)\.json$/.exec(f)?.[1])
76
+ .filter(Boolean)
77
+ .reduce((max, n) => Math.max(max, Number(n)), 0) + 1;
78
+
79
+ const id = `what-prints-${String(nextNumber).padStart(3, "0")}`;
80
+ const durationSec = Math.min(10, Math.floor(brollDuration));
81
+
82
+ const episode = {
83
+ broll: `assets/b-roll/${broll}`,
84
+ card: `assets/cards/${cardPng}`,
85
+ titleLine1: "What",
86
+ titleLine2: "Prints?",
87
+ difficulty: meta?.difficulty ?? "medium",
88
+ durationSec,
89
+ revealAtSec: Math.round(durationSec * 0.4),
90
+ trimBeforeSec: 0,
91
+ };
92
+
93
+ writeFileSync(join(EPISODES, `${id}.json`), JSON.stringify(episode, null, 2) + "\n");
94
+
95
+ const caption =
96
+ meta?.caption ??
97
+ [
98
+ "What does this print?",
99
+ "",
100
+ "No IDE.",
101
+ "No running the code.",
102
+ "Only vibes.",
103
+ "",
104
+ "#python #codingchallenge #debugging #softwareengineering #interviewprep #pythondeveloper #devlife",
105
+ ].join("\n");
106
+
107
+ writeFileSync(join(ROOT, "out", `${id}.caption.txt`), caption + "\n");
108
+
109
+ used.cards[cardPng] = id;
110
+ used.broll[broll] = id;
111
+ writeFileSync(join(EPISODES, "used.json"), JSON.stringify(used, null, 2) + "\n");
112
+
113
+ console.log(`Created ${id}`);
114
+ console.log(` card: ${cardPng}${meta ? ` (${meta.name})` : " (no index metadata)"}`);
115
+ console.log(` answer: ${meta?.answer ?? "-"}`);
116
+ console.log(` b-roll: ${broll} (${durationSec}s, reveal at ${episode.revealAtSec}s)`);
117
+ // Place the difficulty meter against this clip's footage.
118
+ execFileSync("npx", ["tsx", join(ROOT, "scripts/place-overlay.ts"), join(EPISODES, `${id}.json`)], {
119
+ stdio: "inherit",
120
+ });
121
+
122
+ // Nothing under Instagram's own chrome...
123
+ try {
124
+ execFileSync(
125
+ "npx",
126
+ ["tsx", join(ROOT, "scripts/check-safe-area.ts"), join(EPISODES, `${id}.json`)],
127
+ { stdio: "inherit" },
128
+ );
129
+ } catch {
130
+ console.error(`\n${id}: an overlay sits under Instagram's UI (see above).`);
131
+ }
132
+
133
+ // ...and then check the overlays actually read against this clip. Runs here rather
134
+ // than on request because the one time it mattered, nobody thought to look —
135
+ // a title that vanished into a bright keyboard shipped as episode 002.
136
+ try {
137
+ execFileSync(
138
+ "npx",
139
+ ["tsx", join(ROOT, "scripts/check-legibility.ts"), join(EPISODES, `${id}.json`)],
140
+ { stdio: "inherit" },
141
+ );
142
+ } catch {
143
+ console.error(
144
+ `\n${id}: overlays don't read against this b-roll. Pick a calmer clip,\n` +
145
+ `or darken the scrim in src/series/what-prints/Composition.tsx.`,
146
+ );
147
+ }
148
+
149
+ console.log(`\nRender: npx remotion render WhatPrints out/${id}.mp4 --props=episodes/${id}.json`);