Sign inSign up

lanedfritz/friday

By lanedfritz

•Updated about 14 hours ago

Sort of like J.A.R.V.I.S. but different. This is a project to explore creating a personal assistant

Image
0

753

lanedfritz/friday repository overview

⁠Friday

⁠Overview

Friday is a self-hosted, personal AI assistant. It's a web app (chat + a dashboard) built around your own choice of LLM backend - you bring the model, Friday brings the persona, memory, tool calling, and voice/automation integrations around it.

Core features work out of the box once you've connected an LLM:

  • Multi-persona chat, each with its own system prompt, model, and voice
  • Tool calling (the assistant can actually do things - check the weather, search the web, control smart-home devices via Home Assistant, and more via MCP servers you configure)
  • Cross-conversation memory (Graphiti + Neo4j, bundled)
  • Voice: text-to-speech, speech-to-text, and wake-word detection are all bundled. Real-time WebRTC voice (LiveKit) and camera streaming (go2rtc) are bundled too.

Friday does not include an LLM. You need your own - a local server (Ollama, vLLM, or anything else with an OpenAI-compatible API) or a cloud provider's API key (OpenAI, Groq, etc.). Everything else - which provider, which model, your first persona - is set up through an in-app first-run wizard after you log in for the first time, not through compose file edits.

⁠Prerequisites

