Project Info
Inspiration
I spend a large part of my time learning, coding, experimenting, and building projects. My goal is to become a software engineer and eventually specialize in intelligent physical systems and robotics. I also live with a significant physical disability, which creates practical challenges in my daily life. That has strongly influenced the way I see technology. Code and algorithms exist to solve problems, so whenever I face a difficulty, my instinct is often to imagine a system that could remove it, simplify it, or make it more accessible. I enjoy solving my own problems, but I also love building things that make other people’s lives easier. Seeing someone smile because something I created helped them is one of the main reasons I enjoy engineering. Muse started with my sister. While preparing to move into a new apartment, she saw an Instagram video of a woman using a wardrobe-related application that her husband had built for her. The video only showed the concept briefly, but my sister sent it to me and said, “You could build something like this.” At first, it remained only an idea. Later that same evening, I discovered OpenAI Build Week and learned that participants would use Codex and GPT-5.6 to build a complete project. I immediately remembered my sister’s suggestion and decided to combine something useful for someone I love with something I genuinely enjoy doing. I did not want to build only a clothing gallery or a simple list of outfits. I imagined a dedicated physical device mounted near a wardrobe, acting as its digital counterpart. The idea also connected directly to my own life. A nurse helps me get ready every morning, and depending on the situation, selecting and retrieving clothes can require assistance. With Muse, I can prepare an outfit in advance and simply show the exact garments that should be taken from the wardrobe. It also helps with another frustrating problem: clothes can get misplaced. A digital wardrobe creates a persistent visual record of what I own, even when the physical item is temporarily difficult to find. What started as an idea for my sister became a product that could help both of us, while also being useful to many other people.
What it does
Muse is a local-first smart wardrobe running on a dedicated Raspberry Pi 5 with a touchscreen. It allows users to create a visual digital version of their physical wardrobe. Garments can be added, organized, viewed, and used to build complete outfits. The main features include: cataloguing garments; adding clothing from a phone through a temporary QR code; browsing a visual wardrobe; creating outfits with a drag-and-drop builder; positioning, resizing, and layering garments; saving completed outfits; reopening saved outfits later; managing local data, backups, storage, and device information. The phone upload workflow happens over the local network. Muse displays a temporary QR code, the user scans it with a phone, selects a garment photo, enters its information, and uploads it directly to the Raspberry Pi. The new garment then appears inside Muse. Muse is designed as a dedicated appliance rather than an ordinary website opened in a browser. When the Raspberry Pi starts, Muse launches automatically into a full-screen touchscreen interface. The core wardrobe data is stored locally in SQLite on the device. The main experience does not require a permanent cloud account or remote application server. For someone receiving help while getting dressed, Muse can also act as a clear visual reference. A saved outfit shows exactly which garments should be retrieved, reducing uncertainty and making communication easier.
How we built it
I began by sketching the product, defining the user experience, and deciding what a dedicated wardrobe device should feel like. I then selected the hardware, assembled the Raspberry Pi 5, installed its cooling system, connected the touchscreen, prepared the operating system, and tested the physical components. Muse uses a React and TypeScript frontend, a FastAPI backend, and a SQLite database. The Raspberry Pi runs the application through systemd services. Chromium is configured as a full-screen touchscreen kiosk. The main API listens only on the local loopback interface, while a separate restricted listener handles temporary phone uploads over the local network. The project also includes: database migrations with Alembic; automated backups and backup verification; atomic release directories; deployment and activation scripts; service hardening; health checks; frontend and backend tests; production browser tests; Linux validation through GitHub Actions. I used Codex throughout the project for architecture, implementation, code review, testing, debugging, deployment, and documentation. GPT-5.6 helped reason across the different layers of the system, including the interface, backend, networking, Chromium, Wayland, systemd, and the physical Raspberry Pi environment. This was not a one-prompt generation process. I defined the product vision, created the sketches, selected and assembled the hardware, made the design decisions, tested the application on the real device, reproduced failures, inspected the results, and decided what should be changed. The development process was highly iterative. I tested Muse physically, reported exactly what happened, collected terminal output and logs, and then used Codex to investigate and implement tested corrections. The final repository contains hundreds of automated tests covering the backend, frontend, outfit builder, phone uploads, backups, database migrations, deployment tooling, kiosk startup, and production workflows.
Challenges we ran into
Almost every part of Muse was new to me. Before this project, I had mainly worked with software. I had never built a complete physical product using a Raspberry Pi and a bare touchscreen. I had to learn how to install the operating system, assemble the Raspberry Pi, position thermal pads, install the cooling system, connect power, connect video, connect the touchscreen controller, and verify that every component behaved correctly. The touchscreen arrived with its electronics exposed at the back, without a protective enclosure. That made even simple actions feel important because every connector, board, and cable had to be handled carefully. I had assembled a desktop computer before, but this was very different. Desktop hardware usually has standardized mounting points and clearer layouts. With the Raspberry Pi, I was looking at exposed chips and thermal pads of different sizes while trying to understand exactly where everything belonged. The software deployment introduced an entirely different set of challenges. I had used Linux before, but I had never managed a Linux device remotely from a Mac through SSH at this level. Muse had to coordinate: Linux users and permissions; systemd dependencies; service sandboxing and hardening; Chromium kiosk startup; Wayland access; private runtime directories; local network discovery; QR upload sessions; database migrations; backups; release activation; automatic startup after reboot. Several issues appeared only on the real Raspberry Pi. Chromium initially failed because of Crashpad, keyring behavior, private runtime directories, and restricted access to the Wayland session. The QR upload system later failed because the backend used socket.if_nameindex() to discover local network interfaces, but the service sandbox did not allow AF_NETLINK. The most important failure appeared during a real cold boot. Muse depended on /run/muse, but /run is volatile and is cleared during reboot. The directory had previously been created only during installation. On the next boot, systemd attempted to build a protected mount namespace around a path that no longer existed, causing 226/NAMESPACE. The preparation service failed, and every dependent Muse service failed with it. The earlier automated validation had not reproduced that exact situation because some tests created the directory in advance, while previous physical checks had been warm activations directly after installation. We corrected the architecture by adding a proper systemd tmpfiles configuration that recreates /run/muse during boot with the required root:muse ownership and 0750 permissions. We also added regression coverage that begins with the directory absent and verifies its creation in a clean Linux environment. After deploying the corrected release, removing all temporary overrides, and performing a genuine cold boot, the Raspberry Pi started normally and Muse launched automatically with no Muse errors in the current boot journal.
Accomplishments we're proud of
The accomplishment I am most proud of is that Muse became a real physical product, not only a prototype running on my development computer. This was my first time building with a Raspberry Pi, my first time using a bare touchscreen, and my first complete physical computing project. Muse now: boots automatically into its touchscreen interface; survives a complete cold reboot; preserves garments and saved outfits across deployments; accepts garment uploads from a phone through a QR code; keeps the main API restricted to the device; stores its core data locally; creates and verifies backups; runs through hardened Linux services; passes hundreds of automated tests; has a reproducible release and deployment process. I am also proud that we did not stop at temporary fixes. When physical testing revealed problems, we investigated the real cause and integrated permanent corrections into the repository. For example, the final cold-boot issue could have been hidden by manually recreating /run/muse each time. Instead, we corrected the system architecture, added a proper boot-time producer for the directory, wrote regression tests, rebuilt the release, deployed it, removed the temporary overrides, and verified the result through a real reboot. I am equally proud that my original sketches and product decisions remained central throughout the process. Codex helped me move much faster, but Muse still reflects the experience I wanted to create and the real-life problems I wanted to solve. Building something useful for my sister, while also creating something that can help me in my own daily routine, made the project personally meaningful.
What we learned
Muse taught me that building a real product is very different from making an interface work once on a development computer. A product must survive installation, permissions, service failures, networking conditions, database changes, deployments, and complete reboots. It must protect user data and recover predictably. I learned how hardware and software affect each other. A configuration that looks correct in source code can still fail because of the physical display session, the Linux boot sequence, a missing volatile directory, or a restricted system capability. I gained practical experience with: Raspberry Pi hardware; touchscreen integration; Linux administration; SSH; systemd; Chromium kiosk applications; Wayland; local networking; FastAPI; React and TypeScript; SQLite and Alembic; deployment automation; release packaging; backups; service hardening; browser and integration testing; production debugging. I also learned how valuable AI-assisted engineering can be when it is used as a collaboration rather than as a replacement for human decisions. Codex could analyze logs, inspect code, write tests, and propose fixes, but the real device still required observation and judgment from me. The AI could not physically see whether the touchscreen displayed the correct interface, whether a cable was connected properly, whether Muse appeared after reboot, or whether the experience felt right to use. That collaboration allowed me to attempt something far beyond my previous hardware experience without approaching it completely alone. One of my secondary-school teachers used LaTeX to create our lessons, gave me some of the first programming books I ever read, and told me that I could go far with code. Completing my first working physical product made me think about those words again.
What's next
The Build Week version of Muse focuses on creating a reliable local-first digital wardrobe, importing garments from a phone, and visually building and saving outfits. The next major feature I want to add is weather-aware outfit assistance. Muse could display the current local temperature and suggest saved outfits that are appropriate for the weather. While creating an outfit, it could also provide small practical notes, such as warning that a top may be too light for the expected temperature or suggesting an additional layer. The home screen could adapt visually to the season, with different atmospheres for winter, spring, summer, and autumn. Other future improvements include: improved garment background removal; better body and outfit visualization; automatic software updates with rollback; multiple Muse devices and user profiles; improved Wi-Fi and display controls; custom enclosures; a dedicated printed circuit board; synchronization options that preserve the local-first architecture; support for households and families; smarter outfit recommendations based on context, season, weather, and personal preferences. In the longer term, Muse could move from answering “What is in my wardrobe?” to answering “What should I wear today?” Muse is my first physical computing product, but it will not be my last.
Muse
Muse is a local-first smart wardrobe for a dedicated Raspberry Pi 5 touchscreen. Clothing, outfits, image processing, preferences, and storage remain on the device. No cloud account, subscription, hosted service, or paid API is mandatory; phone upload uses only the trusted local network. Optional Internet connectivity may later be used for explicitly initiated software updates.
The current implementation includes the complete garment, phone-import, outfit,
and product-experience slices: secure local streaming import, short-lived QR
handoff, exact-original preservation, safe local derivatives, SQLite
persistence, Wardrobe and Clothing Details, the manual Outfit Builder,
deterministic local preview generation, the approved Saved Outfits grid,
readiness-aware Splash, typed Settings, local backups, capability-aware device
information, and the complete Raspberry Pi release/deployment architecture. On
July 17, 2026, the application was physically exercised on a Raspberry Pi 5 with
8 GB RAM, Raspberry Pi OS, labwc/Wayland, and the intended 1280 × 800
touchscreen. The touchscreen UI, Wardrobe, Details, QR phone upload, Outfit
Builder, Saved Outfits, Settings, local-network status, persistence, and backups
all operated on that device. Each immutable release still requires its own clean
deployment and cold-boot acceptance record.
MVP principles
- Local-first data and processing
- Touch-first at
1280 × 800 - User-controlled outfit selection
- No mandatory paid API or hosted service
- Reliable Raspberry Pi kiosk operation
- Approved mockups and design-system documentation as the visual source of truth
See docs/mvp-scope.md for product scope and docs/architecture.md for the runtime and persistence design. Production deployment is documented in docs/raspberry-pi-deployment.md, with the physical sequence in docs/raspberry-pi-operator-runbook.md.
OpenAI Build Week
Muse was designed and implemented during OpenAI Build Week with Codex as the primary engineering collaborator. Codex helped audit product documents and mockups, translate the approved scope into phased architecture, implement and test the React/FastAPI/SQLite system, review security boundaries, build release tooling, and diagnose failures observed on the physical Raspberry Pi. GPT-5.6 Sol powered the primary Codex implementation sessions. The repository records the material assisted work and verification evidence in docs/codex-build-log.md.
The human retained the product and release decisions: MVP scope, approved visual direction, the choice to keep garment selection manual and storage local, the two-listener phone-upload boundary, the Raspberry Pi hardware and network setup, which trade-offs were acceptable, and whether physical behavior passed the demo gate. Codex did not replace hardware observation, operator authentication, or the final release decision. Muse has no runtime dependency on Codex, GPT-5.6 Sol, another OpenAI model, or an OpenAI API.
The reproducible entry points are:
| Goal | Command or section |
|---|---|
| Install development dependencies | Follow First-time setup with uv sync --locked --all-groups and npm ci |
| Run locally | Start muse-backend migrate, muse-backend serve --reload, then npm run dev as documented in Local development |
| Run the complete verification suite | Use the backend, frontend, browser, kiosk, and release commands in Verification |
| Build an immutable Pi release | Run ./kiosk/build-release.sh from a clean synchronized commit |
| Deploy and validate the Pi | Follow the operator runbook, beginning with a verified production backup |
Repository structure
assets/ Approved design references, brand assets, icons, and media
backend/ FastAPI API, SQLite migrations, persistence, and local storage
docs/ Product, interface, architecture, and roadmap documentation
frontend/ React, TypeScript, and Vite touchscreen application
kiosk/ Raspberry Pi startup, deployment, and kiosk configuration
Supported toolchains
- Python
3.13.x(constrained to>=3.13,<3.14) - uv
0.11.28 - Node.js
24.18.0 - npm
11.16.0
Python 3.13 is available for current 64-bit Raspberry Pi OS and gives Muse a
single reproducible development and ARM64 production target. The exact Python
minor and uv version are recorded in backend/.python-version and
backend/pyproject.toml; Python dependencies are frozen in backend/uv.lock.
Node and npm requirements are recorded in .nvmrc, frontend/package.json, and
frontend/package-lock.json.
Install uv 0.11.28 and nvm, or provide the same
toolchain versions another way. No globally installed Python packages are used.
On macOS, if Python reports that hidden .pth files inside .venv were skipped,
set export UV_PROJECT_ENVIRONMENT=venv before uv sync and in each new shell
that runs backend commands. The alternate backend/venv/ directory is already
gitignored; Linux and Raspberry Pi environments can use uv's normal .venv.
First-time setup
From the repository root:
cd backend
uv sync --locked --all-groups
cd ../frontend
nvm install
nvm use
npm ci
npx playwright install chromium
On Debian, Ubuntu, or Raspberry Pi OS development images, Playwright's browser and operating-system dependencies can be installed with:
cd frontend
npx playwright install --with-deps chromium
Playwright is a development/CI dependency only. The deployed kiosk uses the system Chromium package.
Local development
Copy the documented backend settings only when you need local overrides; the checked-in defaults already work for development:
cp backend/.env.example backend/.env
Start the backend before the frontend. In terminal 1:
cd backend
uv run muse-backend migrate
uv run muse-backend serve --reload
In terminal 2:
cd frontend
npm run dev
npm run dev:mobile and npm run preview:mobile are available for isolated
responsive styling work, but they do not replace the token-authorized LAN
listener. Use the two-process procedure below for a real phone upload.
The ordinary Vite workflow exercises the loopback device application. To test
the real restricted phone surface, build both frontends and give the main and
restricted processes the same disposable configuration. Set these values in
the untracked backend/.env before starting either process:
MUSE_DATA_ROOT=/tmp/muse-phone-upload-dev
MUSE_PHONE_UPLOAD_ENABLED=true
MUSE_PHONE_UPLOAD_BIND_HOST=127.0.0.1
MUSE_PHONE_UPLOAD_TRUSTED_HOSTS=["127.0.0.1","localhost"]
MUSE_PHONE_UPLOAD_FRONTEND_BUILD_PATH=../frontend/dist-phone
Then build, migrate the disposable database, restart the main process in terminal 1, and start the restricted process in terminal 3:
cd frontend
npm run build
cd ../backend
uv run muse-backend migrate
uv run muse-backend serve --reload
# Run from backend/ in another terminal with the same backend/.env.
uv run muse-backend serve-phone-upload
When testing from a separate phone, set both bind and advertised IPv4 to the Pi
or development machine's same exact private LAN address, add it to the phone
trusted-host list, and keep the main server on 127.0.0.1. A loopback listener
deliberately cannot advertise a LAN hostname or address.
Open http://127.0.0.1:5173. Vite proxies relative /api requests to
http://127.0.0.1:8000, so frontend code follows the same-origin production
contract and never embeds a production hostname. Override only the development
proxy target with MUSE_DEV_API_ORIGIN in frontend/.env.
Useful local endpoints:
- Backend health:
http://127.0.0.1:8000/api/v1/health - Backend readiness:
http://127.0.0.1:8000/api/v1/readiness - Restricted-listener status when enabled:
http://127.0.0.1:8787/listener-status - OpenAPI UI:
http://127.0.0.1:8000/api/docs - OpenAPI JSON:
http://127.0.0.1:8000/api/openapi.json
The frontend shows its backend diagnostic automatically in development, or when
?diagnostics=1 is supplied explicitly. A failed health request produces a
visible unavailable state rather than a blank application.
Database and local data
Development defaults to the gitignored local-data/ directory at the repository
root:
local-data/
muse.sqlite3
media/
garments/original/
garments/processed/
garments/thumbnails/
garments/cutouts/
outfits/previews/
tmp/uploads/
tmp/previews/
.locks/
backups/
SQLite rows store relative media references; bytes remain on the filesystem.
Production and test settings reject writable data inside the source tree. Set
MUSE_DATA_ROOT to a durable external directory, for example /var/lib/muse,
in production. Configuration also prevents database, temporary, backup, public
media, and frontend-build paths from overlapping. Runtime directories and newly
promoted media are owner-only.
Metadata API bodies default to a 64 KiB limit. Garment import has a separate 25 MiB image limit plus bounded multipart overhead and writes upload chunks only inside the configured temporary root.
Garment import and local images
Open Wardrobe and choose Add Garment, or navigate directly to
/wardrobe/add, to choose the import method. /wardrobe/add/device opens the
local form; /wardrobe/add/phone opens the QR session view. Both methods accept
one JPEG, PNG, or WebP source photograph with validated garment metadata through
the same importer. The server verifies the file signature, declared MIME type,
filename suffix, dimensions, pixel count, frame count, color mode, complete
decode, EXIF orientation, and bounded color-profile data.
An acknowledged import has already stored:
- the exact, never-overwritten original bytes;
- a browser-safe normalized WebP with a maximum
1600 pxside; and - a WebP thumbnail with a maximum
384 pxside.
The defaults reject images above 24 megapixels, a 12000 px side, or 25 MiB.
All limits and derivative dimensions are configurable through documented
MUSE_ settings. Derivatives contain no source EXIF or other unnecessary public
metadata.
Optional background cleanup runs through one bounded local worker after the
core import succeeds. The shipped Pillow processor preserves meaningful source
transparency and can remove a highly uniform, border-connected background when
its conservative confidence checks pass. Otherwise the garment records a
truthful completed_with_fallback state and continues using its normalized
image. Muse does not download an ML model or require the Internet. Display
selection is cutout, normalized, then original; grids prefer thumbnails.
Import attempts use backend-owned temporary directories and durable manifests to coordinate atomic file promotion with the SQLite transaction. Startup reconciliation preserves committed media, compensates interrupted uncommitted promotions, clears stale temporary attempts, and resumes interrupted optional processing. Soft deletion does not remove any image bytes.
Import from a phone
Add Garment offers Upload on this device and Upload from phone. The second option creates a persistent, short-lived session and displays a locally generated QR code, readable fallback URL, expiry countdown, cancel, and regenerate controls. A phone on the same trusted network opens the dedicated responsive Muse page, previews or replaces a JPEG, PNG, or WebP photograph, enters garment metadata, and uploads through the same secure streaming importer as local-device import. Muse then refreshes Wardrobe and opens the committed garment automatically.
Phone import uses two server processes. The complete SPA and /api/v1 API stay
on 127.0.0.1; a restricted listener binds one configured LAN interface and
serves only the mobile build, safe session status, and one authorized upload.
It exposes no clothing/outfit/Settings CRUD, media browser, readiness details,
OpenAPI, filesystem paths, or privileged actions. CORS is not used as
authentication, and no wildcard origin is enabled.
Before creating or regenerating a session, the loopback API probes the
restricted listener's exact configured bind address at /listener-status with
a 500 ms timeout. The probe follows no redirect, performs no DNS lookup, sends
no token, and requires the exact minimal JSON response. If the listener or its
compiled mobile build is unavailable, creation fails safely before a token row
is issued. Each active device status response repeats this bounded probe so the
screen's network state describes listener reachability, not merely successful
SQLite session creation. The listener revalidates its bounded Vite manifest and
every allow-listed mobile asset for each readiness response, so deleting or
corrupting a deployed build fails closed even after process startup.
The kiosk uses versioned loopback routes under
/api/v1/phone-upload-sessions. The LAN surface is limited to the safe
/listener-status readiness response, /u/, locally compiled
/phone-assets/*, GET /phone-api/v1/session, and
POST /phone-api/v1/upload. Requests for the main /api/v1 tree through the
LAN listener remain unavailable.
Each token has at least 256 bits of entropy. SQLite stores only its SHA-256
digest. The QR URL places the raw token in the URL fragment, which is not sent
in the HTTP request target or Referrer. Mobile code removes the fragment from
visible history, retains the validated value only in origin-scoped
sessionStorage for refresh recovery, and sends it as the
X-Muse-Upload-Token authorization header. Terminal states clear it. The
listener disables access logging, and ordinary device status never returns the
secret. Completion, cancellation, expiry, or regeneration makes the token
unusable. A safe failed attempt may be retried with the same token only while
the server explicitly reports it retryable and the attempt and expiry limits
remain. Transactional session
claiming plus the existing import idempotency contract guarantees at most one
committed garment per successful session. On restart, Muse checks that stable
key before trusting a stale session state: if a garment already committed, even
a concurrent visible failure, cancellation, or expiry is reconciled to
completed instead of deleting or duplicating the garment.
Session creation defaults to a ten-minute lifetime. Startup drains session
recovery in repeated transactions of at most 100 mutations so every stale row
is handled without an unbounded transaction. Each periodic or operator cleanup
pass shares one 100-record budget across committed-import recovery, expiry,
interrupted-claim recovery, terminal-row deletion, and abandoned temporary
attempts. The main process checks at most every 300 seconds by default, and the
restricted listener also removes one bounded stale-attempt batch when it
starts. A completed, cancelled, or expired row is deleted only after it
has remained unchanged for 24 hours. An uncommitted failed row first expires;
no cleanup path removes a committed garment or registered image. Import,
best-effort cutout processing, startup reconciliation, and cleanup all use the
same cross-process gate, preventing cleanup from racing active temporary files
while also bounding concurrent Pillow work.
MUSE_PHONE_UPLOAD_CLEANUP_BATCH_SIZE bounds one cleanup pass. The listener's
in-memory abuse guard is configured by
MUSE_PHONE_UPLOAD_RATE_LIMIT_REQUESTS,
MUSE_PHONE_UPLOAD_RATE_LIMIT_WINDOW_SECONDS, and
MUSE_PHONE_UPLOAD_RATE_LIMIT_CLIENTS; it is not authentication and stores no
persistent client profile. Session authorization remains the token.
Muse supports JPEG, PNG, and WebP from phones. HEIC and HEIF are rejected with an actionable message rather than renamed or decoded partially. Although a third-party HEIF plugin publishes Python 3.13 Linux AArch64 artifacts, its complete Raspberry Pi resource, codec, and licensing profile has not passed target-hardware acceptance. The mobile camera picker requests a compatible representation where the browser supports it, but Muse does not claim that an iPhone will always convert an existing HEIC library photograph.
The advertised URL may use a configured muse.local hostname when Raspberry Pi
OS and the phone already have working mDNS, but this milestone does not install
or reconfigure the device's mDNS service. The direct private IPv4 address is the
deterministic fallback. Muse requires neither public DNS, an Internet tunnel, a
cloud relay, nor an external QR service. The HTTP token is protected against
guessing and replay but is not confidential from a malicious observer on the
LAN; phone upload therefore assumes a trusted local network.
Apply committed migrations and inspect their state with:
cd backend
uv run muse-backend migrate
uv run muse-backend migration-status
uv run muse-backend migration-check
Normal startup never calls create_all() and never silently applies schema
changes. Run migrations before starting the service. Migration bootstrap creates
the configured storage tree with owner-only directory permissions and keeps the
SQLite file owner-readable/writable only.
For development only, reset the configured SQLite database and recreate it at the migration head with:
cd backend
uv run muse-backend reset-dev --confirm
The command refuses non-development environments and requires the explicit
confirmation flag. It removes the database plus its WAL/SHM journals, but does
not delete media. Review MUSE_DATA_ROOT before running it.
Clothing and outfits are soft-deleted. Normal queries exclude deleted records, while an existing saved outfit remains valid when one of its garments is deleted, including its primary-image reference. Soft deletion never silently removes garment files.
The Clothing collection accepts an optional garment_category query parameter
for the approved Wardrobe category navigation. Search, arbitrary filters, and
favorites are intentionally absent.
Outfit Builder and Saved Outfits
Open /outfit-builder to create an outfit, or pass an existing identifier as
/outfit-builder?outfitId={id}. The editor can add and remove garments, place
them directly on the workspace, move/resize/rotate them with touch-friendly
commands, reorder layers, reset transforms, and keep several distinct garments
in one body zone. Adding the same garment again activates its existing
placement rather than creating an accidental duplicate.
The browser and backend share one placement contract:
- a logical
640 × 800workspace; - normalized garment-center
xandycoordinates with a top-left origin; - one proportional scale value;
- clockwise rotation around the garment center; and
- deterministic back-to-front layer ordering.
The editor draft lives in one reducer-backed session separate from TanStack
Query server state. A versioned, validated, size-bounded sessionStorage record
recovers the draft after a browser reload. API save failures preserve the local
draft. Existing outfits support update, save as new, restore saved state, and
confirmed soft deletion.
Create and placement-changing update requests render a deterministic local
600 × 750 lossless WebP. Pillow tries cutout, normalized, then original
garment media and substitutes a neutral placeholder only when every candidate
is unusable. A private staging directory and durable manifest coordinate atomic
promotion with the SQLite transaction. Name-only or unchanged-placement updates
reuse the existing preview; failed work preserves the previous row and preview.
Successful replacement removes the superseded unregistered preview, with
startup reconciliation retrying deferred cleanup. Soft-deleted outfit previews
are retained until Muse has an explicit permanent-retention policy.
/saved-outfits displays the approved three-column grid at 1280 × 800,
newest-updated first. Cards use the generated preview, fall back safely when it
is missing, and reopen the exact outfit in the Builder. Grid scroll position is
preserved within the browser session. Long-press/fullscreen preview and exact
duplicate-outfit detection are optional extensions, not current MVP behavior.
Local saved-outfit endpoints are:
POST /api/v1/outfitsGET /api/v1/outfitsGET /api/v1/outfits/{id}PATCH /api/v1/outfits/{id}DELETE /api/v1/outfits/{id}
An Apple M4 development benchmark rendered 20 placements of one synthetic
800 × 1200 WebP with a 0.2334 s median and 0.2383 s maximum across five
warmed runs; the output was 40,034 bytes. This is non-Pi regression evidence.
Raspberry Pi 5 latency, memory, thermal, touch, storage, and interruption checks
remain required by
docs/raspberry-pi-validation.md.
Splash, Settings, and local data maintenance
Muse starts through a local CSS/SVG Splash sequence coordinated with
GET /api/v1/readiness. Readiness may complete before the designed sequence; if
it takes longer, Muse holds the final wordmark and retries at a bounded rate.
Persistent failure retains a branded Retry state instead of exposing a backend
trace. The full sequence plays once per cold browser session, does not replay
during internal navigation, and has a restrained Reduced Motion path.
/settings follows the approved five-card layout:
- W & N reports safe local-network and restricted-listener status. It does not manage Wi-Fi credentials in P6.
- Display persists interface dimming, screen timeout, Reduced Motion, and Splash mode. Dimming is an application overlay, not hardware backlight control; the sleep overlay preserves the active route.
- Data reports local storage, creates and lists local backups, downloads or deletes a selected backup, cleans bounded temporary data, and stages restore or delete-all maintenance.
- Device shows sanitized local information and explicit capability states. Restart, reboot, and shutdown are available only when P7's constrained helper passes ownership, permission, and authorization checks. Wi-Fi management and hardware brightness remain unavailable.
- About Muse describes the local-first privacy model, license, repository, and Build Week context using only bundled content.
Settings use the main loopback API only. The restricted phone listener mounts no Settings, backup, device, maintenance, media, health, documentation, or main SPA route. Settings mutation requests must be JSON and pass the extra origin check.
Backups are private *.muse-backup.zip archives below the configured data root.
Each contains a SQLite online snapshot, only media referenced by that snapshot,
and a closed versioned manifest with sizes and SHA-256 checksums. Operational
phone-upload sessions, nested backups, temporary files, logs, caches,
environment files, and secrets are excluded. Restore validates the archive and
stages replacement data but returns staged_restart_required; it never swaps a
database used by a running listener.
Apply staged maintenance only after both Muse listeners are stopped:
cd backend
uv run muse-backend apply-staged-maintenance \
--confirm "APPLY STAGED MUSE MAINTENANCE"
The command obtains an exclusive runtime lease and refuses to run while either listener holds its shared lease. Restore keeps the pre-operation safety backup. Delete-all requires the UI's two confirmations, the exact typed phrase, and explicit backup-loss acknowledgement; activation recreates the migrated empty database and required local directories without touching application code. P7 systemd units coordinate the stop/apply/migrate/start sequence on the physical device. Activation remains an explicit operator workflow.
Verification
Run the backend checks:
cd backend
uv run ruff format --check .
uv run ruff check .
uv run mypy src tests
uv run pytest
uv run muse-backend migration-check
Run a focused backend group without applying the whole-suite coverage threshold
with uv run pytest -m unit --no-cov or
uv run pytest -m integration --no-cov. Apply formatting with
uv run ruff format ..
Run the frontend checks:
cd frontend
npm run typecheck
npm run lint
npm run format:check
npm run test
npm run build
npm run test:e2e
Apply frontend formatting with npm run format. The Playwright shell suite uses
Chromium at the target 1280 × 800 viewport.
The ordinary and P4 production browser suites target disposable FastAPI
processes prepared as described below. P6 is different: its harness migrates,
starts, stops, and restarts both listeners itself, so ports 8000 and 8787
must be free before running the P6 command.
cd frontend
PLAYWRIGHT_BASE_URL=http://127.0.0.1:8000 npm run test:e2e:production
PLAYWRIGHT_BASE_URL=http://127.0.0.1:8000 \
PLAYWRIGHT_PHONE_UPLOAD_BASE_URL=http://127.0.0.1:8787 \
MUSE_BACKEND_EXECUTABLE=/absolute/path/to/backend/.venv/bin/muse-backend \
MUSE_MAIN_PID_FILE=/tmp/muse-phone-main.pid \
MUSE_PHONE_PID_FILE=/tmp/muse-phone-upload.pid \
MUSE_PHONE_E2E_DATA_ROOT=/tmp/muse-phone-e2e \
npm run test:e2e:production:p4
MUSE_P6_RUNTIME_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/muse-p6-runtime.XXXXXX")"
MUSE_P6_DATA_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/muse-p6-data.XXXXXX")"
chmod 700 "$MUSE_P6_RUNTIME_ROOT" "$MUSE_P6_DATA_ROOT"
PLAYWRIGHT_BASE_URL=http://127.0.0.1:8000 \
PLAYWRIGHT_PHONE_UPLOAD_BASE_URL=http://127.0.0.1:8787 \
MUSE_BACKEND_EXECUTABLE=/absolute/path/to/backend/.venv/bin/muse-backend \
MUSE_P6_E2E_RUNTIME_ROOT="$MUSE_P6_RUNTIME_ROOT" \
MUSE_P6_E2E_DATA_ROOT="$MUSE_P6_DATA_ROOT" \
npm run test:e2e:production:p6
The first command runs the local garment import/edit/delete flow and the P5
outfit flow. The P4 command additionally requires the restricted listener and
uses separate device and phone browser contexts to decode the QR, upload a real
image, observe automatic completion, restart both disposable test processes,
verify persistence, and reject replay after restart. CI supplies the backend
executable and isolated runtime paths to the Playwright harness. P6 creates a
private mode-0700 per-attempt runtime directory, retains the exact child
process handles it launches, refuses symlinked runtime files, and never signals
a PID read from a file. It relaunches the two documented CLI commands against
the same dedicated, initially empty temporary data root. It never reuses the
P1/P5 smoke database. Muse exposes no restart or privileged test endpoint. The
P5 scenario imports local garments, creates overlapping placements, transforms
and layers them, verifies the generated 600 × 750 preview and approved
three-column grid, reloads, updates, saves as new, deletes, and checks local-only
requests and 1280 × 800 horizontal overflow. To run only that check, use
npm run test:e2e:production:p5 with the same PLAYWRIGHT_BASE_URL. Do not
point any production E2E command at a personal wardrobe database.
The P6 scenario observes the real Splash/readiness transition, exercises all
Settings sections, persists Reduced Motion, creates and restores a backup,
applies staged restore and delete-all only after stopping both test listeners,
then verifies readiness, data integrity, reset behavior, LAN isolation, local
assets, touch targets, and 1280 × 800 overflow. It is intentionally
destructive and must use fresh private data and runtime roots.
The P4 PID files must already contain the actual disposable listener process IDs
and be writable by the test user; that harness rewrites them after restart. The
P6 harness owns its isolated attempt directories and exact child-process
handles, starts and stops both disposable listeners itself, and refuses
non-loopback targets. Its private PID files exist only for bounded CI crash
cleanup and are never trusted by Playwright as process authority. Use the
production executable path selected for the test environment (venv instead
of .venv on the documented macOS workaround). The CI workflow is the
canonical complete setup and creates owner-only PID and log files.
Production build and startup
Build the frontend on a development machine or in CI:
cd frontend
npm ci
npm run build
The build command emits the kiosk application to frontend/dist and the
restricted phone page to frontend/dist-phone. Copy both immutable build
outputs to the Pi; do not run Vite or install Node on the production device.
Build a complete checksummed Raspberry Pi release with:
./kiosk/build-release.sh
The allow-list builder validates both Vite manifests and every referenced asset
and writes the archive below gitignored release-output/. Validate an operator
command without contacting a device using:
./kiosk/deploy.sh --host muse.local --user kyle \
--release /path/to/muse-<release-id>.tar.gz --dry-run
Read the deployment architecture and operator runbook linked above before any physical installation. Prior physical success does not validate a newly built archive; its installed release ID, services, data preservation, and cold boot must be checked again.
The installer creates /etc/muse/muse.env from kiosk/muse.env.example only
when no production configuration exists. Private IPv4, advertised address, and
phone trusted hosts are generated atomically under /run/muse; operators do
not hardcode DHCP state. The main service always uses 127.0.0.1:8000, and the
restricted listener uses port 8787 on exactly one detected RFC1918 address.
Production invokes /opt/muse/current/kiosk/muse-backend, which directly uses
the active release's .venv/bin/python and immutable backend source. It never
uses uv run at boot and cannot install a development dependency group during
runtime. Use the fixed muse-ctl commands documented in the deployment guide
for readiness, listener isolation, cleanup, backups, logs, and lifecycle work.
The main FastAPI process serves the compiled SPA and API from the same loopback
origin. Direct navigation to React routes receives index.html; unknown
/api/* paths remain API 404s. The restricted listener serves only
dist-phone and its narrow upload contract. The backend keeps health
diagnostics available if the main frontend is missing, while readiness reports
the missing build. Normal Raspberry Pi runtime needs Python, the locked
environment, both precompiled frontend directories, SQLite data, and Chromium;
it does not need Node.js or Internet access.
Run the target-hardware acceptance procedure in docs/raspberry-pi-validation.md before calling a release Raspberry Pi validated. Development-machine timings and CI do not replace that hardware run.
Offline runtime assets
Inter and Playfair Display are bundled through Fontsource and emitted into the
Vite build. Muse does not request required fonts, CSS, icons, or graphical assets
from a CDN. Approved PNG mockups under assets/ui/mockups/ remain references and
are not shipped as application UI.
Analysis
View
Metric
- 1
Figures cover GitHub contributors during the hackathon window. A co-authored commit counts in full for each author, so per-member totals add up to more than the whole-team figures.
Technology
- CSSIn code
- FastAPIIn code
- HTMLIn code
- JavaScriptIn code
- PythonIn code
- ReactIn code
- TypeScriptIn code
7 of 7 appear in the indexed code.
AI coding agents
- CodexConfig
Detected from committed agent config files and commit authorship. Absence of a signal is not proof an agent was unused.
Codebase size
Source size
1.9 MB
Source files
294
Counts recognized source files only; vendored directories, binaries and lockfiles are excluded, so this is smaller than the repository on disk.
Repository
LazyWheelDev/Muse
338 files · 12.6 MB · @ 5f7506f
Structure
Interface
65 files · 19%Screens, components and styles rendered to the user.
API & routing
22 files · 7%Request entry points: routes, handlers and controllers.
Application logic
109 files · 32%Domain rules, services and shared utilities.
+4 moreData & schema
25 files · 7%Schema definitions, migrations and data access.
Supporting
Layers are inferred from where files sit in the tree, not from reading the code. A project that names its directories unconventionally will read oddly here — open the file browser to check anything the diagram implies.
Languages
- Python38%
- TypeScript36%
- Markdown18%
- CSS5%
- Shell2%
- YAML1%
- Other (2)0%
Share of indexed source by file size. Binary and vendored files are excluded.
Dependencies
frontend/package.json
npm · 32- @fontsource-variable/inter
- @fontsource-variable/playfair-display
- @tanstack/react-query
- lucide-react
- qrcode.react
- react
- react-dom
- react-router-dom
- +24 more
backend/pyproject.toml
pypi · 8- alembic
- fastapi
- pillow
- pydantic
- pydantic-settings
- python-multipart
- sqlalchemy
- uvicorn
Declared in the repository’s manifests at the indexed commit. A declared package is not proof it is used, and runtime dependencies are listed first.
This project’s features have not been analysed yet.
Export this project's context (description, README, evidence, key source files) to chat with an AI agent elsewhere.