My haunted mirror possesses your reflection with open-weight AI. A player’s idea made it hunt you in the dark A developer built Scare-TV, a Halloween installation that uses open-weight models to possess a visitor's own camera reflection, running YOLO11n-pose on a TV with a webcam and MediaPipe Face Mesh in the browser with all frames processed on-device. A player's suggestion led to a same-day lights-out mode, and the developer reports that across 35 real scares on October 31 people jumped 44% more in the dark. A second model, SD-Turbo, generates horror faces that YOLO-pose grades, with 8 of 10 passing as possessable, and the developer documents nine bugs and their fixes. This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine https://dev.to/challenges/hacktoberfest-week4-2026-10-26 After trying my haunted mirror, a player sent me the most useful message I got all week: "Instead of looking at yourself, make the screen go black. Say it can't see you. Then jump out with a really loud noise." They were right. If you can watch it coming, you're ready for it. So I built lights-out mode the same afternoon. The screen goes black. A flickering "I can't see you…" sits in the middle, and a voice whispers it in the dark. An open-weight face model keeps watching you in the dark, and when it decides, or the moment you blink, your own possessed face jumps out at you, 2.4Γ— louder. This post is about that loop: build, put it in front of a person, listen, fix, measure. It covers the ghosts I built with open models, everything that broke along the way, and what AI made possible for one developer in one week. TL;DR Scare-TV possesses your own reflection : black eyes, a mouth that tears open, a grin that isn't yours, a pale double that lags a second behind you. It runs on a TV with a webcam YOLO11n-pose and on any phone MediaPipe Face Mesh , in the browser . No frame ever leaves the device. - A jump-o-meter measures how far your face moves in the 1.5 s after the hit. That single number is the only thing the server gets. Production stores one row per person , from TVs and phones, with their highest jump level 0–10 . A player's feedback shipped the same day: lights-out mode. Both modes are stored separately, and the data says the player was right: in the dark, people jumped 44% more on average 35 real scares on October 31 . Scare-TV 2: a second open model, SD-Turbo , imagines horror faces on my laptop. YOLO-pose grades every one, and only faces it can possess get to scare anyone. 8 of 10 made it. Plus 20 classic scares for anyone who doesn't want to turn the camera on. Nine things broke one silently threw away every phone scare, another only showed up on a real TV in a dark living room . Each one is below, with the fix. All of the public side runs on Render from one Blueprint: Django + DRF, PostgreSQL, the admin, and a keep-alive so the first scare of the night never waits for a cold start. Try it now on your phone: scare-tv.onrender.com https://scare-tv.onrender.com . Dim the lights, turn the sound up, and pick Lights out πŸŒ‘ if you're brave. Lights-out mode. Demo face generated with SD-Turbo not a real person ; simulated camera and flinch. πŸŽƒ Live: scare-tv.onrender.com https://scare-tv.onrender.com . It works on any phone with a front camera. Choose Look πŸ‘οΈ for the mirror or Lights out πŸŒ‘ for the ambush. At the end you get a polaroid of your own face half a second after the hit, with your jump-o-meter score and how you rank against everyone else. Every demo face in this post and in the video was generated with SD-Turbo on my laptop. None of them is a real person, and the flinches in the GIFs are simulated. The real reactions are in the numbers section. Ghost in the Machine, literally: a ghost that lives inside your camera feed. You look at the screen, and the screen looks back. A laptop behind the TV, a webcam on top, and a friendly jack-o'-lantern on screen saying "Happy Halloween Come closer… I have treats for you." People come closer, right in front of the camera. YOLO11n-pose 6 MB, open weights checks every frame for a person , not just movement, so the cat and the curtains don't count. Then the TV becomes a mirror, and one of six things happens to their real reflection : | | What happens to you | |---|---| | Black eyes | Your eyes fill with black and dark veins branch out across your skin. "I see you." | | Faceless | Your features smooth into blank skin, then a mouth full of teeth tears open. | | Behind you | A pale girl with long black hair peeks out from behind your shoulder. "Don't turn around." | | Melting | Your face drips downward and your eyes go hollow. | | Smile | A grin from ear to ear that isn't yours. "Smile for me." | | The double | Your reflection lags a second behind, pale and black-eyed, and creeps closer. | And 20 classic scares. Not everyone wants to point a camera at their face, so the TV and the phone also have 20 classic jump scares a skull, a thousand smiles, something on the ceiling, a dead channel, a flatline… . The art and every sound are generated in code, with no samples and no copyrighted characters. The same six possessions run inside a phone browser , on top of MediaPipe Face Mesh: 468 landmarks, open weights, about 10 MB, served with the page instead of from a CDN. Nothing to install. The server never receives a frame. Cartoon ghosts are fun, but you always know they're drawings. So a second open model, SD-Turbo , imagines realistic horror portraits on my 8 GB MacBook Air. Then YOLO-pose grades every photo before it's allowed to scare anyone: it needs a clear head, visible eyes and shoulders below the nose. Photos that fail get imagined again, up to five times, and the rest are thrown away. The survivors get possessed and become a scare: darkness, a flashlight sweeping the room, then the whole face lunging at you. Every scare, from every TV and every phone, lands in one PostgreSQL database on Render: one row per person , with their scare count, their highest jump level 0–10 and whether they ran out of frame. A Django admin lets me filter by phone or TV, by possession, by mode, by jump level. This is the part I'm proudest of, and it isn't code. Before: the mirror gives you 2.6 seconds of tension. The picture darkens and pushes in on your face, a line appears "I see you…" , and then comes the hit. It looks great. But a player pointed out that it warns you . You see your eyes going black, you brace yourself, and the hit lands on someone who was expecting it. The feedback, word for word translated from Spanish : "Instead of looking at the mirror, make the screen go black, have it say 'I can't see you', and then suddenly the scare comes out and makes a lot of noise." After: lights-out mode. web or web dark , so the question Left: the mirror, you watch it coming. Right: lights out. That one conversation also led to two changes in the mirror itself: The mirror felt finished before I showed it to someone. It wasn't. Things broke all week, several of them in production. Here's every one. 1. My first realistic faces looked like a clothing catalogue. SD-Turbo's first batch was moody portraits of sad people, not horror. The second batch, after I asked for "horror" harder, was half abstract textures. The fix was two-sided: 2. The scare had a rhythm. You looked into the mirror, and 1.3 s later it always started. Anyone who tried it twice could feel it coming. Now the wait is random and the hit is triggered by your blink. 3. The first phone scares vanished. This one hurt. The first phone version was a static page with no database scare-tv-mirror . I later deployed the real one with a database, but the link I'd been sharing still pointed to the old page, and phones had cached it. The scares happened, and none of them was saved. Production showed 0 phone visits while I was recording videos of the mirror working on my own phone. app.js?v=6 , so phones drop the cached copy. 4. Production crashed because of numpy. I moved the 0–10 jump-level calculation into the domain layer. It lived in a module that imports numpy, which the web service on Render doesn't install. All tests passed locally, but two deploys in a row failed on boot with ModuleNotFoundError: No module named 'numpy' . Now the score lives in a pure-Python module, and before every deploy the checks run in a clean environment without numpy , the same as Render. 5. The admin user existed but couldn't log in. createsuperuser --noinput without a password doesn't fail: it creates a user with an unusable password, and setting the variable later doesn't fix it, because it never runs again. I replaced it with a small ensure superuser command that runs on every boot and sets or updates the password from the environment. 6. One person counted as five. On the phone, every "Again πŸ‘»" recorded a new arrival, so someone who pressed it five times looked like five people. Now one phone visit is one person, and every scare hangs from the person it happened to. 7. I pushed my demo folder to GitHub. GIFs, a video and AI-generated faces went into the repo with a feature commit. It's now in .gitignore and untracked. Boring, but it's in the list because it happened. 8. The veins looked painted on. Looking at the frames for this post, the black-eyes veins were obviously straight strokes drawn on top of the photo. They broke the illusion more than any glitch. Now each vein is a seeded random walk that bends and branches out of the eye socket, thick at the root and thin at the tip, and it's multiplied into the skin with a soft bruise around it instead of painted over it. They grow with the possession, never flicker between frames, and the TV OpenCV and the phone canvas draw the same trees. The realistic faces were all regenerated with them. 9. Lights out never struck on a real TV. On the phone it worked every time. Then I tried it the way it was meant to be played: laptop behind the living-room TV, webcam on top, lights off. The screen said "I can't see you…" for more than 30 seconds and nothing happened. It was telling the truth. A black screen in a dark room leaves the webcam nothing to see, and lights-out mode waits for a face before it strikes. On a phone, the screen is a few centimetres from your face and that's enough light. On a TV across the room, it isn't. That's also why none of those TV scares reached the database: they never happened. The real test: the TV in my living room, lights off. The black screen looks blue because of the TV's backlight and my phone's camera. The fix fits the story: if it can't see you, it looks for you. After 2 seconds without a face, the screen slowly lights up in a cold, flickering glow and the text changes to "WHERE ARE YOU…?" . A TV that size makes a decent lamp, and as soon as the face model finds someone, it waits a random moment and strikes. Short losses of the face in the dark no longer restart the wait. Deployed the same evening as app.js?v=8 . And from the first days of the TV version: Two different kinds of AI are in this project, and they do three different jobs. 1. Open models as the ghosts. Face Mesh and YOLO-pose find your eyes, nose and mouth in every frame, and everything scary is drawn on those points. SD-Turbo imagines faces that don't exist. None of the effects use a generative model on your face. It's geometry on top of open-weight landmarks, which is why it runs live in a phone browser. 2. One model grading another. SD-Turbo generates, YOLO-pose judges, and the bad ones are imagined again. No human picked the faces. A cheap, local, checkable loop between two open models is what made Scare-TV 2 work on a laptop. PHONE browser TV laptop + webcam camera ─▢ MediaPipe Face Mesh 468 pts, local webcam ─▢ YOLO11n-pose 6 MB, local β”‚ β”‚ β”œβ”€β–Ά possession drawn on