Required:

  • Docker and Docker Compose
  • Your own LLM backend, reachable from wherever you deploy this (a local server, or a cloud API key)
  • An NVIDIA GPU, for the bundled text-to-speech engine (remove the omnivoice-tts service and leave TextToSpeech__Url blank if you don't have one - everything else runs fine on CPU)

Everything below is bundled in the one compose file - nothing else to download or stand up yourself. The compose file is entirely self-contained: its two small config files (for the memory feature's Graphiti server and its nginx workaround) are embedded inline as Compose configs: blocks, not separate files.

FeatureServiceNeeds configuring after first run?
DatabasePostgreSQLNo
Text-to-speechOmniVoice⁠No
Speech-to-textwhisper-asr-webservice⁠No
Face recognitionDeepFace⁠No
Wake-word detectionWyoming openWakeWord⁠Yes - point Friday's Settings page at it
Real-time voice (WebRTC)LiveKit⁠No (env vars only)
Camera streaminggo2rtc⁠No
Cross-conversation memoryNeo4j⁠ + Graphiti⁠Yes - add Graphiti as an MCP server from Friday's McpServers page, and set its LLM/embedding server on the Settings page

Needs a custom-built service, not just a public image - Friday's own wrapper Dockerfiles for these aren't published, so they're only available if you build a compatible replacement yourself: speaker-verification embedding, Piper TTS, and voice-cloning engines other than OmniVoice (Qwen3-TTS, Chatterbox-Turbo, IndexTTS). None of these are required for Friday to run.

⁠Compose

Save this as docker-compose.yml, then run docker compose up -d. Before deploying, generate real values for POSTGRES_PASSWORD, NEO4J_PASSWORD, LIVEKIT_API_KEY/LIVEKIT_API_SECRET (32+ characters), and set GO2RTC_PUBLIC_URL to your server's real address - either in a .env file next to the compose file, or exported before docker compose up. Every placeholder default below is clearly marked and not safe to deploy as-is.

services:
  postgres:
    image: postgres:16
    restart: unless-stopped
    environment:
      POSTGRES_DB: friday_db
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-change-me-generate-a-real-password}
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U admin -d friday_db"]
      interval: 5s
      timeout: 5s
      retries: 10

  # faster-whisper via whisper-asr-webservice - speech-to-text. CPU-mode by default; add a GPU
  # reservation (see omnivoice-tts below for the shape) if you have one.
  whisper:
    image: onerahmet/openai-whisper-asr-webservice:latest
    restart: unless-stopped
    environment:
      ASR_MODEL: base
      ASR_ENGINE: faster_whisper

  # DeepFace's official image with its built-in REST API. CPU-mode by default.
  facerecognition:
    image: serengil/deepface:latest
    restart: unless-stopped

  # rhasspy's Wyoming-protocol wrapper around openWakeWord - streamed to continuously over a
  # plain TCP socket rather than one HTTP-uploaded clip at a time. Point Friday's own Settings
  # page at ws://wakeword-wyoming:10400 (or the mapped host port) after first run - this engine
  # is configured in-app, not via an env var here.
  #
  # --custom-model-dir is NOT optional: this image loads zero wake-word models by default and
  # silently reports "not-detected" forever with no error otherwise.
  wakeword-wyoming:
    image: rhasspy/wyoming-openwakeword
    restart: unless-stopped
    command: ["--custom-model-dir", "/usr/src/.venv/lib/python3.11/site-packages/pyopen_wakeword/models"]
    ports:
      - "10400:10400"

  # Neo4j Community Edition - Graphiti's graph storage backend for cross-conversation memory.
  neo4j:
    image: neo4j:5.26.0
    restart: unless-stopped
    environment:
      NEO4J_AUTH: "neo4j/${NEO4J_PASSWORD:-change-me-generate-a-real-password}"
      NEO4J_server_memory_heap_initial__size: 512m
      NEO4J_server_memory_heap_max__size: 1G
      NEO4J_server_memory_pagecache_size: 512m
    volumes:
      - neo4j-data:/data
      - neo4j-logs:/logs
    healthcheck:
      test: ["CMD", "wget", "-O", "/dev/null", "http://localhost:7474"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  # Graphiti's own official MCP server. Once running, add it as an MCP server from Friday's own
  # McpServers page (Url: http://graphiti-proxy:8000/mcp/ if Friday's on the same compose
  # network, or the host's published address otherwise) - Friday then gets its memory
  # search/recall tools automatically. The actual LLM/embedding server used for extraction is
  # NOT set here - configure it on Friday's own Settings page; this always talks through Friday's
  # /llm-proxy/* endpoints, since this image can't read Friday's database directly.
  #
  # Its own baked-in Neo4j config preset (CONFIG_PATH pointed at the image's own
  # config-docker-neo4j.yaml) was tried and does NOT work - verified live it silently keeps using
  # FalkorDB regardless. The `graphiti_config` file below (mounted over its default config path)
  # is required, not optional.
  graphiti-mcp:
    image: zepai/knowledge-graph-mcp:standalone
    restart: unless-stopped
    depends_on:
      neo4j:
        condition: service_healthy
      web:
        condition: service_started
    environment:
      NEO4J_URI: "bolt://neo4j:7687"
      NEO4J_USER: "neo4j"
      NEO4J_PASSWORD: "${NEO4J_PASSWORD:-change-me-generate-a-real-password}"
      # A small/fast model - keep this in sync with whichever model Friday's own Tool-Calling
      # Model setting points at, since this does the same kind of role (structured extraction).
      MODEL_NAME: ${GRAPHITI_MODEL_NAME:-qwen3:1.7b}
      OPENAI_API_KEY: "none"
      OPENAI_BASE_URL: "http://web:8080/llm-proxy/llm/v1"
    configs:
      - source: graphiti_config
        target: /app/mcp/config/config.yaml

  # Works around a hardcoded Host-header check in the Graphiti MCP server that rejects any
  # hostname other than localhost/127.0.0.1 - see the nginx_proxy_conf config below.
  graphiti-proxy:
    image: nginx:alpine
    restart: unless-stopped
    depends_on:
      - graphiti-mcp
    configs:
      - source: nginx_proxy_conf
        target: /etc/nginx/conf.d/default.conf
    ports:
      - "8020:8000"

  # Bridges RTSP camera feeds to a browser-playable stream for the Dashboard's Camera widget.
  # Streams are registered dynamically through Friday's own UI. Must be reachable directly by the
  # browser (not proxied through `web`) - see Go2Rtc__PublicUrl below. api.origin:"*" is needed
  # because go2rtc rejects cross-origin requests by default, and Friday's page is served from a
  # different origin - passed inline since go2rtc accepts a config string as well as a file.
  go2rtc:
    image: alexxit/go2rtc:latest
    restart: unless-stopped
    command: ["go2rtc", "-c", "{\"api\":{\"origin\":\"*\"},\"streams\":{}}"]
    ports:
      - "1984:1984"
    volumes:
      - go2rtc-data:/config

  # Single-node self-hosted setup: host networking sidesteps mapping LiveKit's WebRTC UDP port
  # range through Docker's bridge network (LiveKit's own recommendation for this case). That also
  # means the `web` container can't reach it via a service name on the compose network - see
  # LiveKit__ServerApiUrl below. LIVEKIT_KEYS must be "key: secret" (with that exact spacing) and
  # the secret needs to be 32+ characters or the server refuses to start.
  livekit:
    image: livekit/livekit-server:latest
    restart: unless-stopped
    network_mode: host
    environment:
      LIVEKIT_KEYS: "${LIVEKIT_API_KEY:-changeme-key}: ${LIVEKIT_API_SECRET:-changeme-generate-a-real-32-char-plus-secret}"
      LIVEKIT_RTC_PORT_RANGE_START: 50000
      LIVEKIT_RTC_PORT_RANGE_END: 50100
      LIVEKIT_RTC_USE_EXTERNAL_IP: "false"

  # OmniVoice (k2-fsa/OmniVoice, wrapped by diogod2r/omnivoice-fastapi) - the only TTS engine
  # bundled here. Any other engine (Piper, Qwen3-TTS, Chatterbox, IndexTTS, etc.) is not included -
  # stand one up yourself and point TextToSpeech__Url at it instead. Needs an NVIDIA GPU - remove
  # this service (and leave TextToSpeech__Url blank below) if you don't have one.
  omnivoice-tts:
    image: diogod2r/omnivoice-fastapi:latest
    restart: unless-stopped
    ports:
      - "5007:8880"
    volumes:
      - omnivoice-models:/app/models
    environment:
      DEVICE: cuda:0
      MODEL_ID: k2-fsa/OmniVoice
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]

  web:
    image: ${FRIDAY_IMAGE:-yourdockerhubname/friday-web:latest}
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      TZ: ${TZ:-UTC}
      ConnectionStrings__DefaultConnection: "Host=postgres;Port=5432;Database=friday_db;Username=admin;Password=${POSTGRES_PASSWORD:-change-me-generate-a-real-password}"
      # Persisted outside the container filesystem, or every container recreation invalidates
      # every login session - see the volume mount below.
      DataProtection__KeysPath: /keys

      # --- Bundled above ---
      TextToSpeech__Url: "http://omnivoice-tts:8880"
      SpeechToText__Url: "http://whisper:9000"
      FaceRecognition__Url: "http://facerecognition:5000"
      Neo4j__Uri: "bolt://neo4j:7687"
      Neo4j__User: "neo4j"
      Neo4j__Password: "${NEO4J_PASSWORD:-change-me-generate-a-real-password}"
      # Go2Rtc__Url is what the backend uses to register streams (compose service name);
      # Go2Rtc__PublicUrl is handed to the browser directly, so it needs YOUR server's real,
      # externally-reachable address - "localhost" only works if you're browsing from the same
      # machine this is deployed on.
      Go2Rtc__Url: "http://go2rtc:1984"
      Go2Rtc__PublicUrl: ${GO2RTC_PUBLIC_URL:-http://localhost:1984}
      # Same split as Go2Rtc above, for the same reason - LiveKit__Url goes to the browser,
      # LiveKit__ServerApiUrl is what this container uses. Must match the livekit service's
      # LIVEKIT_KEYS above (same LIVEKIT_API_KEY/LIVEKIT_API_SECRET vars, single source of truth).
      LiveKit__Url: ${LIVEKIT_URL:-ws://localhost:7880}
      LiveKit__ServerApiUrl: "http://host.docker.internal:7880"
      LiveKit__ApiKey: ${LIVEKIT_API_KEY:-changeme-key}
      LiveKit__ApiSecret: ${LIVEKIT_API_SECRET:-changeme-generate-a-real-32-char-plus-secret}

      # --- Not bundled - each needs a custom-built image not published anywhere, so it's only
      # available if you build a compatible replacement yourself. Leave blank to leave off. ---
      SpeakerEmbedding__Url: ${SPEAKER_EMBEDDING_URL:-}
    extra_hosts:
      - "host.docker.internal:host-gateway"
    volumes:
      - dataprotection-keys:/keys
    ports:
      - "${WEB_PORT:-8080}:8080"

volumes:
  postgres-data:
  dataprotection-keys:
  omnivoice-models:
  neo4j-data:
  neo4j-logs:
  go2rtc-data:

configs:
  # Graphiti MCP server config - both api_url fields point at Friday's own web container, not at
  # an LLM host directly. graphiti-mcp is a separate prebuilt image with no way to read Friday's
  # database, so it always talks to a fixed proxy endpoint inside Friday's web app, which forwards
  # each request to whatever LLM/embedding server you've configured on Friday's own Settings page.
  # Change your real LLM/embedding server there, not here.
  graphiti_config:
    content: |
      server:
        transport: "http"
        host: "0.0.0.0"
        port: 8000

      llm:
        provider: "openai"
        model: $${MODEL_NAME:qwen3.5:latest}
        max_tokens: 4096

        providers:
          openai:
            api_key: $${OPENAI_API_KEY:none}
            api_url: "http://web:8080/llm-proxy/llm/v1"

      embedder:
        provider: "openai"
        model: $${EMBEDDER_MODEL:nomic-embed-text}
        dimensions: $${EMBEDDER_DIMENSIONS:768}

        providers:
          openai:
            api_key: $${OPENAI_API_KEY:none}
            api_url: "http://web:8080/llm-proxy/embedding/v1"

      database:
        provider: "neo4j"

        providers:
          neo4j:
            uri: $${NEO4J_URI:bolt://neo4j:7687}
            username: $${NEO4J_USER:neo4j}
            password: $${NEO4J_PASSWORD:change-me}
            database: $${NEO4J_DATABASE:neo4j}

      graphiti:
        group_id: $${GRAPHITI_GROUP_ID:friday}
        user_id: $${USER_ID:friday}
        entity_types:
          - name: "Preference"
            description: "User preferences, choices, opinions, or selections (PRIORITIZE over most other types except User/Assistant)"
          - name: "Requirement"
            description: "Specific needs, features, or functionality that must be fulfilled"
          - name: "Procedure"
            description: "Standard operating procedures and sequential instructions"
          - name: "Location"
            description: "Physical or virtual places where activities occur"
          - name: "Event"
            description: "Time-bound activities, occurrences, or experiences"
          - name: "Organization"
            description: "Companies, institutions, groups, or formal entities"
          - name: "Document"
            description: "Information content in various forms (books, articles, reports, etc.)"
          - name: "Topic"
            description: "Subject of conversation, interest, or knowledge domain (use as last resort)"
          - name: "Object"
            description: "Physical items, tools, devices, or possessions (use as last resort)"

  # The Graphiti MCP server has a hardcoded DNS-rebinding check that only trusts a Host header of
  # "localhost"/"127.0.0.1"/"::1" - it's not exposed as a config option. Reaching it by its Docker
  # service name from the web container fails that check with a 421. This proxy sits in between
  # purely to rewrite the Host header back to something the check accepts.
  nginx_proxy_conf:
    content: |
      server {
          listen 8000;

          location / {
              proxy_pass http://graphiti-mcp:8000;
              proxy_set_header Host "localhost:8000";
              proxy_http_version 1.1;
              proxy_set_header Connection "";
              proxy_redirect http://localhost:8000/ http://graphiti-proxy:8000/;
              proxy_buffering off;
              proxy_cache off;
              proxy_read_timeout 3600s;
          }
      }
⁠First run
  1. docker compose up -d
  2. Open http://<your-server>:8080 and register an account - the first account created has no special setup, registration is open by default. There's no email server configured, so the confirmation link is shown directly on the page instead of being emailed - click it, then log in.
  3. You'll land on a short setup wizard: add your LLM provider (name, type, base URL, API key if needed), add a model, and create your first persona. This is the only manual setup step - everything else (personas, providers, MCP servers, settings) is managed from the app itself afterward.
  4. Optional: add Graphiti as an MCP server (McpServers page, URL http://graphiti-proxy:8000/mcp/) for cross-conversation memory, and point the wake-word engine (Settings page) at ws://wakeword-wyoming:10400 if you're using voice.
⁠Before you go further than a personal/trusted-network deploy
  • Make sure you changed every placeholder credential listed under Compose above - none of them are safe defaults.
  • Put this behind a reverse proxy with HTTPS if it's reachable outside your own network.

Tag summary

Content type

Image

Digest

sha256:61a58fae1…

Size

104.5 MB

Last updated

about 14 hours ago

docker pull lanedfritz/friday