Inspiration
Room to Mind started with one question: what if the room around a person could be part of their recovery, instead of something they simply had to tolerate?
For most people the room is exactly that, something to tolerate. You have a bad afternoon at a cluttered desk, you blame your focus, you move on. After a concussion it is not minor at all. Light sensitivity and noise sensitivity are cardinal symptoms, so the space stops being background and becomes the thing deciding whether you can read, work, or sit upright comfortably. Environmental modification is one of the very few levers a recovering person still controls while everything else is waiting on time. And there is no instrument for it. Sleep has trackers, exertion has heart rate, air has an index on your phone telling you whether today is a bad day to run. The room has nothing. Someone told to reduce glare and noise gets no way to know whether the room they are sitting in is actually the problem, or which part of it is, or what to change first.
So I built the instrument for that person, and it turns out to work for everyone else too. Room to Mind reads a specific space, reports what it found, ranks the single change worth making first, and shows the entire calculation so you can disagree with it. Two rules were fixed before I wrote a line of code. It had to be honest, because there is a whole genre of app willing to tell you clutter causes anxiety and the evidence does not support it. And it had to be genuinely private, because I am asking someone to photograph the room they live in, and the only real answer is an architecture where the photo cannot leave, not a paragraph in a privacy policy.
What it does
Room to Mind measures how much a room is loading your attention.
You take one photo of the space, and optionally add 20 seconds of sound. The photo is read in your browser and discarded. The sound is never recorded. What comes back is a 0-100 Environmental Load Index built from six factors: visual clutter, acoustic load, workspace separation, lighting, screen positioning, and colour environment.
Then it tells you what to do about it. Up to three specific physical changes, ranked, each one quoting the exact measurement that triggered it. Not "declutter your desk," but the number that made the app say so.
The whole calculation is open
Every factor weight is a live slider, so if you disagree with the priors you can overrule them and watch the score recompute. The result page redraws your room as a diagram from the stored numbers alone, with no photo underneath - because there is no photo anywhere.
Sound is composition, not decibels
A steady fan and an intermittent conversation can hit the same loudness while costing completely different amounts of attention. The app tells you what the sound was made of, not how loud it got.
Nothing is invented
A factor with no evidence is dropped from the score entirely, never set to a midpoint. No screen detected means screen positioning is not scored, and the radar shows it as a gap rather than a zero.
The same room, scored two ways
| Factor | Standard | Recovery |
|---|---|---|
| Visual clutter | 0.25 | 0.10 |
| Acoustic load | 0.25 | 0.30 |
| Workspace separation | 0.20 | 0.05 |
| Lighting | 0.15 | 0.30 |
| Screen positioning | 0.10 | 0.20 |
| Colour environment | 0.05 | 0.05 |
Recovery weighting is for people whose light and noise sensitivity makes those the limiting factors, and it surfaces two extra readings: glare range and sound intermittency - that Standard hides.
Alongside the measurement
- Comfort Palette: dims the interface to a warm, low-blue tone, independent of the score.
- Antara: short breathing and rest practices for when the room cannot be changed right now. Measures nothing.
How I built it
Next.js 14 with the App Router and TypeScript, styled with Tailwind through a two-palette design system driven by CSS variables, deployed on Render as a web service with Postgres.
The visual channel runs COCO-SSD on a mobilenet_v2 backbone through TensorFlow.js, entirely in the browser. Detections are bucketed into work, screen, rest, and clutter, with furniture and fixtures deliberately excluded from clutter. Alongside the model sit hand-written Canvas 2D pixel operations: luminance, contrast, bright-region location, Sobel edge density, k-means palette entropy, saturation, and warmth. I wrote those by hand rather than importing a vision library specifically so every feature can be explained in one sentence, which is what lets the app show its reasoning instead of asserting a number.
The acoustic channel uses the Web Audio AnalyserNode to reduce 20 seconds of sound to five spectral values as it goes: loudness, intermittency, speech-band energy, low-frequency energy, and brightness. The microphone is polled ten times a second and collapsed into running numbers in flight. No recording is ever made, because there is no MediaRecorder anywhere in the codebase.
Scoring is piecewise-linear normalization into a weighted composite, computed only over the factors that have evidence. Recovery weighting swaps the coefficients and re-derives the same composite, so both modes run identical arithmetic on identical measurements.
Personalization is a ridge regression implemented by hand with no ML dependency, carrying per-coefficient standard errors so predictions can show honest uncertainty bands rather than a single confident line. It requires 30 calibration responses before it fits, a deliberate statistical floor since six predictors on fewer would be too thin to trust.
Storage is PostgreSQL through Prisma holding numeric feature vectors only, on a schema with no image column and no audio column. The server enforces it too: a POST containing a string inside the feature payload, such as a smuggled base64 image, is rejected outright. That is verified in production.
The interface is hand-drawn SVG for the score dial, factor radar, and room diagram, so unscored factors can render as genuine gaps rather than being flattened to zero by a charting library.
Challenges we ran into
The detector nearly failed silently: The lightweight COCO backbone saw a real laptop at 0.27 confidence, just under the 0.4 threshold. It did not throw an error. It quietly blanked two of the six factors at once, because a laptop feeds both the work-surface and screen buckets, and produced a clean-looking score built on far less evidence than it appeared to have. Diagnosing it meant building a side-by-side harness comparing backbones and resolutions. The full mobilenet_v2 reads the same laptop at 0.54. The same harness proved the 512px downscale does not hurt detection while running ten times faster than native resolution.
The neutral 50 problem: Early on, a factor with no evidence was scored at a neutral midpoint, so a room with no detected furniture was contributing half its score from nothing at all. Rebuilding it to drop unmeasured factors entirely was the single decision that made the tool trustworthy, and it is now the rule the whole design rests on.
Two bugs I only found by reading real output: The app was counting chairs, plants, and fixtures as clutter and telling users to clear away their own furniture. And the sound brightness reading was being dragged toward zero by silent frames until it became a loudness-weighted average. Both are now covered by regression tests. Neither would have surfaced from unit tests alone, only from looking at what the thing actually said to a person.
Analysing sound I was never allowed to keep: The obvious approach is to capture twenty seconds and process it. That was off the table, so the microphone is polled ten times a second and collapsed into running numbers in flight. Getting a stable acoustic reading from a signal that exists only in transit took several rewrites.
Deployment on a 512MB free tier: Builds kept dying silently on Render because Next.js parallel compilation loads TensorFlow into every worker. Capping to a single worker with an explicit heap limit fixed it.
Accomplishments that I'm proud of
Privacy that is architectural, not promised: There is no MediaRecorder anywhere in the codebase, no image column in the database, and the server actively rejects any string smuggled into a stored feature payload, verified in production. The app draws your room back to you from the numbers alone, with no photo underneath, which turns the privacy claim into something you can see rather than something you are told.
The refusal to invent numbers: Most scoring tools quietly impute a value for missing inputs, because a partial score looks broken. Room to Mind drops the factor, marks it unscored, and computes only over what it actually measured, right down to the radar rendering gaps instead of zeros.
A score you are allowed to argue with: Every weight is a slider. Every number shows its arithmetic. Every recommendation quotes the measurement behind it. In a category full of tools that fake confidence, building one you can interrogate line by line is the result I wanted.
The room diagram: Rebuilding a recognizable room from a numeric feature vector, with object boxes, a glow at the light source, and a line from screen to light, is a genuinely hard thing to get right and it is the single most persuasive visual proof that the photo is gone.
It is real software: 122 unit tests across 13 files, a ridge regression written by hand with per-coefficient standard errors, and in-browser inference fitted into a 512MB build. Deployed, live, and working on a phone.
What we learned
A number you cannot interrogate is worth nothing: This was the realisation that reshaped the entire product. Early versions produced a clean, confident score, and confidence is exactly what makes a bad measurement dangerous. The version worth shipping is the one that shows its arithmetic, exposes its priors as adjustable sliders, and is willing to say "this factor was not scored, because nothing was detected." Legibility turned out to be a harder engineering problem than accuracy, and a more valuable one.
Silent failures are the ones that cost you: The lightweight detector read a laptop at 0.27 confidence against a 0.4 threshold. It did not crash, log, or warn. It quietly blanked two of six factors and returned a score that looked perfectly healthy. Finding it meant building a side-by-side harness across backbones and resolutions rather than trusting the output. The lesson generalises past this project: a detection threshold is not a tuning parameter, it is the line between a measurement and a guess, and everything downstream inherits whichever side you land on.
Tests prove the code runs. Only real output proves it is right: Both of my worst bugs passed every test I had written. The app was classifying chairs and plants as clutter and instructing people to clear away their own furniture. The sound brightness reading was being dragged toward zero by silent frames until it became a loudness-weighted average. Neither was a logic error, both were correctness errors, and neither was visible until I read what the app actually said to a human being.
Constraints are a design tool, not an obstacle: Refusing to send anything to a server sounded like a limitation and became the product's greatest strength. It forced every measurement into the browser, which made the app faster, free to run at scale, functional offline, and honest by construction rather than by promise. The 512MB build ceiling worked the same way, pushing me toward optimisations I would never have bothered with otherwise.
Restraint is a feature, and it is harder to build than capability: Almost every decision that improved this project was a decision not to claim something: not to impute missing factors, not to assert causation, not to keep the photo, not to fake a trained model. Building something that says less than it could, and says it accurately, took more engineering than building something that says everything confidently.
What's next for RoomtoMind
The immediate next step is calibration data. The collection tool is live, twenty-seven rooms in, and every response teaches Room to Mind what a room actually costs someone's attention, replacing the provisional hand-set thresholds with values fitted to real people. The regression needs thirty responses before it will fit at all, a deliberate floor, since six predictors on fewer than that would be too thin to trust. Past it, the score stops being a reasonable prior and starts being grounded in evidence.
From there, the real ambition is per-user learning. Room to Mind is already built to run n-of-1 experiments, so instead of claiming clutter is bad in general it can learn what actually costs you: that you work fine in a cluttered room but lose an afternoon to overhead glare, or that lighting matters to you far more than the default weighting assumes. The recapture loop, which compares the predicted improvement against the actual one, is the first step toward that, and it also lets the app report its own prediction error out loud, which very few tools are willing to do.
On the sensing side, a YAMNet upgrade would let the acoustic channel name what it hears, speech, traffic, keyboard, music, rather than only characterising it, making the feedback far more specific.
On the recovery side, the next step is working with concussion recovery groups to pressure-test whether the Recovery weighting matches what genuinely helps people, rather than what the guidelines suggest should.
And with real users over time, the honest question underneath the whole project: does measured environmental load actually track how well people report being able to think? Room to Mind is designed to find out, one person at a time, without ever claiming more than the data shows.
Evidence basis
Recovery Mode is grounded in current international concussion guidance rather than assumption.
Light sensitivity and noise sensitivity are recognised symptoms of concussion and persisting post-concussive symptoms in the Consensus Statement on Concussion in Sport from the 6th International Conference (Amsterdam, 2023), published in the British Journal of Sports Medicine, and in the Living Concussion Guidelines for the management of persisting symptoms. Both frame environmental and activity modification as part of managing symptom burden during recovery, alongside a graded, sub-symptom-threshold return to activity rather than complete rest.
Recovery Mode's reweighting follows from that. Lighting and acoustic load rise to 0.30 each and screen positioning to 0.20, because those are the environmental inputs that provoke the cardinal symptoms, while visual clutter drops to 0.10 because an untidy room is not what limits a photophobic user. Antara follows the same principle: low-stimulation practices, stopped the moment symptoms increase.
Room to Mind is a hackathon prototype. It does not diagnose, does not treat, and does not replace professional care. Anyone with persisting symptoms after a head injury should consult a clinician.
Why these factors, for this user
Two design decisions carry the neuroscience.
The acoustic channel reports composition rather than volume. A steady fan and an intermittent conversation can register the same loudness while loading attention completely differently, because intermittent speech-band sound recruits attentional resources involuntarily in a way steady broadband sound does not. Recovery Mode surfaces sound intermittency as its own reading for exactly this reason: what disrupts a recovering user is the pattern of the sound, not its level.
The lighting factor reads contrast range and glare rather than average brightness, and Recovery Mode surfaces glare range separately. Photophobia after concussion is provoked by harsh contrast and glare specifically, which is why a room can be objectively dim and still be costly. Averaging brightness would hide exactly the thing that matters.
This is why the score changes between modes. The same room might read 34 in Standard and 45 in Recovery. The Recovery number is what that room costs someone whose environment matters far more, and both numbers show their full arithmetic.
Built With
- claude
- cocossd
- css
- git
- github
- nextjs
- postgresql
- prisma
- react
- render
- ridge-regression
- tailwind
- tensorflow
- typescript
- vitest
- webaudioapi
Log in or sign up for Devpost to join the conversation.