I read neuroscience and psychology at King's College London and left in my second year without finishing. What I want to build is a non-invasive brain-computer interface, one that reads intent well enough that you could hold a conversation with an animal through it. Studying the brain was never going to teach me how to build the system that reads it, so I went to learn that instead. I have been at 42 London since 2025. The work below is the route there: take an AI product all the way to production until no layer of it is someone else's problem.
EARL Hub is an AI docent. Seven deceased Korean cultural figures rebuilt as 3D digital
humans you talk to in a browser, live at earlhub.cloud. Four people
worked on it. The two repositories it runs on hold 629 commits and every one of them is mine —
the several author names on them are my own two machines plus an agent in my session. The
team's contribution arrived as delivered assets rather than commits: a rigged
.blend for Nam June Paik (백남준) with Auto-Rig Pro, textures and Marvelous cloth, plus a
MetaHuman package. I wrote the intake, validation, decimation and retargeting around those, and
rigged some of the figures myself. Line counts below are tracked source, with vendored add-ons
and generated files excluded.
Conversation pipeline and voice. Spoken conversation costs latency and money, and no single cache tier fixes both. I put three tiers in front of live generation: question normalization, exact match against pre-generated answers, then a small-LLM intent router. An exact hit puts first audio at 12–21ms. The router began as an embedding classifier and I threw it out the same day. In short spoken Korean the true and false similarity distributions overlap, so there is nowhere to put a threshold. A 72-case benchmark then picked the replacement model: p50 1,029ms → 554ms at 1/6.3 the cost. The live path still takes 2.3s to first audio, and I have that written down as unsolved rather than rounded away. The latency that remained I staged rather than fought. Routing runs on partial transcripts while the visitor is still talking, which removes about 1.2s of waiting and turns tone classification from 850ms into 5ms on a hit. A cold-start module classifies the question and emits an echo or a bridging phrase at t=0, so perceived silence went 2,621ms → 304ms. That module keeps generic speech habits and verified quotations in separate lists on purpose: these are real people, and inventing a quotation is the one failure this project cannot survive. Voice cloning is scripted with the rights exclusions sitting in the code rather than in a checklist. Five of the figures have a tone-sorted filler pack synthesized in their own cloned voice from speech habits documented in their verification file; Beopjeong (법정) has none, because the corpus could not settle whether the absence of fillers was how he spoke or an artefact of our own energy gate.
The web product. React, three.js and Vite: 179 files, 31,414 lines, of which the 3D stage
is 17,406. Five of the stages are procedural, baked by Blender scripts into glTF with albedo,
normal, ORM and emissive maps; the downloaded CC-BY sets are credited in a ledger and only the
decimation there is mine. prefers-reduced-motion is honoured in six stylesheets and again in
JavaScript, because CSS does not stop a WAAPI animation. WCAG contrast ratios are written into
the colour tokens as the reason each value is what it is, and the deliberate exception for
disabled UI cites the clause it relies on.
Digital humans and motion. Our character authored in MetaHuman Creator 5.8 shipped a face
ABP with no LiveLink node in it, so solver curves never reached the mesh while every reading on
the source side stayed healthy. I swapped the
AnimInstance for ULiveLinkInstance at runtime and kept the RigLogic post-process: measured
face pixel delta during speech went 0.70 → 4.9–14.3. The Korean viseme lip-sync solver and
its Live Link source are 4,184 lines of the 17,150 lines of UE 5.8 C++ in the application.
Motion capture comes in from Xsens and Manus; I built the processing, not the shoot. That means
a direct parser for Xsens ASCII FBX (959 lines) after the assimp route turned out to bake the
frame-0 pose into node transforms, a 53-bone to 26-bone retarget with the four-segment spine
mapped onto five by normalized position at 0.132 max error, an Auto-Rig Pro variant, and a
finger audit that found takes whose finger channels were frozen or absent entirely.
Tools and deployment. The Persona Lab console is one page: 881 lines, eleven toggles, model
pickers with per-token cost, and a golden-QA batch runner that exports markdown, so someone who
has never seen the pipeline can open a browser, change something and hear the result. A 1,328-line
web testbed does the same for character slots, and one command takes a rigged .blend through
validation, decimation, manifest, in-browser measurement and take matching. Deploy is Cloudflare
Pages with a tunnel to the conversation server and a WebRTC pixel-streaming subdomain, plus a
static cache mode that keeps the demo running with the backend at zero. I wrote the eight-minute
presentation script, the live-demo cue sheet and the slides for my own sections.
| What | Live | Repo | |
|---|---|---|---|
| EARL Hub | AI docent: seven Korean cultural figures rebuilt as 3D digital humans you talk to in a browser | earlhub.cloud | earl-showcase |
| Muse Pilgrimage (뮤지순례) | 1,042 Korean museums and galleries linked into a pilgrimage. PWA offline passport, edge worker, Supabase | holymoly.cloud | muse-pilgrimage-showcase |
| earl-korean-metahuman | Korean MetaHuman lip-sync and a text-to-motion Live Link plugin (UE 5.8, MIT) | — | source |
The first two are closed source, and those links are showcases: the problems and how they were solved, without the code. The reason is rights, not trade secrets — the repos hold rights documentation and the verified-quote ledger for real people who are no longer alive, and the persona prompts would work as a finished impersonation kit. The two Unreal modules carry no personal IP, so I pulled those out and released them under MIT.
Also:
- FIVE ALIBIS (live) — browser detective game. Every round generates a new case from a seed, five suspects are played by gpt-4o, and you have 3–5 minutes to name the killer. The LLM is fenced out of game state entirely: it writes dialogue, while budget, unlocks and scoring live in pure functions. Cases pass seven validators before they are playable.
- AssetBox (live) — internal 3D asset portal for a TA and backend cohort, Spring Boot 4 and React. 96 of 307 backend commits and 203 of 212 frontend commits are mine. The landing page embeds an arcade of five phone-controller party games I built on Cloudflare Durable Objects: the host browser simulates, the DO relays.
- Unframed — UE5 educational game where you capture escaped painting figures and quiz them back into the frame. I did the backend, the API spec and team integration over three days.
- nan-typing — typing roguelike where each word kills once per run and the dictionary holds thirty.
- 42Prague × Škoda — employee-facing view of an AI skill coach built on Škoda's real HR exports.
In progress: a voice-cloning vocal coach, a fingerstyle guitar practice assistant, and a study app built around how ADHD attention actually behaves rather than how planners assume it does.
- 2nd of 13 teams — 2026 Metaverse Academy Midnight Camp hackathon (Unframed, three days).
- 3rd place — 42 London × BearingPoint hackathon.
- Finalist — 42 Prague × Škoda hackathon. The employee-facing view is public above.
- Finalist — Yeongju City youth-housing competition, for a plan to regenerate a declining local high street.
Python for the SSE streaming conversation server. TypeScript, React 19, three.js and GSAP for the real-time 3D web. C++ and UE 5.8 for MetaHuman, Pixel Streaming 2 and Live Link. Blender with Auto-Rig Pro for the asset pipeline. Java and Spring Boot where the work is a service. Cloudflare Pages, Workers and Tunnel for deploy. EARL ran split across three machines: macOS, an RTX 4070 laptop, an RTX 4090 desktop.
Decisions get written down as ADRs, with the alternatives I rejected. 45 of them on EARL. When a decision is later reversed I do not delete the file, I issue a new number. That way "why is it like this?" stays answerable months later.
I decide each time whether the instruments or the eye wins. In the lip-sync A/B the recipe with more gain and lookahead measured 46% higher peak articulation, and I shipped the stock one anyway because it read better on screen: larger articulation is not better articulation. Motion capture went the other way — three clips I had picked by eye as "talking" turned out to be silence once I measured angular velocity, where speech runs 28–46°/s against silence at 8–13°/s.
Unfinished work is written down as unfinished. Where EARL stands today: every finalist figure still shares a single Nam June Paik (백남준) mesh, and only the motion, the voice and the stage are their own. There is no Quest standalone APK; zero builds exist. Remote WebRTC has zero measured sessions, and finding an srflx candidate is not a successful connection. Muse Pilgrimage's visit verification is a pre-GPS MVP simulation. I audit my own numbers too: a line count I had put in an application turned out to be irreproducible, so I re-measured, corrected it downward, and the corrected figure is the one used above.