{"id":49416322,"url":"https://github.com/parthalon025/framecast","last_synced_at":"2026-04-29T03:08:47.708Z","repository":{"id":345383098,"uuid":"1185675251","full_name":"parthalon025/framecast","owner":"parthalon025","description":"Turn any TV into a family photo frame. Raspberry Pi photo \u0026 video display with web uploads, photo map, WiFi hotspot, and 9.9/10 reliability.","archived":false,"fork":false,"pushed_at":"2026-03-28T20:51:10.000Z","size":958,"stargazers_count":0,"open_issues_count":12,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-28T22:18:48.255Z","etag":null,"topics":["digital-signage","flask","hdmi","iot","photo-frame","python","raspberry-pi","raspberry-pi-3","slideshow","vlc"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/parthalon025.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-18T20:41:35.000Z","updated_at":"2026-03-28T20:13:31.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/parthalon025/framecast","commit_stats":null,"previous_names":["parthalon025/framecast"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/parthalon025/framecast","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/parthalon025%2Fframecast","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/parthalon025%2Fframecast/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/parthalon025%2Fframecast/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/parthalon025%2Fframecast/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/parthalon025","download_url":"https://codeload.github.com/parthalon025/framecast/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/parthalon025%2Fframecast/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32408504,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-29T02:37:21.628Z","status":"ssl_error","status_checked_at":"2026-04-29T02:36:50.947Z","response_time":110,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["digital-signage","flask","hdmi","iot","photo-frame","python","raspberry-pi","raspberry-pi-3","slideshow","vlc"],"created_at":"2026-04-29T03:08:46.997Z","updated_at":"2026-04-29T03:08:47.699Z","avatar_url":"https://github.com/parthalon025.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# FrameCast\n\n[![CI](https://github.com/parthalon025/framecast/actions/workflows/test.yml/badge.svg)](https://github.com/parthalon025/framecast/actions/workflows/test.yml)\n[![Build Image](https://github.com/parthalon025/framecast/actions/workflows/build-image.yml/badge.svg)](https://github.com/parthalon025/framecast/actions/workflows/build-image.yml)\n[![Python](https://img.shields.io/badge/python-3.11+-blue)](https://www.python.org)\n[![License: MIT](https://img.shields.io/badge/license-MIT-green)](LICENSE)\n\n**Turn any TV into a family photo frame -- flash an SD card, plug in the Pi, done.**\n\nAnyone on your WiFi uploads photos from their phone's browser. No app. No cloud. No subscription. Photos appear on the TV within seconds.\n\n---\n\n## Quick Start\n\n1. **Download** the latest `.zip` from [Releases](../../releases)\n2. **Flash** with [Raspberry Pi Imager](https://www.raspberrypi.com/software/) (select \"Use custom\")\n3. **Boot** the Pi -- it displays a setup screen with a QR code\n4. **Scan the QR code** from your phone to open the web UI\n5. **Upload photos** -- the slideshow starts automatically\n\nNo SSH. No terminal. No Linux knowledge required.\n\n---\n\n## Who This Is For\n\n**This is for you if:**\n- You want a zero-config photo frame that anyone in the family can add photos to from their phone\n- You want a self-hosted solution with no cloud dependency, no subscriptions, and no data leaving your network\n- You want something that boots, connects to WiFi, and just works -- even after power outages\n\n**This is not for you if:**\n- You need cloud sync (Google Photos, iCloud) -- FrameCast is local-only by design\n- You need video playback with audio -- FrameCast displays photos and silent video loops\n- You want to run this on non-Pi hardware -- the OS image is Pi-specific (3/4/5, arm64)\n\n---\n\n## Features\n\n| Category | What it does |\n|---|---|\n| **Slideshow** | Weighted rotation with CSS transitions (fade, slide, Ken Burns, dissolve). \"On This Day\" memories from EXIF dates. Recency, favorites, and diversity weighting. |\n| **Upload** | Drag-and-drop from any browser. Auto-resize, duplicate detection, EXIF GPS extraction. |\n| **Albums \u0026 Favorites** | Organize photos into albums. Star favorites for 3x slideshow weight. |\n| **Multi-user** | Each person gets credit for their uploads. Per-user stats and attribution. |\n| **Stats dashboard** | Most shown, least shown, upload timeline, per-user breakdown, storage usage. |\n| **Photo map** | GPS EXIF data plotted on an offline SVG world map. |\n| **WiFi setup** | Captive portal with onboarding wizard. AP mode auto-starts on first boot. Hotspot fallback when home network is unavailable. |\n| **TV control** | HDMI-CEC scheduled on/off. Manual toggle from the web UI. |\n| **OTA updates** | GitHub Releases API with SHA-256 verification. Health-check rollback within 90 seconds. |\n| **Security** | PIN authentication (4/6 digit), rate limiting, ufw firewall (RFC1918 only), cookie hardening. |\n| **Self-healing** | Crash recovery via systemd watchdog. Config restore. Hardware watchdog. |\n| **Terminal aesthetic** | superhot-ui green phosphor monitor interface. piOS voice. |\n\n---\n\n## Supported Hardware\n\n| Pi Model | Status |\n|----------|--------|\n| Raspberry Pi 3B / 3B+ | Supported (64-bit, performance-optimized) |\n| Raspberry Pi 4 | Supported |\n| Raspberry Pi 5 | Supported |\n\nAny HDMI TV or monitor. A 7\" or 10\" HDMI display works well as a dedicated frame.\n\n**Requirements:** microSD card (16 GB min, 32 GB recommended), power supply for your Pi model, HDMI cable (micro-HDMI adapter for Pi 4/5).\n\n---\n\n## Architecture\n\nOne Flask app serves two surfaces:\n\n```\n+-----------------------------------------------+\n|                 Raspberry Pi                   |\n|                                                |\n|  +------------------+   +------------------+  |\n|  | framecast-kiosk  |   |  Flask + SSE     |  |     +-----------+\n|  | cage + GTK-WebKit|   |  (gunicorn)      |\u003c------\u003e| Phone /   |\n|  | /display route   |   |  Upload, API,    |  |WiFi | Computer  |\n|  | Preact slideshow |   |  settings, map   |  |     +-----------+\n|  +-------+----------+   +--------+---------+  |\n|          |                       |             |\n|          +----------+------------+             |\n|                     |                          |\n|             +-------v--------+                 |\n|             |  SQLite DB +   |                 |\n|             |  ~/media/      |                 |\n|             +----------------+                 |\n|                                                |\n|  systemd services | watchdog | ufw firewall    |\n+------------------------------------------------+\n                      | HDMI-CEC\n                      v\n              +---------------+\n              |  TV / Monitor |\n              +---------------+\n```\n\n- **Phone** -- upload, settings, albums, favorites, stats, map, users, update (Preact SPA, 4 nav tabs)\n- **TV** -- slideshow with CSS animations, boot sequence, QR codes (Wayland kiosk via cage + GTK-WebKit)\n- **Database** -- SQLite with WAL mode, co-located with photos for unified backup\n\nWayland only -- no X11. The kiosk browser renders the slideshow page served by the same Flask app.\n\n---\n\n## Configuration\n\nAll settings live in `/opt/framecast/app/.env` and can be changed from the web UI Settings page.\n\n### Slideshow\n\n| Setting | Default | Description |\n|---------|---------|-------------|\n| `PHOTO_DURATION` | `10` | Seconds each photo is displayed |\n| `TRANSITION_TYPE` | `fade` | `fade`, `slide`, `zoom`, `dissolve`, `none` |\n| `TRANSITION_MODE` | `single` | `single` (one type) or `random` (mix) |\n| `TRANSITION_DURATION_MS` | `1000` | Transition speed in ms (500-3000) |\n| `KENBURNS_INTENSITY` | `moderate` | `subtle`, `moderate`, `dramatic` |\n| `PHOTO_ORDER` | `shuffle` | `shuffle`, `newest`, `oldest`, `alphabetical` |\n| `QR_DISPLAY_SECONDS` | `30` | QR code duration on boot (0 to disable) |\n\n### Security\n\n| Setting | Default | Description |\n|---------|---------|-------------|\n| `ACCESS_PIN` | (generated) | PIN shown on TV for authentication |\n| `PIN_LENGTH` | `4` | `4` or `6` digits |\n| `PIN_ROTATE_ON_BOOT` | `no` | New PIN every boot |\n\n### Display Schedule\n\n| Setting | Default | Description |\n|---------|---------|-------------|\n| `HDMI_SCHEDULE_ENABLED` | `no` | Automatic TV on/off |\n| `HDMI_ON_TIME` | `08:00` | Turn on (24h) |\n| `HDMI_OFF_TIME` | `22:00` | Turn off (24h) |\n| `DISPLAY_SCHEDULE_DAYS` | `mon,tue,wed,thu,fri,sat,sun` | Active days |\n\n### Server \u0026 Media\n\n| Setting | Default | Description |\n|---------|---------|-------------|\n| `WEB_PORT` | `8080` | HTTP port |\n| `MAX_UPLOAD_MB` | `200` | Max upload size |\n| `AUTO_RESIZE_MAX` | `1920` | Max dimension for auto-resize (0 to disable) |\n| `MEDIA_DIR` | `/home/pi/media` | Photo storage path |\n| `AUTO_UPDATE_ENABLED` | `no` | Check for OTA updates daily |\n\n---\n\n## Building from Source\n\n### Prerequisites\n\n- Node.js 18+ (frontend build)\n- Python 3.11+ with pip\n- Linux x86_64 with sudo (native image build) or Docker\n\n### Frontend\n\n```bash\ncd app/frontend\nnpm install\nnpm run build\n```\n\n### Run Locally\n\n```bash\npip install flask gunicorn pillow\ncd app\ngunicorn -c gunicorn.conf.py web_upload:app\n```\n\nOpen `http://localhost:8080` for the phone UI. The TV display (`/display`) requires a Wayland compositor.\n\n### Build the OS Image\n\nBuilt with [pi-gen](https://github.com/RPi-Distro/pi-gen) (bookworm-arm64):\n\n```bash\ncd pi-gen\n./build.sh                 # Full build (~35 min first time)\n./build.sh --app-only      # Rebuild app stage only (~5 min)\n./build.sh --base-only     # OS layer without app\n./build.sh --continue      # Add app layer to existing base\n./build.sh --docker        # Build via Docker\n./build.sh --clean         # Wipe work/ and deploy/ first\n```\n\nOutput: `pi-gen/pi-gen/deploy/image_*-FrameCast-v*.zip`\n\n---\n\n## Project Structure\n\n```\nframecast/\n|-- app/\n|   |-- web_upload.py            # Flask app factory + static serving\n|   |-- api.py                   # REST API (~70 routes)\n|   |-- sse.py                   # Server-Sent Events\n|   |-- gunicorn.conf.py         # workers=1 (mandatory — SSE singleton)\n|   |-- modules/                 # db, rotation, users, cec, auth, wifi, updater, config, media, services, rate_limiter, boot_config\n|   |-- frontend/src/            # Preact + esbuild + superhot-ui\n|   |-- static/                  # Built CSS/JS\n|   |-- templates/               # spa.html (SPA shell)\n|-- pi-gen/                      # OS image build\n|-- scripts/                     # health-check, HDMI control, post-update, smoke test\n|-- systemd/                     # 6 service/timer units\n|-- tests/                       # 368 tests (340 Python + 15 vitest + 13 bats)\n```\n\n---\n\n## CI/CD\n\n### PR Gate (16 jobs)\n\n| Job | What |\n|-----|------|\n| lint-python | ruff |\n| shellcheck | all `.sh` files |\n| typecheck | mypy strict |\n| pytest | unit, property, concurrency, fault injection, benchmarks |\n| integration | gunicorn + real endpoint verification |\n| build-frontend | esbuild + asset verification |\n| test-frontend | vitest (SSE client) |\n| test-shell | bats (health-check rollback) |\n| architecture | structural invariants |\n| smoke | file structure, permissions, systemd units |\n| Claude Code Review | AI review against CLAUDE.md conventions |\n| Claude Security Review | OWASP analysis (path-triggered) |\n| actionlint | workflow validation |\n| commitlint | conventional commits |\n| CodeQL | SAST |\n| gitleaks | secret scanning |\n\n### Release Pipeline (on `v*` tag)\n\n1. Full test suite gate\n2. Pi-gen image build (~41 min, cached ~5 min)\n3. Structural image validation (boot partition + rootfs + systemd units)\n4. SBOM generation (CycloneDX -- Python + Node)\n5. Cosign keyless signing (Sigstore OIDC)\n6. SLSA Build Level 2 attestation\n7. GitHub Release (image + checksums + signatures + SBOMs)\n8. Telegram notification\n\n**Automation:** [release-please](https://github.com/googleapis/release-please) (auto VERSION + CHANGELOG), [Dependabot](https://docs.github.com/en/code-security/dependabot) (weekly pip/npm, monthly Actions), branch protection (squash-only, linear history).\n\n### Verify a Release\n\n```bash\n# Cosign signature\ncosign verify-blob \\\n  --certificate image.pem --signature image.sig \\\n  --certificate-oidc-issuer https://token.actions.githubusercontent.com \\\n  image.zip\n\n# SLSA provenance\ngh attestation verify image.zip -R parthalon025/framecast\n```\n\n---\n\n## Troubleshooting\n\n### WiFi won't connect\n\n- **Check the password.** Most common cause is a typo. Re-enter via the web UI.\n- **Network not found.** Move the Pi closer to the router.\n- **AP mode stuck.** Power cycle the Pi. It will restart the captive portal.\n- **Hidden network.** FrameCast cannot scan hidden SSIDs.\n\n### TV is black\n\n- **Check HDMI.** Unplug and re-plug.\n- **Power supply.** Undervoltage throttling may prevent display output. Use the official PSU.\n- **Schedule.** If enabled, the display turns off at the configured time.\n- **Kiosk crash.** `journalctl -u framecast-kiosk -n 50`. Auto-restarts within 60s.\n\n### Photos not showing\n\n- **Upload completed?** Check the upload page for errors.\n- **File format.** Only standard image/video formats accepted.\n- **Disk full.** Check Settings for storage usage.\n- **Quarantined.** Corrupt images are auto-quarantined. Check `journalctl -u framecast`.\n\n### OTA update failed\n\n- **No internet.** Updates require internet access.\n- **SHA mismatch.** Try again. If persistent, report an issue.\n- **Rollback.** Health-check timer auto-rolls back within 90 seconds.\n\n### Debugging\n\n```bash\njournalctl -u framecast -n 100        # Web server\njournalctl -u framecast-kiosk -n 50   # Display\njournalctl -u wifi-manager -n 50      # WiFi\nsystemctl status framecast framecast-kiosk wifi-manager\nsudo ufw status                       # Firewall\n```\n\n---\n\n## Contributing\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for dev setup and PR guidelines.\n\n## API\n\nSee [API.md](API.md) for complete endpoint documentation.\n\n## Credits\n\nBased on [pi-video-photo-slideshow](https://github.com/bobburgers7/pi-video-photo-slideshow) by bobburgers7.\n\n## License\n\nMIT -- see [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fparthalon025%2Fframecast","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fparthalon025%2Fframecast","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fparthalon025%2Fframecast/lists"}