From 5ba367d8a0028d99d7265ce488a366eea8a3778e Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Sat, 30 Aug 2025 20:52:18 +0000 Subject: [PATCH 01/35] wow giga commit --- CLAUDE.md | 9 +- README.md | 23 +- backends/advanced/Dockerfile | 6 +- backends/advanced/Docs/UI.md | 34 +- backends/advanced/Docs/architecture.md | 13 +- backends/advanced/Docs/memories.md | 20 +- backends/advanced/Docs/quickstart.md | 128 ++++-- backends/advanced/README.md | 36 +- backends/advanced/SETUP_SCRIPTS.md | 170 ++++++++ backends/advanced/docker-compose.yml | 2 - backends/advanced/init-https.sh | 105 +++++ backends/advanced/init.sh | 397 +++++++++++++++--- backends/advanced/run-test.sh | 14 + .../controllers/memory_controller.py | 15 +- .../controllers/system_controller.py | 139 ++++++ .../advanced/src/advanced_omi_backend/main.py | 109 ++++- .../src/advanced_omi_backend/memory/base.py | 28 ++ .../memory/compat_service.py | 15 + .../memory/memory_service.py | 22 + .../memory/providers/vector_stores.py | 49 ++- .../src/advanced_omi_backend/processors.py | 88 ++++ .../routers/modules/system_routes.py | 29 +- .../speaker_recognition_client.py | 79 ++++ .../src/advanced_omi_backend/users.py | 3 + .../advanced/webui/src/pages/Memories.tsx | 224 +++++++++- backends/advanced/webui/src/pages/System.tsx | 259 +++++++++++- backends/advanced/webui/src/services/api.ts | 19 + backends/advanced/webui/src/styles/slider.css | 78 ++++ extras/openmemory-mcp/docker-compose.yml | 7 +- .../webui/src/pages/Enrollment.tsx | 27 +- 30 files changed, 1947 insertions(+), 200 deletions(-) create mode 100644 backends/advanced/SETUP_SCRIPTS.md create mode 100755 backends/advanced/init-https.sh create mode 100644 backends/advanced/webui/src/styles/slider.css diff --git a/CLAUDE.md b/CLAUDE.md index 21507cafd..a5e14a77a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -481,7 +481,9 @@ The sophisticated in-house memory implementation with full control and customiza - Individual fact storage (no JSON blobs) - Smart deduplication algorithms - Intelligent memory updates (ADD/UPDATE/DELETE decisions) -- Direct Qdrant vector storage +- **Semantic search** with relevance threshold filtering +- **Memory count API** with total count tracking from native Qdrant +- Direct Qdrant vector storage with accurate similarity scoring - Custom memory prompts and processing - No external dependencies @@ -518,10 +520,12 @@ Thin client that delegates all memory processing to external OpenMemory MCP serv | **Processing** | Custom LLM extraction | Delegates to OpenMemory | | **Deduplication** | Custom algorithms | OpenMemory handles | | **Vector Storage** | Direct Qdrant | OpenMemory handles | +| **Search Features** | Semantic search with threshold filtering | Semantic search with relevance scoring | +| **Memory Count** | Native Qdrant count API | Varies by OpenMemory support | | **Dependencies** | Qdrant + MongoDB | External OpenMemory server | | **Customization** | Full control | Limited to OpenMemory features | | **Cross-client** | Friend-Lite only | Works with Claude Desktop, Cursor, etc | -| **Web UI** | Friend-Lite WebUI | OpenMemory UI + Friend-Lite WebUI | +| **Web UI** | Friend-Lite WebUI with advanced search | OpenMemory UI + Friend-Lite WebUI | | **Memory Format** | Individual facts | OpenMemory format | | **Setup Complexity** | Medium | High (external server required) | @@ -640,6 +644,7 @@ Project includes `.cursor/rules/always-plan-first.mdc` requiring understanding b ### Memory & Conversation Debugging - **GET /api/admin/memories**: All memories across all users with debug stats (Admin only) - **GET /api/memories/unfiltered**: User's memories without filtering +- **GET /api/memories/search**: Semantic memory search with relevance scoring - **GET /api/conversations**: User's conversations with transcripts - **GET /api/conversations/{audio_uuid}**: Specific conversation details diff --git a/README.md b/README.md index 6c2997b3d..7e749850a 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,18 @@ ## Quick Start -**Quick start:** +**Interactive setup (recommended):** 1. Go to `backends/advanced/` for the main application -2. Copy `.env.template` to `.env` and configure your API keys -3. Run `docker compose up --build -d` -4. Visit `http://localhost:3000` for the web interface +2. Run `./init.sh` to configure all services with guided prompts +3. Run `docker compose up --build -d` to start the system +4. Visit `http://localhost:5173` for the React web dashboard -**Documentation:** See `CLAUDE.md` and `backends/advanced/Docs/` +**Manual setup (alternative):** +1. Copy `.env.template` to `.env` and configure your API keys manually +2. Set up transcription provider (Deepgram/Mistral/Offline), LLM provider (OpenAI/Ollama), and memory provider +3. Start services and optional extras as needed + +**Documentation:** See `CLAUDE.md` and `backends/advanced/Docs/quickstart.md` for detailed setup ## Overview Friend-Lite provides essential components for developers working with OMI-compatible audio devices: @@ -41,6 +46,9 @@ Friend-Lite supports AI-powered personal systems through multiple OMI-compatible **Core Features:** - **Advanced memory system** with pluggable providers (Friend-Lite native or OpenMemory MCP) - **Memory extraction** from conversations with individual fact storage +- **Semantic memory search** with relevance threshold filtering and live results +- **Memory count display** with total count tracking from native providers +- **Speaker-based memory filtering** to control processing based on participant presence - **Action item detection** and tracking - **Home automation** integration - **Multi-device support** for comprehensive audio capture @@ -70,6 +78,7 @@ DevKit2 streams audio via Bluetooth using OPUS codec. The processing pipeline in - LLM-based conversation analysis (OpenAI or local Ollama) - **Dual memory system**: Friend-Lite native or OpenMemory MCP integration - Enhanced memory extraction with individual fact storage +- **Semantic search** with relevance scoring and threshold filtering - Smart deduplication and memory updates (ADD/UPDATE/DELETE) - Action item detection @@ -119,12 +128,14 @@ Choose one based on your needs: - Audio processing pipeline with real-time WebSocket support - **Pluggable memory system**: Choose between Friend-Lite native or OpenMemory MCP - Enhanced memory extraction with individual fact storage (no generic fallbacks) +- **Semantic memory search** with relevance threshold filtering and total count display +- **Speaker-based memory filtering**: Optional control over processing based on participant presence - Smart memory updates with LLM-driven action proposals (ADD/UPDATE/DELETE) - Speaker recognition and enrollment - Action items extraction from conversations - Audio cropping (removes silence, keeps speech) - Conversation management with session timeouts -- Web UI for management and monitoring +- Modern React web UI with live recording and advanced search - Multiple ASR options (Deepgram API + offline ASR) - MongoDB for structured data storage - RESTful API for all operations diff --git a/backends/advanced/Dockerfile b/backends/advanced/Dockerfile index e6319c102..947c8abec 100644 --- a/backends/advanced/Dockerfile +++ b/backends/advanced/Dockerfile @@ -33,8 +33,10 @@ RUN uv sync --extra deepgram # Copy all application code COPY . . -# Copy memory config (created by init.sh from template) -COPY memory_config.yaml ./ +# Copy configuration files if they exist, otherwise they will be created from templates at runtime +# The files are expected to exist, but we handle the case where they don't gracefully +COPY memory_config.yaml* ./ +COPY diarization_config.json* ./ # Run the application diff --git a/backends/advanced/Docs/UI.md b/backends/advanced/Docs/UI.md index 859290568..dff5a5c8d 100644 --- a/backends/advanced/Docs/UI.md +++ b/backends/advanced/Docs/UI.md @@ -1,14 +1,18 @@ -# Streamlit Web Dashboard Documentation +# React Web Dashboard Documentation ## Overview -The Friend-Lite web dashboard provides a comprehensive interface for managing conversations, memories, users, and system debugging. Built with Streamlit, it offers real-time access to audio processing pipelines and administrative functions. +The Friend-Lite web dashboard provides a comprehensive interface for managing conversations, memories, users, and system debugging. Built with modern React and TypeScript, it offers real-time access to audio processing pipelines, advanced memory search, and administrative functions. + +> **Note**: This documentation covers the modern React interface located in `./webui/`. The legacy Streamlit interface has been moved to `src/_webui_original/` for reference. ## Access & Authentication ### Dashboard URL -- **Local**: `http://localhost:8501` -- **Audio**: Configure `BACKEND_PUBLIC_URL` environment variable to point to some public URL of your server. Your BROWSER must be able to access this URL. +- **HTTP**: `http://localhost:5173` (development) or `http://localhost:3000` (production) +- **HTTPS**: `https://localhost/` (with HTTPS configuration via `init-https.sh`) +- **Live Recording**: Available at `/live-record` page for real-time audio streaming +- **Network Access**: Configure `BACKEND_PUBLIC_URL` for remote device access via Tailscale/LAN ### Authentication Methods 1. **Email/Password Login**: Standard authentication via backend API @@ -36,15 +40,29 @@ The Friend-Lite web dashboard provides a comprehensive interface for managing co - Advanced filtering and search capabilities ### 2. Memories Tab -**Purpose**: Browse and search extracted conversation memories - -**Features**: -- Semantic search across all memories +**Purpose**: Browse and search extracted conversation memories with advanced filtering + +**Core Search Features**: +- **Text Search**: Traditional keyword-based memory filtering +- **Semantic Search Button**: AI-powered contextual memory search with relevance scoring +- **Dual-layer Filtering**: Combine semantic results with text search for precise filtering +- **Relevance Threshold Slider**: 0-100% threshold to filter semantic results by confidence +- **Active Filter Indicators**: Visual feedback showing active semantic filters with clear buttons +- **Memory Count Display**: Shows "X of Y memories" with total count from native providers + +**Advanced Features**: +- **Live Filtering**: Real-time results as threshold slider moves +- **Reset Functionality**: Clear all filters and return to full memory list - Memory categorization and tagging - Temporal filtering and sorting - Memory source tracking (which conversation) - Export capabilities +**UI Improvements**: +- **Search Input Enhancement**: Semantic search button integrated into search field +- **Visual Feedback**: Loading states, relevance scores, and filter status indicators +- **Responsive Design**: Works across desktop and mobile devices + **Admin Features**: - **Admin Debug Section**: Load and view all user memories for debugging - **System-wide Memory View**: Access all memories across users diff --git a/backends/advanced/Docs/architecture.md b/backends/advanced/Docs/architecture.md index 9c8bdbed2..c0264528e 100644 --- a/backends/advanced/Docs/architecture.md +++ b/backends/advanced/Docs/architecture.md @@ -4,7 +4,7 @@ ## System Overview -Friend-Lite is a comprehensive real-time conversation processing system that captures audio streams, performs speech-to-text transcription, and extracts memories. The system features a FastAPI backend with WebSocket audio streaming, a Streamlit web dashboard for management, and complete user authentication with role-based access control. +Friend-Lite is a comprehensive real-time conversation processing system that captures audio streams, performs speech-to-text transcription, and extracts memories. The system features a FastAPI backend with WebSocket audio streaming, a modern React web dashboard with advanced search capabilities, and complete user authentication with role-based access control. **Core Implementation**: The complete system is implemented in `src/advanced_omi_backend/main.py` with supporting services in dedicated modules, using a modular router/controller architecture pattern. @@ -189,11 +189,14 @@ graph TB > 📖 **Read more**: [Authentication Architecture](./auth.md) for complete authentication system details -#### Streamlit Dashboard (`webui/streamlit_app.py`) -- **User-Friendly Interface**: Complete web-based management interface -- **Authentication Integration**: Login with backend JWT tokens or Google OAuth +#### React Dashboard (`webui/`) +- **Modern Interface**: Complete React/TypeScript web-based management interface +- **Advanced Memory Search**: Semantic search with relevance threshold filtering and live results +- **Memory Count Display**: Total count tracking with dual-layer filtering capabilities +- **Authentication Integration**: Login with backend JWT tokens and user management - **Real-Time Monitoring**: Live client status and conversation management -- **Data Management**: User, conversation, and memory interfaces +- **Live Recording**: Real-time audio recording with WebSocket streaming (HTTPS) +- **Data Management**: User, conversation, and memory interfaces with enhanced UX - **Audio Playback**: Smart audio player with original/cropped audio options - **System Health**: Visual service status and configuration display diff --git a/backends/advanced/Docs/memories.md b/backends/advanced/Docs/memories.md index 60c856696..06aa3f600 100644 --- a/backends/advanced/Docs/memories.md +++ b/backends/advanced/Docs/memories.md @@ -566,13 +566,27 @@ def consolidate_memories(self, user_id: str, time_window_hours: int = 24): ## API Endpoints -The memory service exposes these endpoints: +The memory service exposes these endpoints with enhanced search capabilities: -- `GET /api/memories` - Get user memories (keyed by database user_id) -- `GET /api/memories/search?query={query}` - Search memories (user-scoped) +- `GET /api/memories` - Get user memories with total count support (keyed by database user_id) +- `GET /api/memories/search?query={query}&limit={limit}` - **Semantic memory search** with relevance scoring (user-scoped) +- `GET /api/memories/unfiltered` - User's memories without filtering for debugging - `DELETE /api/memories/{memory_id}` - Delete specific memory (requires authentication) - `GET /api/memories/admin` - Admin view of all memories across all users (superuser only) +### Enhanced Search Features + +**Semantic Search (`/api/memories/search`)**: +- **Relevance Scoring**: Returns similarity scores from vector database (0.0-1.0 range) +- **Configurable Limits**: Supports `limit` parameter for result count control +- **User Scoped**: Results automatically filtered by authenticated user +- **Vector-based**: Uses embeddings for contextual understanding beyond keyword matching + +**Memory Count API**: +- **Friend-Lite Provider**: Native Qdrant count API provides accurate total counts +- **OpenMemory MCP Provider**: Count support varies by OpenMemory implementation +- **Response Format**: `{"memories": [...], "total_count": 42}` when supported + ### Admin Endpoints #### All Memories Endpoint (`/api/memories/admin`) diff --git a/backends/advanced/Docs/quickstart.md b/backends/advanced/Docs/quickstart.md index 9684eec78..307c8fdce 100644 --- a/backends/advanced/Docs/quickstart.md +++ b/backends/advanced/Docs/quickstart.md @@ -10,8 +10,9 @@ At the moment, the basic functionalities are: - Audio transcription - **Advanced memory system** with pluggable providers (Friend-Lite native or OpenMemory MCP) - **Enhanced memory extraction** with individual fact storage and smart updates +- **Semantic memory search** with relevance threshold filtering and live results - Action item extraction -- Modern React web dashboard with live recording +- Modern React web dashboard with live recording and advanced search features - Comprehensive user management with JWT authentication **Core Implementation**: See `src/advanced_omi_backend/main.py` for the complete FastAPI application and WebSocket handling. @@ -19,27 +20,99 @@ At the moment, the basic functionalities are: ## Prerequisites - Docker and Docker Compose -- (Optional) Deepgram API key for high-quality cloud transcription -- (Optional) Ollama for local LLM processing (memory extraction) -- (Optional) Wyoming ASR for offline speech-to-text processing +- API keys for your chosen providers (see setup script) ## Quick Start -### 0. Quick Testing (Optional) -To verify your setup, run the integration test suite: +### Step 1: Interactive Setup (Recommended) + +Run the interactive setup wizard to configure all services with guided prompts: ```bash -# Ensure .env file has DEEPGRAM_API_KEY and OPENAI_API_KEY configured -./run-test.sh +cd backends/advanced +./init.sh +``` -# Alternative: Manual test with detailed logging -source .env && export DEEPGRAM_API_KEY OPENAI_API_KEY && uv run pytest tests/test_integration.py -vv -s --log-cli-level=INFO --log-cli-format='%(asctime)s - %(levelname)s - %(message)s' +**The setup wizard will guide you through:** +- **Authentication**: Admin email/password setup +- **Transcription Provider**: Choose Deepgram, Mistral, or Offline (Parakeet) +- **LLM Provider**: Choose OpenAI or Ollama for memory extraction +- **Memory Provider**: Choose Friend-Lite Native or OpenMemory MCP +- **Optional Services**: Speaker Recognition and other extras +- **Network Configuration**: Ports and host settings + +**Example flow:** ``` -This end-to-end test demonstrates the complete audio processing pipeline. +🚀 Friend-Lite Interactive Setup +=============================================== + +► Authentication Setup +---------------------- +Admin email [admin@example.com]: john@company.com +Admin password (min 8 chars): ******** + +► Speech-to-Text Configuration +------------------------------- +Choose your transcription provider: + 1) Deepgram (recommended - high quality, requires API key) + 2) Mistral (Voxtral models - requires API key) + 3) Offline (Parakeet ASR - requires GPU, runs locally) + 4) None (skip transcription setup) +Enter choice (1-4) [1]: 1 + +Get your API key from: https://console.deepgram.com/ +Deepgram API key: dg_xxxxxxxxxxxxx + +► LLM Provider Configuration +---------------------------- +Choose your LLM provider for memory extraction: + 1) OpenAI (GPT-4, GPT-3.5 - requires API key) + 2) Ollama (local models - requires Ollama server) + 3) Skip (no memory extraction) +Enter choice (1-3) [1]: 1 +``` + +### Step 2: HTTPS Setup (Optional) +For microphone access and secure connections, set up HTTPS: +```bash +cd backends/advanced +./setup-https.sh 100.83.66.30 # Your Tailscale/network IP +``` -### 1. Environment Setup +This creates SSL certificates and configures nginx for secure access. + +### Step 3: Start the System + +**Start all services:** +```bash +cd backends/advanced +docker compose up --build -d +``` + +This starts: +- **Backend API**: `http://localhost:8000` +- **Web Dashboard**: `http://localhost:5173` +- **MongoDB**: `localhost:27017` +- **Qdrant**: `localhost:6333` -Copy the `.env.template` file to `.env` and configure the required values: +### Step 4: Optional Services + +**If you configured optional services during setup, start them:** + +```bash +# OpenMemory MCP (if selected) +cd ../../extras/openmemory-mcp && docker compose up -d + +# Parakeet ASR (if selected for offline transcription) +cd ../../extras/asr-services && docker compose up parakeet -d + +# Speaker Recognition (if enabled) +cd ../../extras/speaker-recognition && docker compose up --build -d +``` + +### Manual Configuration (Alternative) + +If you prefer manual configuration, copy the `.env.template` file to `.env` and configure the required values: **Required Environment Variables:** ```bash @@ -96,26 +169,17 @@ PARAKEET_ASR_URL=http://host.docker.internal:8080 - If not set, system falls back to offline ASR service - The system requires either online API keys or offline ASR service configuration -### 2. Start the System +### Testing Your Setup (Optional) -**Recommended: Docker Compose** +After configuration, verify everything works with the integration test suite: ```bash -cd backends/advanced -docker compose up --build -d -``` - -This starts: -- **Backend API**: `http://localhost:8000` -- **Web Dashboard**: `http://localhost:3000` -- **MongoDB**: `localhost:27017` -- **Qdrant**: `localhost:6333` -- (optional) **Ollama**: # commented out - -**Implementation**: See `docker-compose.yml` for complete service configuration and `src/advanced_omi_backend/main.py` for FastAPI application setup. - -### 3. Optional: Start ASR Service +./run-test.sh -For self-hosted speech recognition, see instructions in `extras/asr-services/`: +# Alternative: Manual test with detailed logging +source .env && export DEEPGRAM_API_KEY OPENAI_API_KEY && \ + uv run pytest tests/test_integration.py -vv -s --log-cli-level=INFO +``` +This end-to-end test validates the complete audio processing pipeline. ## Using the System @@ -129,9 +193,11 @@ For self-hosted speech recognition, see instructions in `extras/asr-services/`: ### Dashboard Features - **Conversations**: View audio recordings, transcripts, and cropped audio -- **Memories**: Search extracted conversation memories +- **Memories**: Advanced memory search with semantic search, relevance threshold filtering, and memory count display +- **Live Recording**: Real-time audio recording with WebSocket streaming (HTTPS required) - **User Management**: Create/delete users and their data - **Client Management**: View active connections and close conversations +- **System Monitoring**: Debug tools and system health monitoring ### Audio Client Connection diff --git a/backends/advanced/README.md b/backends/advanced/README.md index ec7aedeb3..eefbf54bb 100644 --- a/backends/advanced/README.md +++ b/backends/advanced/README.md @@ -16,10 +16,34 @@ A FastAPI backend with pluggable memory providers, real-time audio processing, a Modern React-based web dashboard located in `./webui/` with: - Live audio recording and real-time streaming - Chat interface with conversation management -- Memory search and management +- **Advanced memory search** with semantic search and relevance threshold filtering +- **Memory count display** showing total memories with live filtering +- **Dual-layer filtering** combining semantic and text search - System monitoring and debugging tools -### Quick Start (HTTP) +### Quick Start + +#### 1. Interactive Setup (Recommended) +```bash +# Run interactive setup wizard +./init.sh +``` + +**The setup wizard guides you through:** +- **Authentication**: Admin email/password setup with secure keys +- **Transcription Provider**: Choose between Deepgram, Mistral, or Offline (Parakeet) +- **LLM Provider**: Choose between OpenAI (recommended) or Ollama for memory extraction +- **Memory Provider**: Choose between Friend-Lite Native or OpenMemory MCP +- **Optional Services**: Speaker Recognition, network configuration +- **API Keys**: Prompts for all required keys with helpful links + +**HTTPS Setup (Optional):** +```bash +# For microphone access and secure connections +./setup-https.sh your-tailscale-ip +``` + +#### 2. Start Services (HTTP) ```bash # Start with hot reload development server docker compose up --build -d @@ -27,13 +51,13 @@ docker compose up --build -d - **Web Dashboard**: http://localhost:5173 -### HTTPS Setup (Required for Microphone Access) +#### 3. HTTPS Setup (Optional - For Microphone Access) -For network access and microphone features, set up HTTPS: +For network access and microphone features, HTTPS can be configured during initialization or separately: ```bash -# Initialize HTTPS with your Tailscale/network IP -./init.sh 100.83.66.30 # Replace with your IP +# If not done during init.sh, run HTTPS setup +./init-https.sh 100.83.66.30 # Replace with your IP # Start with HTTPS proxy docker compose --profile https up --build -d diff --git a/backends/advanced/SETUP_SCRIPTS.md b/backends/advanced/SETUP_SCRIPTS.md new file mode 100644 index 000000000..1c04e29bb --- /dev/null +++ b/backends/advanced/SETUP_SCRIPTS.md @@ -0,0 +1,170 @@ +# Setup Scripts Guide + +This document explains the different setup scripts available in Friend-Lite and when to use each one. + +## Script Overview + +| Script | Purpose | When to Use | +|--------|---------|-------------| +| `init.sh` | **Main interactive setup wizard** | **Recommended for all users** - First time setup with guided configuration | +| `setup-https.sh` | HTTPS certificate generation | **Optional** - When you need secure connections for microphone access | + +## Main Setup Script: `init.sh` + +**Purpose**: Interactive wizard that configures all services with guided prompts. + +### What it does: +- ✅ **Authentication Setup**: Admin email/password with secure key generation +- ✅ **Transcription Provider Selection**: Choose between Deepgram, Mistral, or Offline (Parakeet) +- ✅ **LLM Provider Configuration**: Choose between OpenAI (recommended) or Ollama +- ✅ **Memory Provider Setup**: Choose between Friend-Lite Native or OpenMemory MCP +- ✅ **API Key Collection**: Prompts for required keys with helpful links to obtain them +- ✅ **Optional Services**: Speaker Recognition, network configuration +- ✅ **Configuration Validation**: Creates complete .env with all settings + +### Usage: +```bash +cd backends/advanced +./init.sh +``` + +### Example Flow: +``` +🚀 Friend-Lite Interactive Setup +=============================================== + +► Authentication Setup +---------------------- +Admin email [admin@example.com]: john@company.com +Admin password (min 8 chars): ******** +✅ Admin account configured + +► Speech-to-Text Configuration +------------------------------- +Choose your transcription provider: + 1) Deepgram (recommended - high quality, requires API key) + 2) Mistral (Voxtral models - requires API key) + 3) Offline (Parakeet ASR - requires GPU, runs locally) + 4) None (skip transcription setup) +Enter choice (1-4) [1]: 1 + +Get your API key from: https://console.deepgram.com/ +Deepgram API key: dg_xxxxxxxxxxxxx +✅ Deepgram configured + +► LLM Provider Configuration +---------------------------- +Choose your LLM provider for memory extraction: + 1) OpenAI (GPT-4, GPT-3.5 - requires API key) + 2) Ollama (local models - requires Ollama server) + 3) Skip (no memory extraction) +Enter choice (1-3) [1]: 1 + +Get your API key from: https://platform.openai.com/api-keys +OpenAI API key: sk-xxxxxxxxxxxxx +OpenAI model [gpt-4o-mini]: gpt-4o-mini +✅ OpenAI configured + +...continues through all configuration sections... + +► Configuration Summary +----------------------- +✅ Admin Account: john@company.com +✅ Transcription: deepgram +✅ LLM Provider: openai +✅ Memory Provider: friend_lite +✅ Backend URL: http://localhost:8000 +✅ Dashboard URL: http://localhost:5173 + +► Next Steps +------------ +1. Start the main services: + docker compose up --build -d + +2. Access the dashboard: + http://localhost:5173 + +Setup complete! 🎉 +``` + +## HTTPS Setup Script: `setup-https.sh` + +**Purpose**: Generate SSL certificates and configure nginx for secure HTTPS access. + +### When needed: +- **Microphone access** from browsers (HTTPS required) +- **Remote access** via Tailscale or network +- **Production deployments** requiring secure connections + +### Usage: +```bash +cd backends/advanced +./setup-https.sh 100.83.66.30 # Your Tailscale or network IP +``` + +### What it does: +- Generates self-signed SSL certificates for your IP +- Configures nginx proxy for HTTPS access +- Sets up docker-compose HTTPS profile +- Provides HTTPS URLs for dashboard access + +### After HTTPS setup: +```bash +# Start with HTTPS profile +docker compose --profile https up --build -d + +# Access via HTTPS +https://localhost/ +https://100.83.66.30/ # Your configured IP +``` + +## Migration from Old Scripts + +### Old vs New Script Names: + +| Old Name | New Name | Purpose | +|----------|----------|---------| +| `init.sh` (old) | `setup-https.sh` | HTTPS setup only | +| `setup.sh` (created) | `init.sh` (new) | Main interactive setup | + +### If you have old scripts: +The old `init.sh` has been renamed to `setup-https.sh` to avoid confusion. The new `init.sh` is the main setup wizard. + +## Recommended Setup Flow + +### New Users (Recommended): +1. **Run main setup**: `./init.sh` +2. **Start services**: `docker compose up --build -d` +3. **Optional HTTPS**: `./setup-https.sh your-ip` (if needed) + +### Manual Configuration (Advanced): +1. **Copy template**: `cp .env.template .env` +2. **Edit manually**: Configure all providers and keys +3. **Start services**: `docker compose up --build -d` + +## Script Locations + +All setup scripts are located in: +``` +backends/advanced/ +├── init.sh # Main interactive setup wizard +├── setup-https.sh # HTTPS certificate generation +├── .env.template # Environment template +└── docker-compose.yml +``` + +## Getting Help + +- **Setup Issues**: See `Docs/quickstart.md` for detailed documentation +- **Configuration**: See `MEMORY_PROVIDERS.md` for provider comparisons +- **Troubleshooting**: Check `CLAUDE.md` for common issues +- **HTTPS Problems**: Ensure your IP is accessible and not behind firewall + +## Key Benefits of New Setup + +✅ **No more guessing**: Interactive prompts guide you through every choice +✅ **API key validation**: Links provided to obtain required keys +✅ **Provider selection**: Choose best services for your needs +✅ **Complete configuration**: Creates working .env with all settings +✅ **Next steps guidance**: Clear instructions for starting services +✅ **No manual editing**: Reduces errors from manual .env editing \ No newline at end of file diff --git a/backends/advanced/docker-compose.yml b/backends/advanced/docker-compose.yml index ffd887035..e9db4e761 100644 --- a/backends/advanced/docker-compose.yml +++ b/backends/advanced/docker-compose.yml @@ -112,8 +112,6 @@ services: condition: service_healthy webui: condition: service_started - profiles: - - https restart: unless-stopped healthcheck: test: ["CMD", "curl", "-f", "-k", "https://localhost/health"] diff --git a/backends/advanced/init-https.sh b/backends/advanced/init-https.sh new file mode 100755 index 000000000..b8a53b052 --- /dev/null +++ b/backends/advanced/init-https.sh @@ -0,0 +1,105 @@ +#!/bin/bash +set -e + +# Initialize Friend-Lite Advanced Backend with HTTPS proxy +# Usage: ./init.sh + +if [ $# -ne 1 ]; then + echo "Usage: $0 " + echo "Example: $0 100.83.66.30" + echo "" + echo "This script will:" + echo " 1. Generate SSL certificates for localhost and your Tailscale IP" + echo " 2. Create nginx.conf from template" + echo " 3. Set up HTTPS proxy for the backend" + exit 1 +fi + +TAILSCALE_IP="$1" + +# Validate IP format (basic check) +if ! echo "$TAILSCALE_IP" | grep -E '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$' > /dev/null; then + echo "Error: Invalid IP format. Expected format: xxx.xxx.xxx.xxx" + exit 1 +fi + +echo "🚀 Initializing Friend-Lite Advanced Backend with Tailscale IP: $TAILSCALE_IP" +echo "" + +# Check if nginx.conf.template exists +if [ ! -f "nginx.conf.template" ]; then + echo "❌ Error: nginx.conf.template not found" + echo " Make sure you're running this from the backends/advanced directory" + exit 1 +fi + +# Generate SSL certificates +echo "📄 Step 1: Generating SSL certificates..." +if [ -f "ssl/generate-ssl.sh" ]; then + ./ssl/generate-ssl.sh "$TAILSCALE_IP" + echo "✅ SSL certificates generated" +else + echo "❌ Error: ssl/generate-ssl.sh not found" + exit 1 +fi + +echo "" + +# Create nginx.conf from template +echo "📄 Step 2: Creating nginx configuration..." +sed "s/TAILSCALE_IP/$TAILSCALE_IP/g" nginx.conf.template > nginx.conf +echo "✅ nginx.conf created with IP: $TAILSCALE_IP" + +echo "" + +# Update .env file with HTTPS CORS origins +echo "📄 Step 3: Updating CORS origins..." +if [ -f ".env" ]; then + # Update existing .env file + if grep -q "CORS_ORIGINS" .env; then + # Update existing CORS_ORIGINS line + sed -i "s/CORS_ORIGINS=.*/CORS_ORIGINS=https:\/\/localhost,https:\/\/localhost:443,https:\/\/127.0.0.1,https:\/\/$TAILSCALE_IP/" .env + else + # Add CORS_ORIGINS line + echo "CORS_ORIGINS=https://localhost,https://localhost:443,https://127.0.0.1,https://$TAILSCALE_IP" >> .env + fi + echo "✅ Updated CORS origins in .env file" +else + echo "⚠️ No .env file found. You may need to:" + echo " 1. Copy .env.template to .env" + echo " 2. Add: CORS_ORIGINS=https://localhost,https://localhost:443,https://127.0.0.1,https://$TAILSCALE_IP" +fi + +# Create memory_config.yaml from template if it doesn't exist +echo "" +echo "📄 Step 4: Checking memory configuration..." +if [ ! -f "memory_config.yaml" ] && [ -f "memory_config.yaml.template" ]; then + cp memory_config.yaml.template memory_config.yaml + echo "✅ memory_config.yaml created from template" +elif [ -f "memory_config.yaml" ]; then + echo "✅ memory_config.yaml already exists" +else + echo "⚠️ Warning: memory_config.yaml.template not found" +fi + +echo "" +echo "🎉 Initialization complete!" +echo "" +echo "Next steps:" +echo " 1. Start the services:" +echo " docker compose --profile https up --build -d" +echo "" +echo " 2. Access the dashboard:" +echo " 🌐 https://localhost/ (accept SSL certificate)" +echo " 🌐 https://$TAILSCALE_IP/" +echo "" +echo " 3. Test live recording:" +echo " 📱 Navigate to Live Record page" +echo " 🎤 Microphone access will work over HTTPS" +echo "" +echo "🔧 Services included:" +echo " - Friend-Lite Backend: Internal (proxied through nginx)" +echo " - Web Dashboard: https://localhost/ or https://$TAILSCALE_IP/" +echo " - WebSocket Audio: wss://localhost/ws_pcm or wss://$TAILSCALE_IP/ws_pcm" +echo "" +echo "📚 For more details, see: Docs/HTTPS_SETUP.md" \ No newline at end of file diff --git a/backends/advanced/init.sh b/backends/advanced/init.sh index f3a4498c3..ef51a21ea 100755 --- a/backends/advanced/init.sh +++ b/backends/advanced/init.sh @@ -1,93 +1,360 @@ #!/bin/bash set -e -# Initialize Friend-Lite Advanced Backend with HTTPS proxy -# Usage: ./init.sh +# Friend-Lite Advanced Backend Interactive Setup Script +# Interactive configuration for all services and API keys -if [ $# -ne 1 ]; then - echo "Usage: $0 " - echo "Example: $0 100.83.66.30" +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +MAGENTA='\033[0;35m' +NC='\033[0m' # No Color + +print_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +print_header() { echo "" - echo "This script will:" - echo " 1. Generate SSL certificates for localhost and your Tailscale IP" - echo " 2. Create nginx.conf from template" - echo " 3. Set up HTTPS proxy for the backend" - exit 1 -fi + echo -e "${CYAN}===============================================${NC}" + echo -e "${CYAN}$1${NC}" + echo -e "${CYAN}===============================================${NC}" + echo "" +} -TAILSCALE_IP="$1" +print_section() { + echo "" + echo -e "${MAGENTA}► $1${NC}" + echo -e "${MAGENTA}$(echo "$1" | sed 's/./-/g')${NC}" +} -# Validate IP format (basic check) -if ! echo "$TAILSCALE_IP" | grep -E '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$' > /dev/null; then - echo "Error: Invalid IP format. Expected format: xxx.xxx.xxx.xxx" +# Check if we're in the right directory +if [ ! -f "pyproject.toml" ] || [ ! -d "src" ]; then + print_error "Please run this script from the backends/advanced directory" exit 1 fi -echo "🚀 Initializing Friend-Lite Advanced Backend with Tailscale IP: $TAILSCALE_IP" +print_header "🚀 Friend-Lite Interactive Setup" +echo "This wizard will help you configure Friend-Lite with all necessary services." +echo "We'll ask for your API keys and preferences step by step." echo "" -# Check if nginx.conf.template exists -if [ ! -f "nginx.conf.template" ]; then - echo "❌ Error: nginx.conf.template not found" - echo " Make sure you're running this from the backends/advanced directory" - exit 1 -fi +# Function to prompt yes/no +prompt_yes_no() { + local prompt="$1" + local default="$2" + local response + + if [ "$default" = "y" ]; then + prompt="$prompt [Y/n]: " + else + prompt="$prompt [y/N]: " + fi + + read -p "$prompt" response + response=${response:-$default} + + if [[ "$response" =~ ^[Yy]$ ]]; then + return 0 + else + return 1 + fi +} -# Generate SSL certificates -echo "📄 Step 1: Generating SSL certificates..." -if [ -f "ssl/generate-ssl.sh" ]; then - ./ssl/generate-ssl.sh "$TAILSCALE_IP" - echo "✅ SSL certificates generated" -else - echo "❌ Error: ssl/generate-ssl.sh not found" - exit 1 +# Function to prompt for value with default +prompt_value() { + local prompt="$1" + local default="$2" + local response + + if [ -n "$default" ]; then + read -p "$prompt [$default]: " response + response=${response:-$default} + else + read -p "$prompt: " response + fi + + echo "$response" +} + +# Function to prompt for password (hidden input) +prompt_password() { + local prompt="$1" + local password + read -s -p "$prompt: " password + echo "" + echo "$password" +} + +# Backup existing .env if it exists +if [ -f ".env" ]; then + cp .env .env.backup.$(date +%Y%m%d_%H%M%S) + print_info "Backed up existing .env file" fi +# Start with template +cp .env.template .env.new +print_success "Starting from .env.template" + +# ============================================================================= +# AUTHENTICATION SETUP +# ============================================================================= +print_section "Authentication Setup" +echo "Configure admin account for the dashboard" echo "" -# Create nginx.conf from template -echo "📄 Step 2: Creating nginx configuration..." -sed "s/TAILSCALE_IP/$TAILSCALE_IP/g" nginx.conf.template > nginx.conf -echo "✅ nginx.conf created with IP: $TAILSCALE_IP" +ADMIN_EMAIL=$(prompt_value "Admin email" "admin@example.com") +ADMIN_PASSWORD=$(prompt_password "Admin password (min 8 chars)") +AUTH_SECRET=$(openssl rand -hex 32 2>/dev/null || cat /dev/urandom | head -c 32 | base64) + +sed -i "s|ADMIN_EMAIL=.*|ADMIN_EMAIL=$ADMIN_EMAIL|" .env.new +sed -i "s|ADMIN_PASSWORD=.*|ADMIN_PASSWORD=$ADMIN_PASSWORD|" .env.new +sed -i "s|AUTH_SECRET_KEY=.*|AUTH_SECRET_KEY=$AUTH_SECRET|" .env.new + +print_success "Admin account configured" +# ============================================================================= +# TRANSCRIPTION PROVIDER +# ============================================================================= +print_section "Speech-to-Text Configuration" +echo "Choose your transcription provider:" +echo " 1) Deepgram (recommended - high quality, requires API key)" +echo " 2) Mistral (Voxtral models - requires API key)" +echo " 3) Offline (Parakeet ASR - requires GPU, runs locally)" +echo " 4) None (skip transcription setup)" echo "" -# Update .env file with HTTPS CORS origins -echo "📄 Step 3: Updating CORS origins..." -if [ -f ".env" ]; then - # Update existing .env file - if grep -q "CORS_ORIGINS" .env; then - # Update existing CORS_ORIGINS line - sed -i "s/CORS_ORIGINS=.*/CORS_ORIGINS=https:\/\/localhost,https:\/\/localhost:443,https:\/\/127.0.0.1,https:\/\/$TAILSCALE_IP/" .env - else - # Add CORS_ORIGINS line - echo "CORS_ORIGINS=https://localhost,https://localhost:443,https://127.0.0.1,https://$TAILSCALE_IP" >> .env - fi - echo "✅ Updated CORS origins in .env file" -else - echo "⚠️ No .env file found. You may need to:" - echo " 1. Copy .env.template to .env" - echo " 2. Add: CORS_ORIGINS=https://localhost,https://localhost:443,https://127.0.0.1,https://$TAILSCALE_IP" +TRANSCRIPTION_CHOICE=$(prompt_value "Enter choice (1-4)" "1") + +case $TRANSCRIPTION_CHOICE in + 1) + TRANSCRIPTION_PROVIDER="deepgram" + print_info "Deepgram selected" + echo "Get your API key from: https://console.deepgram.com/" + DEEPGRAM_API_KEY=$(prompt_value "Deepgram API key" "") + if [ -n "$DEEPGRAM_API_KEY" ]; then + sed -i "s|DEEPGRAM_API_KEY=.*|DEEPGRAM_API_KEY=$DEEPGRAM_API_KEY|" .env.new + sed -i "s|TRANSCRIPTION_PROVIDER=.*|TRANSCRIPTION_PROVIDER=deepgram|" .env.new + print_success "Deepgram configured" + else + print_warning "No API key provided - transcription will not work" + fi + ;; + 2) + TRANSCRIPTION_PROVIDER="mistral" + print_info "Mistral selected" + echo "Get your API key from: https://console.mistral.ai/" + MISTRAL_API_KEY=$(prompt_value "Mistral API key" "") + MISTRAL_MODEL=$(prompt_value "Mistral model" "voxtral-mini-2507") + if [ -n "$MISTRAL_API_KEY" ]; then + sed -i "s|# MISTRAL_API_KEY=.*|MISTRAL_API_KEY=$MISTRAL_API_KEY|" .env.new + sed -i "s|# MISTRAL_MODEL=.*|MISTRAL_MODEL=$MISTRAL_MODEL|" .env.new + sed -i "s|TRANSCRIPTION_PROVIDER=.*|TRANSCRIPTION_PROVIDER=mistral|" .env.new + print_success "Mistral configured" + else + print_warning "No API key provided - transcription will not work" + fi + ;; + 3) + TRANSCRIPTION_PROVIDER="offline" + print_info "Offline Parakeet ASR selected" + PARAKEET_URL=$(prompt_value "Parakeet ASR URL" "http://host.docker.internal:8767") + echo "PARAKEET_ASR_URL=$PARAKEET_URL" >> .env.new + print_warning "Remember to start Parakeet service: cd ../../extras/asr-services && docker compose up parakeet" + ;; + 4) + print_info "Skipping transcription setup" + ;; +esac + +# ============================================================================= +# LLM PROVIDER +# ============================================================================= +print_section "LLM Provider Configuration" +echo "Choose your LLM provider for memory extraction:" +echo " 1) OpenAI (GPT-4, GPT-3.5 - requires API key)" +echo " 2) Ollama (local models - requires Ollama server)" +echo " 3) Skip (no memory extraction)" +echo "" + +LLM_CHOICE=$(prompt_value "Enter choice (1-3)" "1") + +case $LLM_CHOICE in + 1) + LLM_PROVIDER="openai" + print_info "OpenAI selected" + echo "Get your API key from: https://platform.openai.com/api-keys" + OPENAI_API_KEY=$(prompt_value "OpenAI API key" "") + OPENAI_MODEL=$(prompt_value "OpenAI model" "gpt-4o-mini") + OPENAI_BASE_URL=$(prompt_value "OpenAI base URL (for proxies/compatible APIs)" "https://api.openai.com/v1") + + if [ -n "$OPENAI_API_KEY" ]; then + sed -i "s|OPENAI_API_KEY=.*|OPENAI_API_KEY=$OPENAI_API_KEY|" .env.new + sed -i "s|OPENAI_MODEL=.*|OPENAI_MODEL=$OPENAI_MODEL|" .env.new + sed -i "s|OPENAI_BASE_URL=.*|OPENAI_BASE_URL=$OPENAI_BASE_URL|" .env.new + sed -i "s|LLM_PROVIDER=.*|LLM_PROVIDER=openai|" .env.new + print_success "OpenAI configured" + else + print_warning "No API key provided - memory extraction will not work" + fi + ;; + 2) + LLM_PROVIDER="ollama" + print_info "Ollama selected" + OLLAMA_BASE_URL=$(prompt_value "Ollama server URL" "http://host.docker.internal:11434") + OLLAMA_MODEL=$(prompt_value "Ollama model" "llama3.2") + + sed -i "s|# OLLAMA_BASE_URL=.*|OLLAMA_BASE_URL=$OLLAMA_BASE_URL|" .env.new + sed -i "s|# OLLAMA_MODEL=.*|OLLAMA_MODEL=$OLLAMA_MODEL|" .env.new + sed -i "s|LLM_PROVIDER=.*|LLM_PROVIDER=ollama|" .env.new + print_success "Ollama configured" + print_warning "Make sure Ollama is running and the model is pulled" + ;; + 3) + print_info "Skipping LLM setup - memory extraction disabled" + ;; +esac + +# ============================================================================= +# MEMORY PROVIDER +# ============================================================================= +print_section "Memory Storage Configuration" +echo "Choose your memory storage backend:" +echo " 1) Friend-Lite Native (Qdrant + custom extraction)" +echo " 2) OpenMemory MCP (cross-client compatible, external server)" +echo "" + +MEMORY_CHOICE=$(prompt_value "Enter choice (1-2)" "1") + +case $MEMORY_CHOICE in + 1) + MEMORY_PROVIDER="friend_lite" + print_info "Friend-Lite Native memory provider selected" + sed -i "s|MEMORY_PROVIDER=.*|MEMORY_PROVIDER=friend_lite|" .env.new + + # Qdrant configuration + QDRANT_URL=$(prompt_value "Qdrant URL" "qdrant") + sed -i "s|QDRANT_BASE_URL=.*|QDRANT_BASE_URL=$QDRANT_URL|" .env.new + print_success "Friend-Lite memory provider configured" + ;; + 2) + MEMORY_PROVIDER="openmemory_mcp" + print_info "OpenMemory MCP selected" + OPENMEMORY_URL=$(prompt_value "OpenMemory MCP server URL" "http://host.docker.internal:8765") + OPENMEMORY_CLIENT=$(prompt_value "OpenMemory client name" "friend_lite") + OPENMEMORY_USER=$(prompt_value "OpenMemory user ID" "openmemory") + + sed -i "s|MEMORY_PROVIDER=.*|MEMORY_PROVIDER=openmemory_mcp|" .env.new + sed -i "s|# OPENMEMORY_MCP_URL=.*|OPENMEMORY_MCP_URL=$OPENMEMORY_URL|" .env.new + sed -i "s|# OPENMEMORY_CLIENT_NAME=.*|OPENMEMORY_CLIENT_NAME=$OPENMEMORY_CLIENT|" .env.new + sed -i "s|# OPENMEMORY_USER_ID=.*|OPENMEMORY_USER_ID=$OPENMEMORY_USER|" .env.new + print_success "OpenMemory MCP configured" + print_warning "Remember to start OpenMemory: cd ../../extras/openmemory-mcp && docker compose up -d" + ;; +esac + +# ============================================================================= +# OPTIONAL SERVICES +# ============================================================================= +print_section "Optional Services" + +if prompt_yes_no "Enable Speaker Recognition?" "n"; then + SPEAKER_URL=$(prompt_value "Speaker Recognition service URL" "http://host.docker.internal:8001") + echo "SPEAKER_SERVICE_URL=$SPEAKER_URL" >> .env.new + print_success "Speaker Recognition configured" + print_info "Start with: cd ../../extras/speaker-recognition && docker compose up -d" +fi + +# ============================================================================= +# NETWORK CONFIGURATION +# ============================================================================= +print_section "Network Configuration" + +HOST_IP=$(prompt_value "Host IP for services" "localhost") +BACKEND_PORT=$(prompt_value "Backend port" "8000") +WEBUI_PORT=$(prompt_value "Web UI port" "5173") + +sed -i "s|HOST_IP=.*|HOST_IP=$HOST_IP|" .env.new +sed -i "s|BACKEND_PUBLIC_PORT=.*|BACKEND_PUBLIC_PORT=$BACKEND_PORT|" .env.new +sed -i "s|WEBUI_PORT=.*|WEBUI_PORT=$WEBUI_PORT|" .env.new + +# ============================================================================= +# FINALIZE +# ============================================================================= +print_header "Configuration Complete!" + +# Move new .env into place +mv .env.new .env +print_success ".env file created successfully" + +# Copy other configuration files +if [ ! -f "memory_config.yaml" ]; then + cp memory_config.yaml.template memory_config.yaml + print_success "memory_config.yaml created" fi +if [ ! -f "diarization_config.json" ]; then + cp diarization_config.json.template diarization_config.json + print_success "diarization_config.json created" +fi + +# ============================================================================= +# SUMMARY +# ============================================================================= +print_section "Configuration Summary" echo "" -echo "🎉 Initialization complete!" +echo "✅ Admin Account: $ADMIN_EMAIL" +echo "✅ Transcription: $TRANSCRIPTION_PROVIDER" +echo "✅ LLM Provider: $LLM_PROVIDER" +echo "✅ Memory Provider: $MEMORY_PROVIDER" +echo "✅ Backend URL: http://$HOST_IP:$BACKEND_PORT" +echo "✅ Dashboard URL: http://$HOST_IP:$WEBUI_PORT" + +# ============================================================================= +# NEXT STEPS +# ============================================================================= +print_section "Next Steps" echo "" -echo "Next steps:" -echo " 1. Start the services:" -echo " docker compose --profile https up --build -d" +echo "1. Start the main services:" +echo " ${CYAN}docker compose up --build -d${NC}" echo "" -echo " 2. Access the dashboard:" -echo " 🌐 https://localhost/ (accept SSL certificate)" -echo " 🌐 https://$TAILSCALE_IP/" +echo "2. Access the dashboard:" +echo " ${CYAN}http://$HOST_IP:$WEBUI_PORT${NC}" echo "" -echo " 3. Test live recording:" -echo " 📱 Navigate to Live Record page" -echo " 🎤 Microphone access will work over HTTPS" +echo "3. Check service health:" +echo " ${CYAN}curl http://$HOST_IP:$BACKEND_PORT/health${NC}" + +if [ "$MEMORY_PROVIDER" = "openmemory_mcp" ]; then + echo "" + echo "4. Start OpenMemory MCP:" + echo " ${CYAN}cd ../../extras/openmemory-mcp && docker compose up -d${NC}" +fi + +if [ "$TRANSCRIPTION_PROVIDER" = "offline" ]; then + echo "" + echo "5. Start Parakeet ASR:" + echo " ${CYAN}cd ../../extras/asr-services && docker compose up parakeet -d${NC}" +fi + echo "" -echo "🔧 Services included:" -echo " - Friend-Lite Backend: Internal (proxied through nginx)" -echo " - Web Dashboard: https://localhost/ or https://$TAILSCALE_IP/" -echo " - WebSocket Audio: wss://localhost/ws_pcm or wss://$TAILSCALE_IP/ws_pcm" +print_success "Setup complete! 🎉" echo "" -echo "📚 For more details, see: Docs/HTTPS_SETUP.md" \ No newline at end of file +echo "For detailed documentation, see:" +echo " • Docs/quickstart.md" +echo " • MEMORY_PROVIDERS.md" +echo " • CLAUDE.md" \ No newline at end of file diff --git a/backends/advanced/run-test.sh b/backends/advanced/run-test.sh index 96d2ea1ac..a53a81fe1 100755 --- a/backends/advanced/run-test.sh +++ b/backends/advanced/run-test.sh @@ -67,6 +67,20 @@ fi print_info "DEEPGRAM_API_KEY length: ${#DEEPGRAM_API_KEY}" print_info "OPENAI_API_KEY length: ${#OPENAI_API_KEY}" +# Ensure memory_config.yaml exists +if [ ! -f "memory_config.yaml" ] && [ -f "memory_config.yaml.template" ]; then + print_info "Creating memory_config.yaml from template..." + cp memory_config.yaml.template memory_config.yaml + print_success "memory_config.yaml created" +fi + +# Ensure diarization_config.json exists +if [ ! -f "diarization_config.json" ] && [ -f "diarization_config.json.template" ]; then + print_info "Creating diarization_config.json from template..." + cp diarization_config.json.template diarization_config.json + print_success "diarization_config.json created" +fi + # Install dependencies with uv print_info "Installing dependencies with uv..." uv sync --dev --group test diff --git a/backends/advanced/src/advanced_omi_backend/controllers/memory_controller.py b/backends/advanced/src/advanced_omi_backend/controllers/memory_controller.py index 19ada8026..135dbeb0f 100644 --- a/backends/advanced/src/advanced_omi_backend/controllers/memory_controller.py +++ b/backends/advanced/src/advanced_omi_backend/controllers/memory_controller.py @@ -27,8 +27,21 @@ async def get_memories(user: User, limit: int, user_id: Optional[str] = None): # Execute memory retrieval directly (now async) memories = await memory_service.get_all_memories(target_user_id, limit) + + # Get total count if supported (graceful fallback) + total_count = None + try: + if hasattr(memory_service, 'count_memories'): + total_count = await memory_service.count_memories(target_user_id) + except Exception as e: + audio_logger.warning(f"Count memories not supported or failed: {e}") - return {"memories": memories, "count": len(memories), "user_id": target_user_id} + return { + "memories": memories, + "count": len(memories), + "total_count": total_count, + "user_id": target_user_id + } except Exception as e: audio_logger.error(f"Error fetching memories: {e}", exc_info=True) diff --git a/backends/advanced/src/advanced_omi_backend/controllers/system_controller.py b/backends/advanced/src/advanced_omi_backend/controllers/system_controller.py index 14bba5958..da524cf0d 100644 --- a/backends/advanced/src/advanced_omi_backend/controllers/system_controller.py +++ b/backends/advanced/src/advanced_omi_backend/controllers/system_controller.py @@ -912,3 +912,142 @@ async def save_diarization_settings(settings: dict): return JSONResponse( status_code=500, content={"error": f"Failed to save settings: {str(e)}"} ) + + +async def get_speaker_configuration(user: User): + """Get current user's primary speakers configuration.""" + try: + return { + "primary_speakers": user.primary_speakers, + "user_id": user.user_id, + "status": "success" + } + except Exception as e: + logger.error(f"Error getting speaker configuration for user {user.user_id}: {e}") + return JSONResponse( + status_code=500, content={"error": f"Failed to get speaker configuration: {str(e)}"} + ) + + +async def update_speaker_configuration(user: User, primary_speakers: list[dict]): + """Update current user's primary speakers configuration.""" + try: + # Validate speaker data format + for speaker in primary_speakers: + if not isinstance(speaker, dict): + return JSONResponse( + status_code=400, content={"error": "Each speaker must be a dictionary"} + ) + + required_fields = ["speaker_id", "name", "user_id"] + for field in required_fields: + if field not in speaker: + return JSONResponse( + status_code=400, content={"error": f"Missing required field: {field}"} + ) + + # Add timestamp to each speaker + from datetime import UTC, datetime + for speaker in primary_speakers: + speaker["selected_at"] = datetime.now(UTC).isoformat() + + # Update user model + user.primary_speakers = primary_speakers + await user.save() + + logger.info(f"Updated primary speakers configuration for user {user.user_id}: {len(primary_speakers)} speakers") + + return { + "message": "Primary speakers configuration updated successfully", + "primary_speakers": primary_speakers, + "count": len(primary_speakers), + "status": "success" + } + + except Exception as e: + logger.error(f"Error updating speaker configuration for user {user.user_id}: {e}") + return JSONResponse( + status_code=500, content={"error": f"Failed to update speaker configuration: {str(e)}"} + ) + + +async def get_enrolled_speakers(user: User): + """Get enrolled speakers from speaker recognition service.""" + try: + from advanced_omi_backend.speaker_recognition_client import SpeakerRecognitionClient + + # Initialize speaker recognition client + speaker_client = SpeakerRecognitionClient() + + if not speaker_client.enabled: + return { + "speakers": [], + "service_available": False, + "message": "Speaker recognition service is not configured or disabled", + "status": "success" + } + + # Get enrolled speakers - using hardcoded user_id=1 for now (as noted in speaker_recognition_client.py) + speakers = await speaker_client.get_enrolled_speakers(user_id="1") + + return { + "speakers": speakers.get("speakers", []) if speakers else [], + "service_available": True, + "message": "Successfully retrieved enrolled speakers", + "status": "success" + } + + except Exception as e: + logger.error(f"Error getting enrolled speakers for user {user.user_id}: {e}") + return { + "speakers": [], + "service_available": False, + "message": f"Failed to retrieve speakers: {str(e)}", + "status": "error" + } + + +async def get_speaker_service_status(): + """Check speaker recognition service health status.""" + try: + from advanced_omi_backend.speaker_recognition_client import SpeakerRecognitionClient + + # Initialize speaker recognition client + speaker_client = SpeakerRecognitionClient() + + if not speaker_client.enabled: + return { + "service_available": False, + "healthy": False, + "message": "Speaker recognition service is not configured or disabled", + "status": "disabled" + } + + # Perform health check + health_result = await speaker_client.health_check() + + if health_result: + return { + "service_available": True, + "healthy": True, + "message": "Speaker recognition service is healthy", + "service_url": speaker_client.service_url, + "status": "healthy" + } + else: + return { + "service_available": False, + "healthy": False, + "message": "Speaker recognition service is not responding", + "service_url": speaker_client.service_url, + "status": "unhealthy" + } + + except Exception as e: + logger.error(f"Error checking speaker service status: {e}") + return { + "service_available": False, + "healthy": False, + "message": f"Health check failed: {str(e)}", + "status": "error" + } diff --git a/backends/advanced/src/advanced_omi_backend/main.py b/backends/advanced/src/advanced_omi_backend/main.py index f04ace2ff..7fcea4b8c 100644 --- a/backends/advanced/src/advanced_omi_backend/main.py +++ b/backends/advanced/src/advanced_omi_backend/main.py @@ -906,6 +906,11 @@ async def health_check(): overall_healthy = True critical_services_healthy = True + + # Get configuration once at the start + memory_provider = os.getenv("MEMORY_PROVIDER", "friend_lite") + speaker_service_url = os.getenv("SPEAKER_SERVICE_URL") + openmemory_mcp_url = os.getenv("OPENMEMORY_MCP_URL") # Check MongoDB (critical service) try: @@ -961,34 +966,55 @@ async def health_check(): } overall_healthy = False - # Check mem0 (depends on Ollama and Qdrant) - try: - # Test memory service connection with timeout - test_success = await memory_service.test_connection() - if test_success: - health_status["services"]["mem0"] = { - "status": "✅ Connected", - "healthy": True, + # Check memory service (provider-dependent) + if memory_provider == "friend_lite": + try: + # Test Friend-Lite memory service connection with timeout + test_success = await memory_service.test_connection() + if test_success: + health_status["services"]["memory_service"] = { + "status": "✅ Friend-Lite Memory Connected", + "healthy": True, + "provider": "friend_lite", + "critical": False, + } + else: + health_status["services"]["memory_service"] = { + "status": "⚠️ Friend-Lite Memory Test Failed", + "healthy": False, + "provider": "friend_lite", + "critical": False, + } + overall_healthy = False + except asyncio.TimeoutError: + health_status["services"]["memory_service"] = { + "status": "⚠️ Friend-Lite Memory Timeout (60s) - Check Qdrant", + "healthy": False, + "provider": "friend_lite", "critical": False, } - else: - health_status["services"]["mem0"] = { - "status": "⚠️ Connection Test Failed", + overall_healthy = False + except Exception as e: + health_status["services"]["memory_service"] = { + "status": f"⚠️ Friend-Lite Memory Failed: {str(e)}", "healthy": False, + "provider": "friend_lite", "critical": False, } overall_healthy = False - except asyncio.TimeoutError: - health_status["services"]["mem0"] = { - "status": "⚠️ Connection Test Timeout (60s) - Depends on Ollama/Qdrant", - "healthy": False, + elif memory_provider == "openmemory_mcp": + # OpenMemory MCP check is handled separately above + health_status["services"]["memory_service"] = { + "status": "✅ Using OpenMemory MCP", + "healthy": True, + "provider": "openmemory_mcp", "critical": False, } - overall_healthy = False - except Exception as e: - health_status["services"]["mem0"] = { - "status": f"⚠️ Connection Test Failed: {str(e)} - Check Ollama/Qdrant services", + else: + health_status["services"]["memory_service"] = { + "status": f"❌ Unknown memory provider: {memory_provider}", "healthy": False, + "provider": memory_provider, "critical": False, } overall_healthy = False @@ -1033,7 +1059,6 @@ async def health_check(): overall_healthy = False # Check Speaker Recognition service (non-critical - optional feature) - speaker_service_url = os.getenv("SPEAKER_SERVICE_URL") if speaker_service_url: try: # Make a health check request to the speaker service @@ -1073,6 +1098,50 @@ async def health_check(): } overall_healthy = False + # Check OpenMemory MCP service (if configured) + if memory_provider == "openmemory_mcp" and openmemory_mcp_url: + try: + # Make a health check request to the OpenMemory MCP service + async with aiohttp.ClientSession() as session: + async with session.get( + f"{openmemory_mcp_url}/docs", timeout=aiohttp.ClientTimeout(total=5) + ) as response: + if response.status == 200: + health_status["services"]["openmemory_mcp"] = { + "status": "✅ Connected", + "healthy": True, + "url": openmemory_mcp_url, + "provider": "openmemory_mcp", + "critical": False, + } + else: + health_status["services"]["openmemory_mcp"] = { + "status": f"⚠️ Unhealthy: HTTP {response.status}", + "healthy": False, + "url": openmemory_mcp_url, + "provider": "openmemory_mcp", + "critical": False, + } + overall_healthy = False + except asyncio.TimeoutError: + health_status["services"]["openmemory_mcp"] = { + "status": "⚠️ Connection Timeout (5s)", + "healthy": False, + "url": openmemory_mcp_url, + "provider": "openmemory_mcp", + "critical": False, + } + overall_healthy = False + except Exception as e: + health_status["services"]["openmemory_mcp"] = { + "status": f"⚠️ Connection Failed: {str(e)}", + "healthy": False, + "url": openmemory_mcp_url, + "provider": "openmemory_mcp", + "critical": False, + } + overall_healthy = False + # Track health check results in debug tracker try: # Can add health check tracking to debug tracker if needed diff --git a/backends/advanced/src/advanced_omi_backend/memory/base.py b/backends/advanced/src/advanced_omi_backend/memory/base.py index d1891a093..48afd0245 100644 --- a/backends/advanced/src/advanced_omi_backend/memory/base.py +++ b/backends/advanced/src/advanced_omi_backend/memory/base.py @@ -133,6 +133,20 @@ async def get_all_memories( """ pass + async def count_memories(self, user_id: str) -> Optional[int]: + """Count total number of memories for a user. + + This is an optional method that providers can implement for efficient + counting. Returns None if the provider doesn't support counting. + + Args: + user_id: User identifier + + Returns: + Total count of memories for the user, or None if not supported + """ + return None + @abstractmethod async def delete_memory(self, memory_id: str) -> bool: """Delete a specific memory by ID. @@ -306,6 +320,20 @@ async def get_memories(self, user_id: str, limit: int) -> List[MemoryEntry]: """ pass + async def count_memories(self, user_id: str) -> int: + """Count total number of memories for a user. + + Default implementation returns 0. Vector stores should override + this method to provide efficient counting if supported. + + Args: + user_id: User identifier + + Returns: + Total count of memories for the user + """ + return 0 + @abstractmethod async def update_memory( self, diff --git a/backends/advanced/src/advanced_omi_backend/memory/compat_service.py b/backends/advanced/src/advanced_omi_backend/memory/compat_service.py index 430d2fab0..79cfd1efa 100644 --- a/backends/advanced/src/advanced_omi_backend/memory/compat_service.py +++ b/backends/advanced/src/advanced_omi_backend/memory/compat_service.py @@ -134,6 +134,21 @@ async def get_all_memories(self, user_id: str, limit: int = 100) -> List[Dict[st for memory in memories ] + async def count_memories(self, user_id: str) -> Optional[int]: + """Count total number of memories for a user. + + Args: + user_id: User identifier + + Returns: + Total count of memories for the user, or None if not supported + """ + if not self._initialized: + await self.initialize() + + # Delegate to the core service + return await self._service.count_memories(user_id) + async def get_all_memories_unfiltered(self, user_id: str, limit: int = 100) -> List[Dict[str, Any]]: """Get all memories without filtering - same as get_all_memories in new implementation. diff --git a/backends/advanced/src/advanced_omi_backend/memory/memory_service.py b/backends/advanced/src/advanced_omi_backend/memory/memory_service.py index f7285a8e5..5cba626c0 100644 --- a/backends/advanced/src/advanced_omi_backend/memory/memory_service.py +++ b/backends/advanced/src/advanced_omi_backend/memory/memory_service.py @@ -272,6 +272,28 @@ async def get_all_memories(self, user_id: str, limit: int = 100) -> List[MemoryE memory_logger.error(f"Get all memories failed: {e}") return [] + async def count_memories(self, user_id: str) -> Optional[int]: + """Count total number of memories for a user. + + Uses the vector store's native count capabilities. + + Args: + user_id: User identifier + + Returns: + Total count of memories for the user, or None if not supported + """ + if not self._initialized: + await self.initialize() + + try: + count = await self.vector_store.count_memories(user_id) + memory_logger.info(f"🔢 Total {count} memories for user {user_id}") + return count + except Exception as e: + memory_logger.error(f"Count memories failed: {e}") + return None + async def delete_memory(self, memory_id: str) -> bool: """Delete a specific memory by ID. diff --git a/backends/advanced/src/advanced_omi_backend/memory/providers/vector_stores.py b/backends/advanced/src/advanced_omi_backend/memory/providers/vector_stores.py index 0b60eec7b..0e60ac0b9 100644 --- a/backends/advanced/src/advanced_omi_backend/memory/providers/vector_stores.py +++ b/backends/advanced/src/advanced_omi_backend/memory/providers/vector_stores.py @@ -6,12 +6,19 @@ Vector stores handle storage, retrieval, and similarity search of memory embeddings. """ -import time import logging +import time import uuid -from typing import List, Dict, Any +from typing import Any, Dict, List + +from qdrant_client.models import ( + FieldCondition, + Filter, + FilterSelector, + MatchValue, +) -from ..base import VectorStoreBase, MemoryEntry +from ..base import MemoryEntry, VectorStoreBase memory_logger = logging.getLogger("memory_service") @@ -144,8 +151,6 @@ async def add_memories(self, memories: List[MemoryEntry]) -> List[str]: async def search_memories(self, query_embedding: List[float], user_id: str, limit: int) -> List[MemoryEntry]: """Search memories in Qdrant.""" try: - from qdrant_client.models import Filter, FieldCondition, MatchValue - # Filter by user_id search_filter = Filter( must=[ @@ -169,8 +174,8 @@ async def search_memories(self, query_embedding: List[float], user_id: str, limi id=str(result.id), content=result.payload.get("content", ""), metadata=result.payload.get("metadata", {}), - # Convert Qdrant cosine distance to similarity [0..1] - score=(1.0 - result.score) if result.score is not None else None, + # Qdrant returns similarity scores directly (higher = more similar) + score=result.score if result.score is not None else None, created_at=result.payload.get("created_at") ) memories.append(memory) @@ -184,8 +189,6 @@ async def search_memories(self, query_embedding: List[float], user_id: str, limi async def get_memories(self, user_id: str, limit: int) -> List[MemoryEntry]: """Get all memories for a user from Qdrant.""" try: - from qdrant_client.models import Filter, FieldCondition, MatchValue - # Filter by user_id search_filter = Filter( must=[ @@ -248,8 +251,6 @@ async def delete_memory(self, memory_id: str) -> bool: async def delete_user_memories(self, user_id: str) -> int: """Delete all memories for a user from Qdrant.""" try: - from qdrant_client.models import Filter, FieldCondition, MatchValue, FilterSelector - # First count memories to delete memories = await self.get_memories(user_id, limit=10000) count = len(memories) @@ -335,6 +336,32 @@ async def update_memory( memory_logger.error(f"Qdrant update memory failed: {e}") return False + async def count_memories(self, user_id: str) -> int: + """Count total number of memories for a user in Qdrant using native count API.""" + try: + + search_filter = Filter( + must=[ + FieldCondition( + key="metadata.user_id", + match=MatchValue(value=user_id) + ) + ] + ) + + # Use Qdrant's native count API (documented in qdrant/qdrant/docs) + # Count operation: CountPoints -> CountResponse with count result + result = await self.client.count( + collection_name=self.collection_name, + count_filter=search_filter + ) + + return result.count + + except Exception as e: + memory_logger.error(f"Qdrant count memories failed: {e}") + return 0 + diff --git a/backends/advanced/src/advanced_omi_backend/processors.py b/backends/advanced/src/advanced_omi_backend/processors.py index 365b4fd06..20c2e921b 100644 --- a/backends/advanced/src/advanced_omi_backend/processors.py +++ b/backends/advanced/src/advanced_omi_backend/processors.py @@ -159,6 +159,69 @@ async def start(self): logger.info("All processors started successfully") + async def _should_process_memory(self, user_id: str, audio_uuid: str) -> tuple[bool, str]: + """ + Determine if memory processing should proceed based on primary speakers configuration. + + Implements graceful degradation: + - No primary speakers configured → Process all (True) + - Speaker service unavailable → Process all (True) + - No speakers identified → Process all (True) + - Primary speakers found → Process (True) + - Only non-primary speakers → Skip (False) + + Args: + user_id: User ID to check primary speakers configuration + audio_uuid: Audio UUID to check transcript speakers + + Returns: + Tuple of (should_process: bool, reason: str) + """ + try: + # Get user's primary speaker configuration + from advanced_omi_backend.users import get_user_by_id + + user = await get_user_by_id(user_id) + if not user or not user.primary_speakers: + return True, "No primary speakers configured - processing all conversations" + + audio_logger.info(f"🔍 Checking primary speakers filter for {audio_uuid} - user has {len(user.primary_speakers)} primary speakers configured") + + # Get transcript with speaker identification + chunk = await self.repository.get_chunk_by_audio_uuid(audio_uuid) + if not chunk or not chunk.get('transcript'): + return True, "No transcript data available - processing conversation" + + # Extract speakers from transcript segments + transcript_speakers = set() + total_segments = 0 + identified_segments = 0 + + for segment in chunk['transcript']: + total_segments += 1 + if 'identified_as' in segment and segment['identified_as'] and segment['identified_as'] != 'Unknown': + transcript_speakers.add(segment['identified_as']) + identified_segments += 1 + + if not transcript_speakers: + return True, f"No speakers identified in transcript ({identified_segments}/{total_segments} segments) - processing conversation" + + # Check if any primary speakers are present + primary_speaker_names = {ps['name'] for ps in user.primary_speakers} + found_primary_speakers = transcript_speakers.intersection(primary_speaker_names) + + if found_primary_speakers: + audio_logger.info(f"✅ Primary speakers found in conversation: {found_primary_speakers} - processing memory") + return True, f"Primary speakers detected: {', '.join(found_primary_speakers)}" + else: + audio_logger.info(f"❌ No primary speakers found - transcript speakers: {transcript_speakers}, primary speakers: {primary_speaker_names} - skipping memory processing") + return False, f"Only non-primary speakers found: {', '.join(transcript_speakers)}" + + except Exception as e: + # On any error, default to processing (fail-safe) + audio_logger.warning(f"Error checking primary speakers filter for {audio_uuid}: {e} - defaulting to process conversation") + return True, f"Error in speaker filtering: {str(e)} - processing conversation as fallback" + async def shutdown(self): """Shutdown all processors gracefully.""" logger.info("Shutting down processors...") @@ -876,6 +939,31 @@ async def _process_memory_item(self, item: MemoryProcessingItem): # Debug tracking removed for cleaner architecture + # Check if memory processing should proceed based on primary speakers configuration + should_process, filter_reason = await self._should_process_memory(item.user_id, item.audio_uuid) + audio_logger.info(f"🎯 Speaker filter decision for {item.audio_uuid}: {filter_reason}") + + if not should_process: + # Update memory processing status to skipped + await self.repository.update_memory_processing_status( + item.audio_uuid, "SKIPPED", error_message=filter_reason + ) + + # Track completion + self.track_processing_stage( + item.client_id, + "memory", + "completed", + { + "audio_uuid": item.audio_uuid, + "status": "skipped", + "reason": filter_reason, + "completed_at": time.time(), + }, + ) + audio_logger.info(f"⏭️ Skipped memory processing for {item.audio_uuid}: {filter_reason}") + return None + # Lazy import memory service if self.memory_service is None: audio_logger.info(f"🔧 Initializing memory service for {item.audio_uuid}...") diff --git a/backends/advanced/src/advanced_omi_backend/routers/modules/system_routes.py b/backends/advanced/src/advanced_omi_backend/routers/modules/system_routes.py index 85e9982a1..782697a56 100644 --- a/backends/advanced/src/advanced_omi_backend/routers/modules/system_routes.py +++ b/backends/advanced/src/advanced_omi_backend/routers/modules/system_routes.py @@ -9,7 +9,7 @@ from fastapi import APIRouter, BackgroundTasks, Depends, File, Query, UploadFile -from advanced_omi_backend.auth import current_superuser +from advanced_omi_backend.auth import current_active_user, current_superuser from advanced_omi_backend.controllers import system_controller from advanced_omi_backend.users import User @@ -101,3 +101,30 @@ async def save_diarization_settings( ): """Save diarization settings. Admin only.""" return await system_controller.save_diarization_settings(settings) + + +@router.get("/speaker-configuration") +async def get_speaker_configuration(current_user: User = Depends(current_active_user)): + """Get current user's primary speakers configuration.""" + return await system_controller.get_speaker_configuration(current_user) + + +@router.post("/speaker-configuration") +async def update_speaker_configuration( + primary_speakers: list[dict], + current_user: User = Depends(current_active_user) +): + """Update current user's primary speakers configuration.""" + return await system_controller.update_speaker_configuration(current_user, primary_speakers) + + +@router.get("/enrolled-speakers") +async def get_enrolled_speakers(current_user: User = Depends(current_active_user)): + """Get enrolled speakers from speaker recognition service.""" + return await system_controller.get_enrolled_speakers(current_user) + + +@router.get("/speaker-service-status") +async def get_speaker_service_status(current_user: User = Depends(current_superuser)): + """Check speaker recognition service health status. Admin only.""" + return await system_controller.get_speaker_service_status() diff --git a/backends/advanced/src/advanced_omi_backend/speaker_recognition_client.py b/backends/advanced/src/advanced_omi_backend/speaker_recognition_client.py index dd7e9495a..d8e601769 100644 --- a/backends/advanced/src/advanced_omi_backend/speaker_recognition_client.py +++ b/backends/advanced/src/advanced_omi_backend/speaker_recognition_client.py @@ -319,3 +319,82 @@ def _process_diarization_result( except Exception as e: logger.error(f"Error processing diarization result: {e}") return {} + + async def get_enrolled_speakers(self, user_id: Optional[str] = None) -> Dict: + """ + Get enrolled speakers from the speaker recognition service. + + Args: + user_id: Optional user ID to filter speakers (for future user isolation) + + Returns: + Dictionary containing speakers list and metadata + """ + if not self.enabled: + return {"speakers": []} + + try: + logger.info(f"Getting enrolled speakers from service: {self.service_url}") + + async with aiohttp.ClientSession() as session: + # Use the /speakers endpoint - currently no user filtering in speaker service + async with session.get( + f"{self.service_url}/speakers", + timeout=aiohttp.ClientTimeout(total=10), + ) as response: + if response.status != 200: + logger.warning( + f"Speaker service returned status {response.status}: {await response.text()}" + ) + return {"speakers": []} + + result = await response.json() + speakers = result.get("speakers", []) + logger.info(f"Retrieved {len(speakers)} enrolled speakers from service") + return result + + except aiohttp.ClientError as e: + logger.warning(f"Failed to connect to speaker recognition service: {e}") + return {"speakers": []} + except Exception as e: + logger.error(f"Error getting enrolled speakers: {e}") + return {"speakers": []} + + async def health_check(self) -> bool: + """ + Check if the speaker recognition service is healthy and responding. + + Returns: + True if service is healthy, False otherwise + """ + if not self.enabled: + return False + + try: + logger.debug(f"Performing health check on speaker service: {self.service_url}") + + async with aiohttp.ClientSession() as session: + # Use the /health endpoint if available, otherwise try a simple endpoint + health_endpoints = ["/health", "/speakers"] + + for endpoint in health_endpoints: + try: + async with session.get( + f"{self.service_url}{endpoint}", + timeout=aiohttp.ClientTimeout(total=5), + ) as response: + if response.status == 200: + logger.debug(f"Speaker service health check passed via {endpoint}") + return True + else: + logger.debug(f"Health check endpoint {endpoint} returned {response.status}") + except Exception as endpoint_error: + logger.debug(f"Health check failed for {endpoint}: {endpoint_error}") + continue + + logger.warning("All health check endpoints failed") + return False + + except Exception as e: + logger.error(f"Error during speaker service health check: {e}") + return False diff --git a/backends/advanced/src/advanced_omi_backend/users.py b/backends/advanced/src/advanced_omi_backend/users.py index 14cd2ba1f..b016c68cb 100644 --- a/backends/advanced/src/advanced_omi_backend/users.py +++ b/backends/advanced/src/advanced_omi_backend/users.py @@ -23,6 +23,7 @@ class UserRead(BaseUser[PydanticObjectId]): display_name: Optional[str] = None registered_clients: dict[str, dict] = Field(default_factory=dict) + primary_speakers: list[dict] = Field(default_factory=list) class UserUpdate(BaseUserUpdate): @@ -37,6 +38,8 @@ class User(BeanieBaseUser, Document): display_name: Optional[str] = None # Client tracking for audio devices registered_clients: dict[str, dict] = Field(default_factory=dict) + # Speaker processing filter configuration + primary_speakers: list[dict] = Field(default_factory=list) @property def user_id(self) -> str: diff --git a/backends/advanced/webui/src/pages/Memories.tsx b/backends/advanced/webui/src/pages/Memories.tsx index e197843f8..ce17772e4 100644 --- a/backends/advanced/webui/src/pages/Memories.tsx +++ b/backends/advanced/webui/src/pages/Memories.tsx @@ -1,7 +1,8 @@ import { useState, useEffect } from 'react' -import { Brain, Search, RefreshCw, Trash2, Calendar, Tag } from 'lucide-react' +import { Brain, Search, RefreshCw, Trash2, Calendar, Tag, X, Target } from 'lucide-react' import { memoriesApi } from '../services/api' import { useAuth } from '../contexts/AuthContext' +import '../styles/slider.css' interface Memory { id: string @@ -22,6 +23,14 @@ export default function Memories() { const [error, setError] = useState(null) const [searchQuery, setSearchQuery] = useState('') const [showUnfiltered, setShowUnfiltered] = useState(false) + const [totalCount, setTotalCount] = useState(null) + + // Semantic search state + const [semanticResults, setSemanticResults] = useState([]) + const [isSemanticFilterActive, setIsSemanticFilterActive] = useState(false) + const [semanticQuery, setSemanticQuery] = useState('') + const [semanticLoading, setSemanticLoading] = useState(false) + const [relevanceThreshold, setRelevanceThreshold] = useState(0) // 0-100 percentage const { user } = useAuth() @@ -38,7 +47,9 @@ export default function Memories() { // Handle the API response structure const memoriesData = response.data.memories || response.data || [] + const totalCount = response.data.total_count console.log('🧠 Processed memories data:', memoriesData) + console.log('🧠 Total count:', totalCount) // Log first few memories to inspect structure if (memoriesData.length > 0) { @@ -47,6 +58,8 @@ export default function Memories() { } setMemories(Array.isArray(memoriesData) ? memoriesData : []) + // Store total count in state for display + setTotalCount(totalCount) setError(null) } catch (err: any) { console.error('❌ Memory loading error:', err) @@ -60,18 +73,63 @@ export default function Memories() { loadMemories() }, [user?.id, showUnfiltered]) + // Semantic search handlers + const handleSemanticSearch = async () => { + if (!searchQuery.trim() || !user?.id) return + + try { + setSemanticLoading(true) + const response = await memoriesApi.search(searchQuery.trim(), user.id, 50) + + setSemanticResults(response.data.results || []) + setSemanticQuery(searchQuery.trim()) + setIsSemanticFilterActive(true) + setSearchQuery('') // Clear search box + setError(null) + } catch (err: any) { + setError(err.message || 'Semantic search failed') + } finally { + setSemanticLoading(false) + } + } + + const clearSemanticFilter = () => { + setIsSemanticFilterActive(false) + setSemanticResults([]) + setSemanticQuery('') + setSearchQuery('') + setRelevanceThreshold(0) // Reset threshold + } + const handleDeleteMemory = async (memoryId: string) => { if (!confirm('Are you sure you want to delete this memory?')) return try { await memoriesApi.delete(memoryId) + // Remove from both regular memories and semantic results if present setMemories(memories.filter(m => m.id !== memoryId)) + if (isSemanticFilterActive) { + setSemanticResults(semanticResults.filter(m => m.id !== memoryId)) + } } catch (err: any) { setError(err.message || 'Failed to delete memory') } } - const filteredMemories = memories.filter(memory => + // Update filtering logic for dual-layer search with relevance threshold + const currentMemories = isSemanticFilterActive ? semanticResults : memories + + // Apply relevance threshold filter for semantic results + const relevanceFilteredMemories = isSemanticFilterActive + ? currentMemories.filter(memory => { + if (!memory.score) return true // If no score, show it + const relevancePercentage = memory.score * 100 + return relevancePercentage >= relevanceThreshold + }) + : currentMemories + + // Apply text search filter + const filteredMemories = relevanceFilteredMemories.filter(memory => memory.memory.toLowerCase().includes(searchQuery.toLowerCase()) || (memory.category?.toLowerCase() || '').includes(searchQuery.toLowerCase()) ) @@ -184,25 +242,123 @@ export default function Memories() { {/* Search */} {memories.length > 0 && ( -
- +
+ setSearchQuery(e.target.value)} placeholder="Search memories..." - className="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500" + className="w-full pl-10 pr-32 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500" + onKeyPress={(e) => e.key === 'Enter' && handleSemanticSearch()} /> +
)}
+ {/* Semantic Filter Indicator with Relevance Slider */} + {isSemanticFilterActive && ( +
+
+
+
+ + + Semantic filter active: "{semanticQuery}" + +
+ +
+
+ + {/* Relevance Threshold Slider */} +
+
+ +
+ + {relevanceThreshold}% + + {relevanceThreshold > 0 && ( + + ({relevanceFilteredMemories.length} of {semanticResults.length} shown) + + )} +
+
+
+ setRelevanceThreshold(Number(e.target.value))} + className="flex-1 h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer slider" + style={{ + ['--progress' as any]: `${relevanceThreshold}%`, + background: `linear-gradient(to right, #3B82F6 0%, #3B82F6 ${relevanceThreshold}%, #E5E7EB ${relevanceThreshold}%, #E5E7EB 100%)` + }} + /> + +
+ {relevanceThreshold > 0 && ( +

+ Showing memories with relevance ≥ {relevanceThreshold}% +

+ )} +
+
+ )} + {/* Status Messages */} - {user && memories.length > 0 && ( + {user && (memories.length > 0 || isSemanticFilterActive) && (

- Showing {showUnfiltered ? 'unfiltered' : 'filtered'} memories ({memories.length} found) + {isSemanticFilterActive ? ( + relevanceThreshold > 0 ? ( + searchQuery ? ( + `Showing ${filteredMemories.length} memories (relevance ≥ ${relevanceThreshold}% and matching "${searchQuery}")` + ) : ( + `Showing ${relevanceFilteredMemories.length} of ${semanticResults.length} semantic matches (relevance ≥ ${relevanceThreshold}%)` + ) + ) : ( + searchQuery ? ( + `Showing ${filteredMemories.length} of ${semanticResults.length} semantic matches filtered by "${searchQuery}"` + ) : ( + `Showing ${semanticResults.length} semantic matches for "${semanticQuery}"` + ) + ) + ) : ( + totalCount !== null ? ( + `Showing ${memories.length} of ${totalCount} ${showUnfiltered ? 'unfiltered' : 'filtered'} memories` + ) : ( + `Showing ${showUnfiltered ? 'unfiltered' : 'filtered'} memories (${memories.length} found)` + ) + )}

@@ -228,8 +384,25 @@ export default function Memories() {

- Found {filteredMemories.length} memories - {searchQuery && ` matching "${searchQuery}"`} + {isSemanticFilterActive ? ( + relevanceThreshold > 0 ? ( + `Filtered to ${filteredMemories.length} memories (threshold: ${relevanceThreshold}%${searchQuery ? `, text: "${searchQuery}"` : ''})` + ) : ( + searchQuery ? ( + `Found ${filteredMemories.length} semantic results matching "${searchQuery}"` + ) : ( + `Found ${filteredMemories.length} semantic matches` + ) + ) + ) : ( + searchQuery ? ( + `Found ${filteredMemories.length} memories matching "${searchQuery}"` + ) : totalCount !== null ? ( + `Showing ${filteredMemories.length} of ${totalCount} memories` + ) : ( + `Found ${filteredMemories.length} memories` + ) + )}

@@ -254,8 +427,14 @@ export default function Memories() {
)} - {memory.score && ( - + {memory.score && isSemanticFilterActive && ( +
+ + Relevance: {(memory.score * 100).toFixed(1)}% +
+ )} + {memory.score && !isSemanticFilterActive && ( + Score: {memory.score.toFixed(3)} )} @@ -306,11 +485,26 @@ export default function Memories() {

- {searchQuery - ? `No memories found matching "${searchQuery}"` - : `No memories found` - } + {isSemanticFilterActive ? ( + searchQuery ? ( + `No semantic results found matching "${searchQuery}"` + ) : ( + `No semantic matches found for "${semanticQuery}"` + ) + ) : ( + searchQuery + ? `No memories found matching "${searchQuery}"` + : `No memories found` + )}

+ {isSemanticFilterActive && ( + + )}
)} diff --git a/backends/advanced/webui/src/pages/System.tsx b/backends/advanced/webui/src/pages/System.tsx index 97be52722..14ba2d09b 100644 --- a/backends/advanced/webui/src/pages/System.tsx +++ b/backends/advanced/webui/src/pages/System.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react' -import { Settings, RefreshCw, CheckCircle, XCircle, AlertCircle, Activity, Users, Database, Server, Volume2 } from 'lucide-react' -import { systemApi } from '../services/api' +import { Settings, RefreshCw, CheckCircle, XCircle, AlertCircle, Activity, Users, Database, Server, Volume2, Mic } from 'lucide-react' +import { systemApi, speakerApi } from '../services/api' import { useAuth } from '../contexts/AuthContext' interface HealthData { @@ -152,6 +152,19 @@ export default function System() { } } + const getServiceDisplayName = (service: string) => { + const displayNames: Record = { + 'mongodb': 'MONGODB', + 'audioai': 'AUDIOAI', + 'mem0': 'MEM0', + 'memory_service': 'MEMORY SERVICE', + 'speech_to_text': 'SPEECH TO TEXT', + 'speaker_recognition': 'SPEAKER RECOGNITION', + 'openmemory_mcp': 'OPENMEMORY MCP' + } + return displayNames[service] || service.replace('_', ' ').toUpperCase() + } + const formatDate = (dateString: string) => { return new Date(dateString).toLocaleString() } @@ -240,14 +253,26 @@ export default function System() {
{getStatusIcon(status.healthy)} - {service.replace('_', ' ').toUpperCase()} + {getServiceDisplayName(service)}
- {status.message && ( - - {status.message} - - )} +
+ {status.message && ( + + {status.message} + + )} + {(status as any).status && ( + + {(status as any).status} + + )} + {(status as any).provider && ( + + ({(status as any).provider}) + + )} +
))} @@ -437,6 +462,9 @@ export default function System() { + {/* Speaker Configuration */} + + {/* Active Clients */}

@@ -516,4 +544,219 @@ export default function System() { )}

) +} + +// Speaker Configuration Component +function SpeakerConfiguration() { + const [speakerServiceStatus, setSpeakerServiceStatus] = useState(null) + const [enrolledSpeakers, setEnrolledSpeakers] = useState([]) + const [primarySpeakers, setPrimarySpeakers] = useState([]) + const [loading, setLoading] = useState(false) + const [saving, setSaving] = useState(false) + const [message, setMessage] = useState('') + const { user } = useAuth() + + useEffect(() => { + loadSpeakerData() + }, []) + + const loadSpeakerData = async () => { + setLoading(true) + try { + // Load current configuration and enrolled speakers in parallel + const [configResponse, speakersResponse, statusResponse] = await Promise.allSettled([ + speakerApi.getSpeakerConfiguration(), + speakerApi.getEnrolledSpeakers(), + user?.is_superuser ? speakerApi.getSpeakerServiceStatus() : Promise.resolve({ data: null }) + ]) + + if (configResponse.status === 'fulfilled') { + setPrimarySpeakers(configResponse.value.data.primary_speakers || []) + } + + if (speakersResponse.status === 'fulfilled') { + setEnrolledSpeakers(speakersResponse.value.data.speakers || []) + } + + if (statusResponse.status === 'fulfilled' && statusResponse.value.data) { + setSpeakerServiceStatus(statusResponse.value.data) + } + + } catch (error) { + console.error('Error loading speaker data:', error) + setMessage('Failed to load speaker configuration') + } finally { + setLoading(false) + } + } + + const togglePrimarySpeaker = (speaker: any) => { + const isSelected = primarySpeakers.some(ps => ps.speaker_id === speaker.id) + + if (isSelected) { + setPrimarySpeakers(prev => prev.filter(ps => ps.speaker_id !== speaker.id)) + } else { + setPrimarySpeakers(prev => [...prev, { + speaker_id: speaker.id, + name: speaker.name, + user_id: speaker.user_id + }]) + } + } + + const saveSpeakerConfiguration = async () => { + setSaving(true) + setMessage('') + + try { + const response = await speakerApi.updateSpeakerConfiguration(primarySpeakers) + setMessage(`✅ Saved! ${primarySpeakers.length} primary speakers configured.`) + + // Auto-hide success message after 3 seconds + setTimeout(() => setMessage(''), 3000) + } catch (error: any) { + console.error('Error saving speaker configuration:', error) + setMessage(`❌ Failed to save: ${error.response?.data?.error || error.message}`) + } finally { + setSaving(false) + } + } + + const resetConfiguration = () => { + setPrimarySpeakers([]) + setMessage('Configuration reset. Click Save to apply changes.') + } + + // Don't show the section if speaker service is explicitly disabled or unavailable + const shouldShowSection = speakerServiceStatus !== null || enrolledSpeakers.length > 0 || loading + + if (!shouldShowSection) { + return null + } + + return ( +
+

+ + Speaker Processing Filter + {speakerServiceStatus && ( + + {speakerServiceStatus.healthy ? 'Service Available' : 'Service Unavailable'} + + )} +

+ +

+ Select primary speakers for memory processing. Only conversations where these speakers are detected will have memories extracted. + Leave empty to process all conversations. +

+ + {/* Service Status Info */} + {speakerServiceStatus && !speakerServiceStatus.healthy && ( +
+
+ +
+

Speaker Service Unavailable

+

+ {speakerServiceStatus.message}. Speaker filtering will be disabled until service is available. +

+
+
+
+ )} + + {/* Loading State */} + {loading && ( +
+ + Loading speaker data... +
+ )} + + {/* No Speakers Available */} + {!loading && enrolledSpeakers.length === 0 && ( +
+ +

+ No enrolled speakers found. Enroll speakers in the speaker recognition service to configure primary users. +

+
+ )} + + {/* Speaker Selection */} + {!loading && enrolledSpeakers.length > 0 && ( +
+ {/* Current Configuration */} +
+ + Primary speakers selected: {primarySpeakers.length} + + +
+ + {/* Speaker List */} +
+ {enrolledSpeakers.map((speaker) => { + const isSelected = primarySpeakers.some(ps => ps.speaker_id === speaker.id) + return ( +
togglePrimarySpeaker(speaker)} + > +
+
+ {isSelected && } +
+
+
{speaker.name}
+
+ {speaker.audio_sample_count || 0} samples +
+
+
+
+ ) + })} +
+ + {/* Save Button */} +
+
+ {message && ( +

+ {message} +

+ )} +
+ +
+
+ )} +
+ ) } \ No newline at end of file diff --git a/backends/advanced/webui/src/services/api.ts b/backends/advanced/webui/src/services/api.ts index 42e7d420d..56b487785 100644 --- a/backends/advanced/webui/src/services/api.ts +++ b/backends/advanced/webui/src/services/api.ts @@ -79,6 +79,10 @@ export const conversationsApi = { export const memoriesApi = { getAll: (userId?: string) => api.get('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/api/memories', { params: userId ? { user_id: userId } : {} }), getUnfiltered: (userId?: string) => api.get('/api/memories/unfiltered', { params: userId ? { user_id: userId } : {} }), + search: (query: string, userId?: string, limit: number = 20) => + api.get('/api/memories/search', { + params: { query, ...(userId && { user_id: userId }), limit } + }), delete: (id: string) => api.delete(`/api/memories/${id}`), } @@ -150,4 +154,19 @@ export const chatApi = { body: JSON.stringify(requestBody) }) } +} + +export const speakerApi = { + // Get current user's speaker configuration + getSpeakerConfiguration: () => api.get('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/api/speaker-configuration'), + + // Update current user's speaker configuration + updateSpeakerConfiguration: (primarySpeakers: Array<{speaker_id: string, name: string, user_id: number}>) => + api.post('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/api/speaker-configuration', primarySpeakers), + + // Get enrolled speakers from speaker recognition service + getEnrolledSpeakers: () => api.get('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/api/enrolled-speakers'), + + // Check speaker service status (admin only) + getSpeakerServiceStatus: () => api.get('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/api/speaker-service-status'), } \ No newline at end of file diff --git a/backends/advanced/webui/src/styles/slider.css b/backends/advanced/webui/src/styles/slider.css new file mode 100644 index 000000000..0fca88d99 --- /dev/null +++ b/backends/advanced/webui/src/styles/slider.css @@ -0,0 +1,78 @@ +/* Custom slider styles for relevance threshold */ +input[type="range"].slider { + -webkit-appearance: none; + appearance: none; + width: 100%; + height: 8px; + border-radius: 5px; + outline: none; + opacity: 0.9; + transition: opacity 0.2s; +} + +input[type="range"].slider:hover { + opacity: 1; +} + +/* Webkit browsers (Chrome, Safari, etc.) */ +input[type="range"].slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 20px; + height: 20px; + border-radius: 50%; + background: #3B82F6; + cursor: pointer; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + transition: transform 0.2s, box-shadow 0.2s; +} + +input[type="range"].slider::-webkit-slider-thumb:hover { + transform: scale(1.1); + box-shadow: 0 2px 8px rgba(59, 130, 246, 0.5); +} + +/* Firefox */ +input[type="range"].slider::-moz-range-thumb { + width: 20px; + height: 20px; + border: none; + border-radius: 50%; + background: #3B82F6; + cursor: pointer; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); + transition: transform 0.2s, box-shadow 0.2s; +} + +input[type="range"].slider::-moz-range-thumb:hover { + transform: scale(1.1); + box-shadow: 0 2px 8px rgba(59, 130, 246, 0.5); +} + +/* Track styling for gradient effect */ +input[type="range"].slider::-webkit-slider-runnable-track { + width: 100%; + height: 8px; + cursor: pointer; + border-radius: 5px; +} + +input[type="range"].slider::-moz-range-track { + width: 100%; + height: 8px; + cursor: pointer; + border-radius: 5px; +} + +/* Dark mode adjustments */ +.dark input[type="range"].slider { + background: linear-gradient(to right, #3B82F6 var(--progress), #374151 var(--progress)); +} + +.dark input[type="range"].slider::-webkit-slider-thumb { + background: #60A5FA; +} + +.dark input[type="range"].slider::-moz-range-thumb { + background: #60A5FA; +} \ No newline at end of file diff --git a/extras/openmemory-mcp/docker-compose.yml b/extras/openmemory-mcp/docker-compose.yml index 70ec30766..4107cf4a1 100644 --- a/extras/openmemory-mcp/docker-compose.yml +++ b/extras/openmemory-mcp/docker-compose.yml @@ -5,7 +5,7 @@ services: ports: - "6335:6333" # Different port to avoid conflict with main Qdrant volumes: - - mem0_storage:/qdrant/storage + - ./data/mem0_storage:/qdrant/storage restart: unless-stopped # OpenMemory MCP Server (built from local cache) @@ -41,7 +41,4 @@ services: depends_on: - openmemory-mcp profiles: - - ui # Only starts when --profile ui is used - -volumes: - mem0_storage: \ No newline at end of file + - ui # Only starts when --profile ui is used \ No newline at end of file diff --git a/extras/speaker-recognition/webui/src/pages/Enrollment.tsx b/extras/speaker-recognition/webui/src/pages/Enrollment.tsx index b0d951272..df732c559 100644 --- a/extras/speaker-recognition/webui/src/pages/Enrollment.tsx +++ b/extras/speaker-recognition/webui/src/pages/Enrollment.tsx @@ -90,12 +90,13 @@ export default function Enrollment() { const calculateSessionQuality = useCallback((audioFiles: EnrollmentAudio[]): 'excellent' | 'good' | 'fair' | 'poor' => { if (audioFiles.length === 0) return 'poor' - const totalDuration = audioFiles.reduce((sum, audio) => sum + audio.duration, 0) + const totalDurationMs = audioFiles.reduce((sum, audio) => sum + audio.duration, 0) + const totalDurationSeconds = totalDurationMs / 1000 // Convert to seconds for quality thresholds const avgSNR = audioFiles.reduce((sum, audio) => sum + audio.snr, 0) / audioFiles.length - if (totalDuration >= 60 && avgSNR >= 30 && audioFiles.length >= 5) return 'excellent' - if (totalDuration >= 30 && avgSNR >= 20 && audioFiles.length >= 3) return 'good' - if (totalDuration >= 15 && avgSNR >= 15 && audioFiles.length >= 2) return 'fair' + if (totalDurationSeconds >= 60 && avgSNR >= 30 && audioFiles.length >= 5) return 'excellent' + if (totalDurationSeconds >= 30 && avgSNR >= 20 && audioFiles.length >= 3) return 'good' + if (totalDurationSeconds >= 15 && avgSNR >= 15 && audioFiles.length >= 2) return 'fair' return 'poor' }, []) @@ -113,7 +114,15 @@ export default function Enrollment() { })) if (currentSession?.id === sessionId) { - setCurrentSession(prev => prev ? { ...prev, ...updates } : null) + setCurrentSession(prev => { + if (!prev) return null + const updatedSession = { ...prev, ...updates } + if (updates.audioFiles) { + updatedSession.totalDuration = updates.audioFiles.reduce((sum, audio) => sum + audio.duration, 0) + updatedSession.quality = calculateSessionQuality(updates.audioFiles) + } + return updatedSession + }) } }, [currentSession, calculateSessionQuality]) @@ -175,9 +184,9 @@ export default function Enrollment() { setIsRecording(true) setRecordingTime(0) - // Start timer immediately + // Start timer immediately (track milliseconds for formatDuration compatibility) recordingIntervalRef.current = setInterval(() => { - setRecordingTime(prev => prev + 1) + setRecordingTime(prev => prev + 1000) }, 1000) mediaRecorder.start(250) // Increased interval for better stability @@ -274,7 +283,7 @@ export default function Enrollment() { id: Math.random().toString(36), name: `Recording ${new Date().toLocaleTimeString()}`, blob: processedBlob, // Use the converted blob for backend submission - duration: audioBuffer.duration, + duration: audioBuffer.duration * 1000, // Convert seconds to milliseconds snr, quality, source: 'recording' @@ -325,7 +334,7 @@ export default function Enrollment() { id: Math.random().toString(36), name: file.name, blob: file, - duration: audioBuffer.duration, + duration: audioBuffer.duration * 1000, // Convert seconds to milliseconds snr, quality, source: 'upload' From 88dd8250b8027986a05506435fa1e1ec5995fabe Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Sat, 30 Aug 2025 20:52:32 +0000 Subject: [PATCH 02/35] whoops --- backends/advanced/setup-https.sh | 272 +++++++++++++++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100755 backends/advanced/setup-https.sh diff --git a/backends/advanced/setup-https.sh b/backends/advanced/setup-https.sh new file mode 100755 index 000000000..943469b4f --- /dev/null +++ b/backends/advanced/setup-https.sh @@ -0,0 +1,272 @@ +#!/bin/bash +set -e + +# Friend-Lite Advanced Backend Initialization Script +# Comprehensive setup for all configuration files and optional services + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +print_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +print_header() { + echo "" + echo -e "${CYAN}===============================================${NC}" + echo -e "${CYAN}$1${NC}" + echo -e "${CYAN}===============================================${NC}" + echo "" +} + +# Check if we're in the right directory +if [ ! -f "pyproject.toml" ] || [ ! -d "src" ]; then + print_error "Please run this script from the backends/advanced directory" + exit 1 +fi + +print_header "Friend-Lite Advanced Backend Initialization" +echo "This script will help you set up the Friend-Lite backend with all necessary configurations." +echo "" + +# Function to prompt yes/no +prompt_yes_no() { + local prompt="$1" + local default="$2" + local response + + if [ "$default" = "y" ]; then + prompt="$prompt [Y/n]: " + else + prompt="$prompt [y/N]: " + fi + + read -p "$prompt" response + response=${response:-$default} + + if [[ "$response" =~ ^[Yy]$ ]]; then + return 0 + else + return 1 + fi +} + +# Step 1: Handle .env file +print_header "Step 1: Environment Configuration" +if [ -f ".env" ]; then + print_info ".env file already exists" + if prompt_yes_no "Do you want to update it from template?" "n"; then + cp .env .env.backup + print_info "Backed up existing .env to .env.backup" + cp .env.template .env + print_success ".env created from template" + print_warning "Please edit .env to add your API keys and configuration" + fi +else + if [ -f ".env.template" ]; then + cp .env.template .env + print_success ".env file created from template" + print_warning "IMPORTANT: Edit .env file to add your API keys:" + echo " - DEEPGRAM_API_KEY (for speech-to-text)" + echo " - OPENAI_API_KEY (for memory extraction)" + echo " - ADMIN_EMAIL and ADMIN_PASSWORD" + echo "" + if prompt_yes_no "Would you like to edit .env now?" "y"; then + ${EDITOR:-nano} .env + fi + else + print_error ".env.template not found!" + exit 1 + fi +fi + +# Step 2: Memory configuration +print_header "Step 2: Memory Configuration" +if [ -f "memory_config.yaml" ]; then + print_info "memory_config.yaml already exists" + if prompt_yes_no "Do you want to reset it from template?" "n"; then + cp memory_config.yaml memory_config.yaml.backup + print_info "Backed up existing memory_config.yaml to memory_config.yaml.backup" + cp memory_config.yaml.template memory_config.yaml + print_success "memory_config.yaml reset from template" + fi +else + if [ -f "memory_config.yaml.template" ]; then + cp memory_config.yaml.template memory_config.yaml + print_success "memory_config.yaml created from template" + else + print_error "memory_config.yaml.template not found!" + exit 1 + fi +fi + +# Step 3: Diarization configuration +print_header "Step 3: Diarization Configuration" +if [ -f "diarization_config.json" ]; then + print_info "diarization_config.json already exists" + if prompt_yes_no "Do you want to reset it from template?" "n"; then + cp diarization_config.json diarization_config.json.backup + print_info "Backed up existing diarization_config.json to diarization_config.json.backup" + cp diarization_config.json.template diarization_config.json + print_success "diarization_config.json reset from template" + fi +else + if [ -f "diarization_config.json.template" ]; then + cp diarization_config.json.template diarization_config.json + print_success "diarization_config.json created from template" + else + print_error "diarization_config.json.template not found!" + exit 1 + fi +fi + +# Step 4: HTTPS Setup (optional) +print_header "Step 4: HTTPS Configuration (Optional)" +echo "HTTPS is required for:" +echo " - Microphone access from browsers" +echo " - Remote access via network/Tailscale" +echo " - Secure WebSocket connections" +echo "" + +if prompt_yes_no "Do you want to set up HTTPS?" "n"; then + if [ -f "init-https.sh" ]; then + echo "" + print_info "Please enter your Tailscale IP or network IP" + print_info "Example: 100.83.66.30" + read -p "IP Address: " TAILSCALE_IP + + if [ -n "$TAILSCALE_IP" ]; then + ./init-https.sh "$TAILSCALE_IP" + HTTPS_ENABLED=true + else + print_warning "Skipping HTTPS setup - no IP provided" + HTTPS_ENABLED=false + fi + else + print_warning "init-https.sh not found, skipping HTTPS setup" + HTTPS_ENABLED=false + fi +else + print_info "Skipping HTTPS setup" + HTTPS_ENABLED=false +fi + +# Step 5: Optional Services +print_header "Step 5: Optional Services (extras/)" + +echo "Configure additional services from extras/:" +echo "" + +# OpenMemory MCP (Memory Provider) +if prompt_yes_no "Use OpenMemory MCP for memory management? (cross-client compatible)" "n"; then + sed -i 's/MEMORY_PROVIDER=.*/MEMORY_PROVIDER=openmemory_mcp/' .env + print_success "Configured for OpenMemory MCP" + OPENMEMORY_ENABLED=true +else + OPENMEMORY_ENABLED=false +fi + +# Parakeet ASR (Offline Transcription) +if prompt_yes_no "Use Parakeet for offline transcription? (requires GPU)" "n"; then + if ! grep -q "PARAKEET_ASR_URL" .env; then + echo "PARAKEET_ASR_URL=http://host.docker.internal:8767" >> .env + fi + print_success "Configured for Parakeet ASR" + PARAKEET_ENABLED=true +else + PARAKEET_ENABLED=false +fi + +# Speaker Recognition +if prompt_yes_no "Enable Speaker Recognition service?" "n"; then + if ! grep -q "SPEAKER_SERVICE_URL" .env; then + echo "SPEAKER_SERVICE_URL=http://host.docker.internal:8001" >> .env + fi + print_success "Configured for Speaker Recognition" + SPEAKER_ENABLED=true +else + SPEAKER_ENABLED=false +fi + +# Step 6: Summary and Next Steps +print_header "Setup Complete!" + +echo "Configuration Summary:" +echo "----------------------" +echo "✅ Environment file (.env) configured" +echo "✅ Memory configuration (memory_config.yaml) ready" +echo "✅ Diarization configuration (diarization_config.json) ready" + +if [ "$HTTPS_ENABLED" = true ]; then + echo "✅ HTTPS configured with SSL certificates" +fi + +echo "" +echo "Next Steps:" +echo "-----------" + +if [ "$HTTPS_ENABLED" = true ]; then + echo "1. Start the services with HTTPS:" + echo " ${CYAN}docker compose --profile https up --build -d${NC}" + echo "" + echo "2. Access the dashboard:" + echo " 🌐 https://localhost/" + echo " 🌐 https://$TAILSCALE_IP/" +else + echo "1. Start the services:" + echo " ${CYAN}docker compose up --build -d${NC}" + echo "" + echo "2. Access the dashboard:" + echo " 🌐 http://localhost:5173" +fi + +echo "" +echo "3. Check service health:" +echo " ${CYAN}curl http://localhost:8000/health${NC}" + +echo "" +if [ "$OPENMEMORY_ENABLED" = true ] || [ "$PARAKEET_ENABLED" = true ] || [ "$SPEAKER_ENABLED" = true ]; then + echo "Start Optional Services:" + echo "------------------------" + + if [ "$OPENMEMORY_ENABLED" = true ]; then + echo "OpenMemory MCP:" + echo " ${CYAN}cd ../../extras/openmemory-mcp && docker compose up -d${NC}" + fi + + if [ "$PARAKEET_ENABLED" = true ]; then + echo "Parakeet ASR:" + echo " ${CYAN}cd ../../extras/asr-services && docker compose up parakeet -d${NC}" + fi + + if [ "$SPEAKER_ENABLED" = true ]; then + echo "Speaker Recognition:" + echo " ${CYAN}cd ../../extras/speaker-recognition && docker compose up --build -d${NC}" + fi +fi + +echo "" +echo "For more information, see:" +echo " - Docs/quickstart.md" +echo " - Docs/memory-configuration-guide.md" +echo " - MEMORY_PROVIDERS.md" + +echo "" +print_success "Initialization complete! 🎉" \ No newline at end of file From 5491d66d42e4c0c2cb338b7547b6cea12bc627de Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Sat, 30 Aug 2025 21:35:59 +0000 Subject: [PATCH 03/35] python trial --- backends/advanced/init.py | 423 +++++++++++++++++++++++++++++ backends/advanced/init.sh | 360 ------------------------ backends/advanced/requirements.txt | 3 + 3 files changed, 426 insertions(+), 360 deletions(-) create mode 100644 backends/advanced/init.py delete mode 100755 backends/advanced/init.sh create mode 100644 backends/advanced/requirements.txt diff --git a/backends/advanced/init.py b/backends/advanced/init.py new file mode 100644 index 000000000..63320e455 --- /dev/null +++ b/backends/advanced/init.py @@ -0,0 +1,423 @@ +#!/usr/bin/env python3 +""" +Friend-Lite Advanced Backend Interactive Setup Script +Interactive configuration for all services and API keys +""" + +import os +import sys +import getpass +import secrets +import shutil +from datetime import datetime +from pathlib import Path +from typing import Dict, Optional, Any + +from rich.console import Console +from rich.prompt import Prompt, Confirm +from rich.panel import Panel +from rich.text import Text +from rich import print as rprint + + +class FriendLiteSetup: + def __init__(self): + self.console = Console() + self.config: Dict[str, Any] = {} + + # Check if we're in the right directory + if not Path("pyproject.toml").exists() or not Path("src").exists(): + self.console.print("[red][ERROR][/red] Please run this script from the backends/advanced directory") + sys.exit(1) + + def print_header(self, title: str): + """Print a colorful header""" + self.console.print() + panel = Panel( + Text(title, style="cyan bold"), + style="cyan", + expand=False + ) + self.console.print(panel) + self.console.print() + + def print_section(self, title: str): + """Print a section header""" + self.console.print() + self.console.print(f"[magenta]► {title}[/magenta]") + self.console.print("[magenta]" + "─" * len(f"► {title}") + "[/magenta]") + + def prompt_value(self, prompt: str, default: str = "") -> str: + """Prompt for a value with optional default""" + try: + # Always provide a default to avoid EOF issues + return Prompt.ask(prompt, default=default) + except EOFError: + self.console.print(f"Using default: {default}") + return default + + def prompt_password(self, prompt: str) -> str: + """Prompt for password (hidden input)""" + while True: + try: + password = getpass.getpass(f"{prompt}: ") + if len(password) >= 8: + return password + self.console.print("[yellow][WARNING][/yellow] Password must be at least 8 characters") + except (EOFError, KeyboardInterrupt): + # For non-interactive environments, generate a secure password + self.console.print("[yellow][WARNING][/yellow] Non-interactive environment detected") + password = f"admin-{secrets.token_hex(8)}" + self.console.print(f"Generated secure password: {password}") + return password + + def prompt_choice(self, prompt: str, choices: Dict[str, str], default: str = "1") -> str: + """Prompt for a choice from options""" + self.console.print(prompt) + for key, desc in choices.items(): + self.console.print(f" {key}) {desc}") + self.console.print() + + while True: + try: + choice = Prompt.ask("Enter choice", default=default) + if choice in choices: + return choice + self.console.print(f"[red]Invalid choice. Please select from {list(choices.keys())}[/red]") + except EOFError: + self.console.print(f"Using default choice: {default}") + return default + + def backup_existing_env(self): + """Backup existing .env file""" + env_path = Path(".env") + if env_path.exists(): + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + backup_path = f".env.backup.{timestamp}" + shutil.copy2(env_path, backup_path) + self.console.print(f"[blue][INFO][/blue] Backed up existing .env file to {backup_path}") + + def setup_authentication(self): + """Configure authentication settings""" + self.print_section("Authentication Setup") + self.console.print("Configure admin account for the dashboard") + self.console.print() + + self.config["ADMIN_EMAIL"] = self.prompt_value("Admin email", "admin@example.com") + self.config["ADMIN_PASSWORD"] = self.prompt_password("Admin password (min 8 chars)") + self.config["AUTH_SECRET_KEY"] = secrets.token_hex(32) + + self.console.print("[green][SUCCESS][/green] Admin account configured") + + def setup_transcription(self): + """Configure transcription provider""" + self.print_section("Speech-to-Text Configuration") + + choices = { + "1": "Deepgram (recommended - high quality, requires API key)", + "2": "Mistral (Voxtral models - requires API key)", + "3": "Offline (Parakeet ASR - requires GPU, runs locally)", + "4": "None (skip transcription setup)" + } + + choice = self.prompt_choice("Choose your transcription provider:", choices, "1") + + if choice == "1": + self.config["TRANSCRIPTION_PROVIDER"] = "deepgram" + self.console.print("[blue][INFO][/blue] Deepgram selected") + self.console.print("Get your API key from: https://console.deepgram.com/") + + api_key = self.prompt_value("Deepgram API key (leave empty to skip)", "") + if api_key: + self.config["DEEPGRAM_API_KEY"] = api_key + self.console.print("[green][SUCCESS][/green] Deepgram configured") + else: + self.console.print("[yellow][WARNING][/yellow] No API key provided - transcription will not work") + + elif choice == "2": + self.config["TRANSCRIPTION_PROVIDER"] = "mistral" + self.console.print("[blue][INFO][/blue] Mistral selected") + self.console.print("Get your API key from: https://console.mistral.ai/") + + api_key = self.prompt_value("Mistral API key (leave empty to skip)", "") + model = self.prompt_value("Mistral model", "voxtral-mini-2507") + + if api_key: + self.config["MISTRAL_API_KEY"] = api_key + self.config["MISTRAL_MODEL"] = model + self.console.print("[green][SUCCESS][/green] Mistral configured") + else: + self.console.print("[yellow][WARNING][/yellow] No API key provided - transcription will not work") + + elif choice == "3": + self.config["TRANSCRIPTION_PROVIDER"] = "offline" + self.console.print("[blue][INFO][/blue] Offline Parakeet ASR selected") + parakeet_url = self.prompt_value("Parakeet ASR URL", "http://host.docker.internal:8767") + self.config["PARAKEET_ASR_URL"] = parakeet_url + self.console.print("[yellow][WARNING][/yellow] Remember to start Parakeet service: cd ../../extras/asr-services && docker compose up parakeet") + + elif choice == "4": + self.console.print("[blue][INFO][/blue] Skipping transcription setup") + + def setup_llm(self): + """Configure LLM provider""" + self.print_section("LLM Provider Configuration") + + choices = { + "1": "OpenAI (GPT-4, GPT-3.5 - requires API key)", + "2": "Ollama (local models - requires Ollama server)", + "3": "Skip (no memory extraction)" + } + + choice = self.prompt_choice("Choose your LLM provider for memory extraction:", choices, "1") + + if choice == "1": + self.config["LLM_PROVIDER"] = "openai" + self.console.print("[blue][INFO][/blue] OpenAI selected") + self.console.print("Get your API key from: https://platform.openai.com/api-keys") + + api_key = self.prompt_value("OpenAI API key (leave empty to skip)", "") + model = self.prompt_value("OpenAI model", "gpt-4o-mini") + base_url = self.prompt_value("OpenAI base URL (for proxies/compatible APIs)", "https://api.openai.com/v1") + + if api_key: + self.config["OPENAI_API_KEY"] = api_key + self.config["OPENAI_MODEL"] = model + self.config["OPENAI_BASE_URL"] = base_url + self.console.print("[green][SUCCESS][/green] OpenAI configured") + else: + self.console.print("[yellow][WARNING][/yellow] No API key provided - memory extraction will not work") + + elif choice == "2": + self.config["LLM_PROVIDER"] = "ollama" + self.console.print("[blue][INFO][/blue] Ollama selected") + + base_url = self.prompt_value("Ollama server URL", "http://host.docker.internal:11434") + model = self.prompt_value("Ollama model", "llama3.2") + + self.config["OLLAMA_BASE_URL"] = base_url + self.config["OLLAMA_MODEL"] = model + self.console.print("[green][SUCCESS][/green] Ollama configured") + self.console.print("[yellow][WARNING][/yellow] Make sure Ollama is running and the model is pulled") + + elif choice == "3": + self.console.print("[blue][INFO][/blue] Skipping LLM setup - memory extraction disabled") + + def setup_memory(self): + """Configure memory provider""" + self.print_section("Memory Storage Configuration") + + choices = { + "1": "Friend-Lite Native (Qdrant + custom extraction)", + "2": "OpenMemory MCP (cross-client compatible, external server)" + } + + choice = self.prompt_choice("Choose your memory storage backend:", choices, "1") + + if choice == "1": + self.config["MEMORY_PROVIDER"] = "friend_lite" + self.console.print("[blue][INFO][/blue] Friend-Lite Native memory provider selected") + + qdrant_url = self.prompt_value("Qdrant URL", "qdrant") + self.config["QDRANT_BASE_URL"] = qdrant_url + self.console.print("[green][SUCCESS][/green] Friend-Lite memory provider configured") + + elif choice == "2": + self.config["MEMORY_PROVIDER"] = "openmemory_mcp" + self.console.print("[blue][INFO][/blue] OpenMemory MCP selected") + + mcp_url = self.prompt_value("OpenMemory MCP server URL", "http://host.docker.internal:8765") + client_name = self.prompt_value("OpenMemory client name", "friend_lite") + user_id = self.prompt_value("OpenMemory user ID", "openmemory") + + self.config["OPENMEMORY_MCP_URL"] = mcp_url + self.config["OPENMEMORY_CLIENT_NAME"] = client_name + self.config["OPENMEMORY_USER_ID"] = user_id + self.console.print("[green][SUCCESS][/green] OpenMemory MCP configured") + self.console.print("[yellow][WARNING][/yellow] Remember to start OpenMemory: cd ../../extras/openmemory-mcp && docker compose up -d") + + def setup_optional_services(self): + """Configure optional services""" + self.print_section("Optional Services") + + try: + enable_speaker = Confirm.ask("Enable Speaker Recognition?", default=False) + except EOFError: + self.console.print("Using default: No") + enable_speaker = False + + if enable_speaker: + speaker_url = self.prompt_value("Speaker Recognition service URL", "http://host.docker.internal:8001") + self.config["SPEAKER_SERVICE_URL"] = speaker_url + self.console.print("[green][SUCCESS][/green] Speaker Recognition configured") + self.console.print("[blue][INFO][/blue] Start with: cd ../../extras/speaker-recognition && docker compose up -d") + + def setup_network(self): + """Configure network settings""" + self.print_section("Network Configuration") + + self.config["HOST_IP"] = self.prompt_value("Host IP for services", "localhost") + self.config["BACKEND_PUBLIC_PORT"] = self.prompt_value("Backend port", "8000") + self.config["WEBUI_PORT"] = self.prompt_value("Web UI port", "5173") + + def generate_env_file(self): + """Generate the .env file from configuration""" + env_content = f"""# ============================================================================= +# Friend-Lite Advanced Backend Configuration +# Generated on {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} +# ============================================================================= + +# Authentication +AUTH_SECRET_KEY={self.config.get('AUTH_SECRET_KEY', '')} +ADMIN_EMAIL={self.config.get('ADMIN_EMAIL', '')} +ADMIN_PASSWORD={self.config.get('ADMIN_PASSWORD', '')} + +# Transcription Provider +TRANSCRIPTION_PROVIDER={self.config.get('TRANSCRIPTION_PROVIDER', '')} +DEEPGRAM_API_KEY={self.config.get('DEEPGRAM_API_KEY', '')} +MISTRAL_API_KEY={self.config.get('MISTRAL_API_KEY', '')} +MISTRAL_MODEL={self.config.get('MISTRAL_MODEL', '')} + +# LLM Provider +LLM_PROVIDER={self.config.get('LLM_PROVIDER', '')} +OPENAI_API_KEY={self.config.get('OPENAI_API_KEY', '')} +OPENAI_MODEL={self.config.get('OPENAI_MODEL', 'gpt-4o-mini')} +OPENAI_BASE_URL={self.config.get('OPENAI_BASE_URL', 'https://api.openai.com/v1')} +OLLAMA_BASE_URL={self.config.get('OLLAMA_BASE_URL', '')} +OLLAMA_MODEL={self.config.get('OLLAMA_MODEL', '')} + +# Memory Provider +MEMORY_PROVIDER={self.config.get('MEMORY_PROVIDER', 'friend_lite')} +QDRANT_BASE_URL={self.config.get('QDRANT_BASE_URL', 'qdrant')} +OPENMEMORY_MCP_URL={self.config.get('OPENMEMORY_MCP_URL', '')} +OPENMEMORY_CLIENT_NAME={self.config.get('OPENMEMORY_CLIENT_NAME', '')} +OPENMEMORY_USER_ID={self.config.get('OPENMEMORY_USER_ID', '')} + +# Optional Services +SPEAKER_SERVICE_URL={self.config.get('SPEAKER_SERVICE_URL', '')} +PARAKEET_ASR_URL={self.config.get('PARAKEET_ASR_URL', '')} + +# Network Configuration +HOST_IP={self.config.get('HOST_IP', 'localhost')} +BACKEND_PUBLIC_PORT={self.config.get('BACKEND_PUBLIC_PORT', '8000')} +WEBUI_PORT={self.config.get('WEBUI_PORT', '5173')} + +# Database +MONGODB_URI=mongodb://mongo:27017 +DATABASE_NAME=friend_db + +# CORS (supports Tailscale IPs automatically) +CORS_ORIGINS=http://localhost:3000,http://localhost:5173 + +# Logging +LOG_LEVEL=INFO +""" + + with open(".env", "w") as f: + f.write(env_content) + + self.console.print("[green][SUCCESS][/green] .env file created successfully") + + def copy_config_templates(self): + """Copy other configuration files""" + if not Path("memory_config.yaml").exists() and Path("memory_config.yaml.template").exists(): + shutil.copy2("memory_config.yaml.template", "memory_config.yaml") + self.console.print("[green][SUCCESS][/green] memory_config.yaml created") + + if not Path("diarization_config.json").exists() and Path("diarization_config.json.template").exists(): + shutil.copy2("diarization_config.json.template", "diarization_config.json") + self.console.print("[green][SUCCESS][/green] diarization_config.json created") + + def show_summary(self): + """Show configuration summary""" + self.print_section("Configuration Summary") + self.console.print() + + self.console.print(f"✅ Admin Account: {self.config.get('ADMIN_EMAIL', 'Not configured')}") + self.console.print(f"✅ Transcription: {self.config.get('TRANSCRIPTION_PROVIDER', 'Not configured')}") + self.console.print(f"✅ LLM Provider: {self.config.get('LLM_PROVIDER', 'Not configured')}") + self.console.print(f"✅ Memory Provider: {self.config.get('MEMORY_PROVIDER', 'friend_lite')}") + self.console.print(f"✅ Backend URL: http://{self.config.get('HOST_IP', 'localhost')}:{self.config.get('BACKEND_PUBLIC_PORT', '8000')}") + self.console.print(f"✅ Dashboard URL: http://{self.config.get('HOST_IP', 'localhost')}:{self.config.get('WEBUI_PORT', '5173')}") + + def show_next_steps(self): + """Show next steps""" + self.print_section("Next Steps") + self.console.print() + + host_ip = self.config.get('HOST_IP', 'localhost') + webui_port = self.config.get('WEBUI_PORT', '5173') + backend_port = self.config.get('BACKEND_PUBLIC_PORT', '8000') + + self.console.print("1. Start the main services:") + self.console.print(" [cyan]docker compose up --build -d[/cyan]") + self.console.print() + self.console.print("2. Access the dashboard:") + self.console.print(f" [cyan]http://{host_ip}:{webui_port}[/cyan]") + self.console.print() + self.console.print("3. Check service health:") + self.console.print(f" [cyan]curl http://{host_ip}:{backend_port}/health[/cyan]") + + if self.config.get("MEMORY_PROVIDER") == "openmemory_mcp": + self.console.print() + self.console.print("4. Start OpenMemory MCP:") + self.console.print(" [cyan]cd ../../extras/openmemory-mcp && docker compose up -d[/cyan]") + + if self.config.get("TRANSCRIPTION_PROVIDER") == "offline": + self.console.print() + self.console.print("5. Start Parakeet ASR:") + self.console.print(" [cyan]cd ../../extras/asr-services && docker compose up parakeet -d[/cyan]") + + def run(self): + """Run the complete setup process""" + self.print_header("🚀 Friend-Lite Interactive Setup") + self.console.print("This wizard will help you configure Friend-Lite with all necessary services.") + self.console.print("We'll ask for your API keys and preferences step by step.") + self.console.print() + + try: + # Backup existing config + self.backup_existing_env() + + # Run setup steps + self.setup_authentication() + self.setup_transcription() + self.setup_llm() + self.setup_memory() + self.setup_optional_services() + self.setup_network() + + # Generate files + self.print_header("Configuration Complete!") + self.generate_env_file() + self.copy_config_templates() + + # Show results + self.show_summary() + self.show_next_steps() + + self.console.print() + self.console.print("[green][SUCCESS][/green] Setup complete! 🎉") + self.console.print() + self.console.print("For detailed documentation, see:") + self.console.print(" • Docs/quickstart.md") + self.console.print(" • MEMORY_PROVIDERS.md") + self.console.print(" • CLAUDE.md") + + except KeyboardInterrupt: + self.console.print() + self.console.print("[yellow]Setup cancelled by user[/yellow]") + sys.exit(0) + except Exception as e: + self.console.print(f"[red][ERROR][/red] Setup failed: {e}") + sys.exit(1) + + +def main(): + """Main entry point""" + setup = FriendLiteSetup() + setup.run() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/backends/advanced/init.sh b/backends/advanced/init.sh deleted file mode 100755 index ef51a21ea..000000000 --- a/backends/advanced/init.sh +++ /dev/null @@ -1,360 +0,0 @@ -#!/bin/bash -set -e - -# Friend-Lite Advanced Backend Interactive Setup Script -# Interactive configuration for all services and API keys - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -MAGENTA='\033[0;35m' -NC='\033[0m' # No Color - -print_info() { - echo -e "${BLUE}[INFO]${NC} $1" -} - -print_success() { - echo -e "${GREEN}[SUCCESS]${NC} $1" -} - -print_warning() { - echo -e "${YELLOW}[WARNING]${NC} $1" -} - -print_error() { - echo -e "${RED}[ERROR]${NC} $1" -} - -print_header() { - echo "" - echo -e "${CYAN}===============================================${NC}" - echo -e "${CYAN}$1${NC}" - echo -e "${CYAN}===============================================${NC}" - echo "" -} - -print_section() { - echo "" - echo -e "${MAGENTA}► $1${NC}" - echo -e "${MAGENTA}$(echo "$1" | sed 's/./-/g')${NC}" -} - -# Check if we're in the right directory -if [ ! -f "pyproject.toml" ] || [ ! -d "src" ]; then - print_error "Please run this script from the backends/advanced directory" - exit 1 -fi - -print_header "🚀 Friend-Lite Interactive Setup" -echo "This wizard will help you configure Friend-Lite with all necessary services." -echo "We'll ask for your API keys and preferences step by step." -echo "" - -# Function to prompt yes/no -prompt_yes_no() { - local prompt="$1" - local default="$2" - local response - - if [ "$default" = "y" ]; then - prompt="$prompt [Y/n]: " - else - prompt="$prompt [y/N]: " - fi - - read -p "$prompt" response - response=${response:-$default} - - if [[ "$response" =~ ^[Yy]$ ]]; then - return 0 - else - return 1 - fi -} - -# Function to prompt for value with default -prompt_value() { - local prompt="$1" - local default="$2" - local response - - if [ -n "$default" ]; then - read -p "$prompt [$default]: " response - response=${response:-$default} - else - read -p "$prompt: " response - fi - - echo "$response" -} - -# Function to prompt for password (hidden input) -prompt_password() { - local prompt="$1" - local password - read -s -p "$prompt: " password - echo "" - echo "$password" -} - -# Backup existing .env if it exists -if [ -f ".env" ]; then - cp .env .env.backup.$(date +%Y%m%d_%H%M%S) - print_info "Backed up existing .env file" -fi - -# Start with template -cp .env.template .env.new -print_success "Starting from .env.template" - -# ============================================================================= -# AUTHENTICATION SETUP -# ============================================================================= -print_section "Authentication Setup" -echo "Configure admin account for the dashboard" -echo "" - -ADMIN_EMAIL=$(prompt_value "Admin email" "admin@example.com") -ADMIN_PASSWORD=$(prompt_password "Admin password (min 8 chars)") -AUTH_SECRET=$(openssl rand -hex 32 2>/dev/null || cat /dev/urandom | head -c 32 | base64) - -sed -i "s|ADMIN_EMAIL=.*|ADMIN_EMAIL=$ADMIN_EMAIL|" .env.new -sed -i "s|ADMIN_PASSWORD=.*|ADMIN_PASSWORD=$ADMIN_PASSWORD|" .env.new -sed -i "s|AUTH_SECRET_KEY=.*|AUTH_SECRET_KEY=$AUTH_SECRET|" .env.new - -print_success "Admin account configured" - -# ============================================================================= -# TRANSCRIPTION PROVIDER -# ============================================================================= -print_section "Speech-to-Text Configuration" -echo "Choose your transcription provider:" -echo " 1) Deepgram (recommended - high quality, requires API key)" -echo " 2) Mistral (Voxtral models - requires API key)" -echo " 3) Offline (Parakeet ASR - requires GPU, runs locally)" -echo " 4) None (skip transcription setup)" -echo "" - -TRANSCRIPTION_CHOICE=$(prompt_value "Enter choice (1-4)" "1") - -case $TRANSCRIPTION_CHOICE in - 1) - TRANSCRIPTION_PROVIDER="deepgram" - print_info "Deepgram selected" - echo "Get your API key from: https://console.deepgram.com/" - DEEPGRAM_API_KEY=$(prompt_value "Deepgram API key" "") - if [ -n "$DEEPGRAM_API_KEY" ]; then - sed -i "s|DEEPGRAM_API_KEY=.*|DEEPGRAM_API_KEY=$DEEPGRAM_API_KEY|" .env.new - sed -i "s|TRANSCRIPTION_PROVIDER=.*|TRANSCRIPTION_PROVIDER=deepgram|" .env.new - print_success "Deepgram configured" - else - print_warning "No API key provided - transcription will not work" - fi - ;; - 2) - TRANSCRIPTION_PROVIDER="mistral" - print_info "Mistral selected" - echo "Get your API key from: https://console.mistral.ai/" - MISTRAL_API_KEY=$(prompt_value "Mistral API key" "") - MISTRAL_MODEL=$(prompt_value "Mistral model" "voxtral-mini-2507") - if [ -n "$MISTRAL_API_KEY" ]; then - sed -i "s|# MISTRAL_API_KEY=.*|MISTRAL_API_KEY=$MISTRAL_API_KEY|" .env.new - sed -i "s|# MISTRAL_MODEL=.*|MISTRAL_MODEL=$MISTRAL_MODEL|" .env.new - sed -i "s|TRANSCRIPTION_PROVIDER=.*|TRANSCRIPTION_PROVIDER=mistral|" .env.new - print_success "Mistral configured" - else - print_warning "No API key provided - transcription will not work" - fi - ;; - 3) - TRANSCRIPTION_PROVIDER="offline" - print_info "Offline Parakeet ASR selected" - PARAKEET_URL=$(prompt_value "Parakeet ASR URL" "http://host.docker.internal:8767") - echo "PARAKEET_ASR_URL=$PARAKEET_URL" >> .env.new - print_warning "Remember to start Parakeet service: cd ../../extras/asr-services && docker compose up parakeet" - ;; - 4) - print_info "Skipping transcription setup" - ;; -esac - -# ============================================================================= -# LLM PROVIDER -# ============================================================================= -print_section "LLM Provider Configuration" -echo "Choose your LLM provider for memory extraction:" -echo " 1) OpenAI (GPT-4, GPT-3.5 - requires API key)" -echo " 2) Ollama (local models - requires Ollama server)" -echo " 3) Skip (no memory extraction)" -echo "" - -LLM_CHOICE=$(prompt_value "Enter choice (1-3)" "1") - -case $LLM_CHOICE in - 1) - LLM_PROVIDER="openai" - print_info "OpenAI selected" - echo "Get your API key from: https://platform.openai.com/api-keys" - OPENAI_API_KEY=$(prompt_value "OpenAI API key" "") - OPENAI_MODEL=$(prompt_value "OpenAI model" "gpt-4o-mini") - OPENAI_BASE_URL=$(prompt_value "OpenAI base URL (for proxies/compatible APIs)" "https://api.openai.com/v1") - - if [ -n "$OPENAI_API_KEY" ]; then - sed -i "s|OPENAI_API_KEY=.*|OPENAI_API_KEY=$OPENAI_API_KEY|" .env.new - sed -i "s|OPENAI_MODEL=.*|OPENAI_MODEL=$OPENAI_MODEL|" .env.new - sed -i "s|OPENAI_BASE_URL=.*|OPENAI_BASE_URL=$OPENAI_BASE_URL|" .env.new - sed -i "s|LLM_PROVIDER=.*|LLM_PROVIDER=openai|" .env.new - print_success "OpenAI configured" - else - print_warning "No API key provided - memory extraction will not work" - fi - ;; - 2) - LLM_PROVIDER="ollama" - print_info "Ollama selected" - OLLAMA_BASE_URL=$(prompt_value "Ollama server URL" "http://host.docker.internal:11434") - OLLAMA_MODEL=$(prompt_value "Ollama model" "llama3.2") - - sed -i "s|# OLLAMA_BASE_URL=.*|OLLAMA_BASE_URL=$OLLAMA_BASE_URL|" .env.new - sed -i "s|# OLLAMA_MODEL=.*|OLLAMA_MODEL=$OLLAMA_MODEL|" .env.new - sed -i "s|LLM_PROVIDER=.*|LLM_PROVIDER=ollama|" .env.new - print_success "Ollama configured" - print_warning "Make sure Ollama is running and the model is pulled" - ;; - 3) - print_info "Skipping LLM setup - memory extraction disabled" - ;; -esac - -# ============================================================================= -# MEMORY PROVIDER -# ============================================================================= -print_section "Memory Storage Configuration" -echo "Choose your memory storage backend:" -echo " 1) Friend-Lite Native (Qdrant + custom extraction)" -echo " 2) OpenMemory MCP (cross-client compatible, external server)" -echo "" - -MEMORY_CHOICE=$(prompt_value "Enter choice (1-2)" "1") - -case $MEMORY_CHOICE in - 1) - MEMORY_PROVIDER="friend_lite" - print_info "Friend-Lite Native memory provider selected" - sed -i "s|MEMORY_PROVIDER=.*|MEMORY_PROVIDER=friend_lite|" .env.new - - # Qdrant configuration - QDRANT_URL=$(prompt_value "Qdrant URL" "qdrant") - sed -i "s|QDRANT_BASE_URL=.*|QDRANT_BASE_URL=$QDRANT_URL|" .env.new - print_success "Friend-Lite memory provider configured" - ;; - 2) - MEMORY_PROVIDER="openmemory_mcp" - print_info "OpenMemory MCP selected" - OPENMEMORY_URL=$(prompt_value "OpenMemory MCP server URL" "http://host.docker.internal:8765") - OPENMEMORY_CLIENT=$(prompt_value "OpenMemory client name" "friend_lite") - OPENMEMORY_USER=$(prompt_value "OpenMemory user ID" "openmemory") - - sed -i "s|MEMORY_PROVIDER=.*|MEMORY_PROVIDER=openmemory_mcp|" .env.new - sed -i "s|# OPENMEMORY_MCP_URL=.*|OPENMEMORY_MCP_URL=$OPENMEMORY_URL|" .env.new - sed -i "s|# OPENMEMORY_CLIENT_NAME=.*|OPENMEMORY_CLIENT_NAME=$OPENMEMORY_CLIENT|" .env.new - sed -i "s|# OPENMEMORY_USER_ID=.*|OPENMEMORY_USER_ID=$OPENMEMORY_USER|" .env.new - print_success "OpenMemory MCP configured" - print_warning "Remember to start OpenMemory: cd ../../extras/openmemory-mcp && docker compose up -d" - ;; -esac - -# ============================================================================= -# OPTIONAL SERVICES -# ============================================================================= -print_section "Optional Services" - -if prompt_yes_no "Enable Speaker Recognition?" "n"; then - SPEAKER_URL=$(prompt_value "Speaker Recognition service URL" "http://host.docker.internal:8001") - echo "SPEAKER_SERVICE_URL=$SPEAKER_URL" >> .env.new - print_success "Speaker Recognition configured" - print_info "Start with: cd ../../extras/speaker-recognition && docker compose up -d" -fi - -# ============================================================================= -# NETWORK CONFIGURATION -# ============================================================================= -print_section "Network Configuration" - -HOST_IP=$(prompt_value "Host IP for services" "localhost") -BACKEND_PORT=$(prompt_value "Backend port" "8000") -WEBUI_PORT=$(prompt_value "Web UI port" "5173") - -sed -i "s|HOST_IP=.*|HOST_IP=$HOST_IP|" .env.new -sed -i "s|BACKEND_PUBLIC_PORT=.*|BACKEND_PUBLIC_PORT=$BACKEND_PORT|" .env.new -sed -i "s|WEBUI_PORT=.*|WEBUI_PORT=$WEBUI_PORT|" .env.new - -# ============================================================================= -# FINALIZE -# ============================================================================= -print_header "Configuration Complete!" - -# Move new .env into place -mv .env.new .env -print_success ".env file created successfully" - -# Copy other configuration files -if [ ! -f "memory_config.yaml" ]; then - cp memory_config.yaml.template memory_config.yaml - print_success "memory_config.yaml created" -fi - -if [ ! -f "diarization_config.json" ]; then - cp diarization_config.json.template diarization_config.json - print_success "diarization_config.json created" -fi - -# ============================================================================= -# SUMMARY -# ============================================================================= -print_section "Configuration Summary" -echo "" -echo "✅ Admin Account: $ADMIN_EMAIL" -echo "✅ Transcription: $TRANSCRIPTION_PROVIDER" -echo "✅ LLM Provider: $LLM_PROVIDER" -echo "✅ Memory Provider: $MEMORY_PROVIDER" -echo "✅ Backend URL: http://$HOST_IP:$BACKEND_PORT" -echo "✅ Dashboard URL: http://$HOST_IP:$WEBUI_PORT" - -# ============================================================================= -# NEXT STEPS -# ============================================================================= -print_section "Next Steps" -echo "" -echo "1. Start the main services:" -echo " ${CYAN}docker compose up --build -d${NC}" -echo "" -echo "2. Access the dashboard:" -echo " ${CYAN}http://$HOST_IP:$WEBUI_PORT${NC}" -echo "" -echo "3. Check service health:" -echo " ${CYAN}curl http://$HOST_IP:$BACKEND_PORT/health${NC}" - -if [ "$MEMORY_PROVIDER" = "openmemory_mcp" ]; then - echo "" - echo "4. Start OpenMemory MCP:" - echo " ${CYAN}cd ../../extras/openmemory-mcp && docker compose up -d${NC}" -fi - -if [ "$TRANSCRIPTION_PROVIDER" = "offline" ]; then - echo "" - echo "5. Start Parakeet ASR:" - echo " ${CYAN}cd ../../extras/asr-services && docker compose up parakeet -d${NC}" -fi - -echo "" -print_success "Setup complete! 🎉" -echo "" -echo "For detailed documentation, see:" -echo " • Docs/quickstart.md" -echo " • MEMORY_PROVIDERS.md" -echo " • CLAUDE.md" \ No newline at end of file diff --git a/backends/advanced/requirements.txt b/backends/advanced/requirements.txt new file mode 100644 index 000000000..89d23f8d8 --- /dev/null +++ b/backends/advanced/requirements.txt @@ -0,0 +1,3 @@ +# Dependencies for interactive setup script +rich>=13.0.0 +pyyaml>=6.0.0 \ No newline at end of file From 61fa5efaeaa095479a2b58a470b05eee9ceeebc8 Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Mon, 1 Sep 2025 10:18:06 +0000 Subject: [PATCH 04/35] updated init --- init.sh | 281 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 279 insertions(+), 2 deletions(-) diff --git a/init.sh b/init.sh index 966dbc08a..220c5b4b8 100644 --- a/init.sh +++ b/init.sh @@ -266,6 +266,118 @@ CORS_ORIGINS="http://${HOST_IP}:${WEBUI_PORT},http://localhost:${WEBUI_PORT},htt print_success "Network configured for access at http://${HOST_IP}:${WEBUI_PORT}" echo +# Speaker Recognition Service Configuration +print_header "🗣️ Speaker Recognition Service (Optional)" +echo "Speaker recognition enables speaker identification for conversations." +echo "1. Set up speaker recognition service" +echo "2. Skip - Configure later" +echo + +while true; do + read -p "Configure speaker recognition? [1-2] (default: 2): " speaker_choice + speaker_choice=${speaker_choice:-2} + + case $speaker_choice in + 1) + SETUP_SPEAKER_RECOGNITION="yes" + + print_info "Speaker recognition requires a Hugging Face token for pyannote models" + while true; do + read -p "Enter your Hugging Face token (sets HF_TOKEN): " hf_token + if [[ ${#hf_token} -ge 10 ]]; then + HF_TOKEN="$hf_token" + break + else + print_error "Please enter a valid Hugging Face token" + fi + done + + echo "Choose compute mode for speaker recognition:" + echo "1. CPU (Recommended for development)" + echo "2. GPU (Requires NVIDIA GPU with CUDA)" + echo + + while true; do + read -p "Select compute mode [1-2] (default: 1): " compute_choice + compute_choice=${compute_choice:-1} + + case $compute_choice in + 1) + SPEAKER_COMPUTE_MODE="cpu" + break + ;; + 2) + SPEAKER_COMPUTE_MODE="gpu" + print_warning "GPU mode requires NVIDIA GPU with CUDA drivers" + break + ;; + *) + print_error "Please select 1 or 2" + ;; + esac + done + + read -p "Speaker similarity threshold (0.0-1.0, higher=more strict) [0.15]: " similarity_threshold + SIMILARITY_THRESHOLD=${similarity_threshold:-0.15} + + print_success "Speaker recognition configured with $SPEAKER_COMPUTE_MODE mode" + break + ;; + 2) + print_warning "Speaker recognition setup skipped - can be configured later" + break + ;; + *) + print_error "Please select 1 or 2" + ;; + esac +done + +echo + +# HTTPS Configuration +print_header "🔒 HTTPS Configuration (Optional)" +echo "HTTPS setup provides secure access and works well with Tailscale networks." +echo "1. Set up HTTPS with Tailscale IP" +echo "2. Set up HTTPS with localhost only" +echo "3. Skip - Use HTTP only" +echo + +while true; do + read -p "Configure HTTPS? [1-3] (default: 3): " https_choice + https_choice=${https_choice:-3} + + case $https_choice in + 1) + SETUP_HTTPS="yes" + read -p "Enter your Tailscale IP address: " tailscale_ip + if [[ $tailscale_ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then + TAILSCALE_IP="$tailscale_ip" + print_success "HTTPS will be configured for localhost and $TAILSCALE_IP" + else + print_error "Invalid IP format, using localhost only" + TAILSCALE_IP="127.0.0.1" + fi + break + ;; + 2) + SETUP_HTTPS="yes" + TAILSCALE_IP="127.0.0.1" + print_success "HTTPS will be configured for localhost only" + break + ;; + 3) + print_warning "HTTPS setup skipped - using HTTP only" + break + ;; + *) + print_error "Please select 1, 2, or 3" + ;; + esac +done + +echo + # Configuration Summary print_header "📋 Configuration Summary" echo "Backend: $BACKEND_NAME" @@ -277,8 +389,19 @@ elif [[ -n $PARAKEET_ASR_URL ]]; then else echo "Speech-to-Text: Not configured" fi +if [[ $SETUP_SPEAKER_RECOGNITION == "yes" ]]; then + echo "Speaker Recognition: Enabled ($SPEAKER_COMPUTE_MODE mode)" +else + echo "Speaker Recognition: Not configured" +fi +if [[ $SETUP_HTTPS == "yes" ]]; then + echo "HTTPS: Enabled (localhost + $TAILSCALE_IP)" + echo "Access URL: https://${HOST_IP} (or https://${TAILSCALE_IP})" +else + echo "HTTPS: Not configured" + echo "Access URL: http://${HOST_IP}:${WEBUI_PORT}" +fi echo "Admin Email: $ADMIN_EMAIL" -echo "Access URL: http://${HOST_IP}:${WEBUI_PORT}" echo read -p "Continue with setup? [Y/n]: " confirm @@ -342,6 +465,15 @@ WEBUI_PORT=$WEBUI_PORT CORS_ORIGINS=$CORS_ORIGINS EOF +# Add speaker recognition config if configured +if [[ $SETUP_SPEAKER_RECOGNITION == "yes" ]]; then + cat >> .env << EOF +# Speaker Recognition Configuration (sets HF_TOKEN, SPEAKER_SERVICE_URL) +HF_TOKEN=$HF_TOKEN +SPEAKER_SERVICE_URL=http://host.docker.internal:8085 +EOF +fi + print_success ".env file created" # Copy memory config for advanced backend @@ -378,19 +510,164 @@ else exit 1 fi +# Additional Service Setup +cd .. # Back to project root + +# Set up speaker recognition service if requested +if [[ $SETUP_SPEAKER_RECOGNITION == "yes" ]]; then + print_header "🗣️ Setting up Speaker Recognition Service" + + if [[ -d "extras/speaker-recognition" ]]; then + cd extras/speaker-recognition + + # Create .env file for speaker recognition + print_info "Creating speaker recognition .env file..." + cat > .env << EOF +# Speaker Recognition Service Environment Configuration +# Generated by init.sh on $(date) + +# Required: Hugging Face token for pyannote models +HF_TOKEN=$HF_TOKEN + +# Docker build configuration +COMPUTE_MODE=$SPEAKER_COMPUTE_MODE + +# Speaker recognition similarity threshold +SIMILARITY_THRESHOLD=$SIMILARITY_THRESHOLD + +# Service Configuration +SPEAKER_SERVICE_HOST=speaker-service +SPEAKER_SERVICE_PORT=8085 +SPEAKER_SERVICE_URL=http://speaker-service:8085 + +# React Web UI Configuration +REACT_UI_HOST=0.0.0.0 +REACT_UI_PORT=5173 +REACT_UI_HTTPS=false + +# Optional: External Services +DEEPGRAM_API_KEY=$DEEPGRAM_API_KEY + +# Test Configuration +SPEAKER_SERVICE_TEST_PORT=8086 +EOF + + # Set up HTTPS for speaker recognition if requested + if [[ $SETUP_HTTPS == "yes" ]]; then + print_info "Setting up HTTPS for speaker recognition..." + + # Generate SSL certificates for speaker recognition + if [[ -f "ssl/generate-ssl.sh" ]]; then + chmod +x ssl/generate-ssl.sh + ./ssl/generate-ssl.sh "$TAILSCALE_IP" + print_success "SSL certificates generated for speaker recognition" + else + print_warning "SSL generation script not found for speaker recognition" + fi + + # Configure nginx.conf with Tailscale IP + if [[ -f "nginx.conf.template" ]]; then + sed "s/TAILSCALE_IP/$TAILSCALE_IP/g" nginx.conf.template > nginx.conf + print_success "nginx.conf configured for speaker recognition with Tailscale IP: $TAILSCALE_IP" + else + print_warning "nginx.conf.template not found for speaker recognition" + fi + fi + + print_info "Starting speaker recognition services..." + if docker compose up --build -d; then + print_success "Speaker recognition service started!" + else + print_warning "Failed to start speaker recognition service - check logs with 'docker compose logs'" + fi + + cd ../.. # Back to project root + else + print_error "Speaker recognition directory not found at extras/speaker-recognition" + fi +fi + +# Set up HTTPS if requested +if [[ $SETUP_HTTPS == "yes" ]]; then + print_header "🔒 Setting up HTTPS" + + cd "$BACKEND_DIR" + + # Generate SSL certificates + print_info "Generating SSL certificates..." + if [[ -f "ssl/generate-ssl.sh" ]]; then + chmod +x ssl/generate-ssl.sh + ./ssl/generate-ssl.sh "$TAILSCALE_IP" + print_success "SSL certificates generated" + else + print_error "SSL generation script not found" + fi + + # Configure nginx.conf with Tailscale IP + print_info "Configuring nginx for HTTPS..." + if [[ -f "nginx.conf.template" ]]; then + sed "s/TAILSCALE_IP/$TAILSCALE_IP/g" nginx.conf.template > nginx.conf + print_success "nginx.conf configured with Tailscale IP: $TAILSCALE_IP" + else + print_error "nginx.conf.template not found" + fi + + # Restart services to enable nginx + print_info "Restarting services to enable HTTPS..." + docker compose restart + + # Wait for services to be ready + sleep 5 + + cd .. # Back to project root +fi + # Final Success Message print_header "🎉 Setup Complete!" cat << EOF Friend-Lite is now running! Here's how to access it: +EOF + +if [[ $SETUP_HTTPS == "yes" ]]; then + cat << EOF +🌐 Web Interface: https://${HOST_IP} or https://${TAILSCALE_IP} +🔧 API Endpoint: https://${HOST_IP}/api or https://${TAILSCALE_IP}/api + +EOF +else + cat << EOF 🌐 Web Interface: http://${HOST_IP}:${WEBUI_PORT} 🔧 API Endpoint: http://${HOST_IP}:${BACKEND_PORT} +EOF +fi + +cat << EOF 👤 Admin Login: Email: ${ADMIN_EMAIL} Password: ${ADMIN_PASSWORD} +EOF + +if [[ $SETUP_SPEAKER_RECOGNITION == "yes" ]]; then + if [[ $SETUP_HTTPS == "yes" ]]; then + cat << EOF +🗣️ Speaker Recognition: https://${HOST_IP}:8444 or https://${TAILSCALE_IP}:8444 (Web UI) + Service API: https://${HOST_IP}:8444/api or https://${TAILSCALE_IP}:8444/api + +EOF + else + cat << EOF +🗣️ Speaker Recognition: http://localhost:5173 (Web UI) + Service API: http://localhost:8085 + +EOF + fi +fi + +cat << EOF 📖 Next Steps: 1. Open the web interface and log in 2. Connect your OMI device via the mobile app @@ -404,7 +681,7 @@ Friend-Lite is now running! Here's how to access it: Run './run-test.sh' to test the full pipeline 🔧 Configuration: - - Edit .env to modify settings + - Edit .env files to modify settings - Run 'docker compose restart' after changes - Use 'docker compose logs' to view service logs From e6de82e0455a52db03c646700276db2da9b6ca71 Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Mon, 1 Sep 2025 10:29:47 +0000 Subject: [PATCH 05/35] perms --- init.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 init.sh diff --git a/init.sh b/init.sh old mode 100644 new mode 100755 From 2646c93404588706ea015da03b56d1dd3592f6a6 Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Mon, 1 Sep 2025 11:28:14 +0000 Subject: [PATCH 06/35] better init --- backends/advanced/init.py | 46 +- ...equirements.txt => setup-requirements.txt} | 0 extras/asr-services/setup.sh | 16 + extras/openmemory-mcp/setup.sh | 17 + extras/speaker-recognition/setup.sh | 37 + init.py | 215 ++++++ init.sh | 696 ------------------ setup-requirements.txt | 2 + 8 files changed, 320 insertions(+), 709 deletions(-) rename backends/advanced/{requirements.txt => setup-requirements.txt} (100%) create mode 100755 extras/asr-services/setup.sh create mode 100755 extras/openmemory-mcp/setup.sh create mode 100755 extras/speaker-recognition/setup.sh create mode 100755 init.py delete mode 100755 init.sh create mode 100644 setup-requirements.txt diff --git a/backends/advanced/init.py b/backends/advanced/init.py index 63320e455..a2ccc80de 100644 --- a/backends/advanced/init.py +++ b/backends/advanced/init.py @@ -9,6 +9,7 @@ import getpass import secrets import shutil +import argparse from datetime import datetime from pathlib import Path from typing import Dict, Optional, Any @@ -21,9 +22,10 @@ class FriendLiteSetup: - def __init__(self): + def __init__(self, args=None): self.console = Console() self.config: Dict[str, Any] = {} + self.args = args or argparse.Namespace() # Check if we're in the right directory if not Path("pyproject.toml").exists() or not Path("src").exists(): @@ -240,17 +242,27 @@ def setup_optional_services(self): """Configure optional services""" self.print_section("Optional Services") - try: - enable_speaker = Confirm.ask("Enable Speaker Recognition?", default=False) - except EOFError: - self.console.print("Using default: No") - enable_speaker = False - - if enable_speaker: - speaker_url = self.prompt_value("Speaker Recognition service URL", "http://host.docker.internal:8001") - self.config["SPEAKER_SERVICE_URL"] = speaker_url - self.console.print("[green][SUCCESS][/green] Speaker Recognition configured") - self.console.print("[blue][INFO][/blue] Start with: cd ../../extras/speaker-recognition && docker compose up -d") + # Check if speaker service URL provided via args + if hasattr(self.args, 'speaker_service_url') and self.args.speaker_service_url: + self.config["SPEAKER_SERVICE_URL"] = self.args.speaker_service_url + self.console.print(f"[green][SUCCESS][/green] Speaker Recognition configured via args: {self.args.speaker_service_url}") + else: + try: + enable_speaker = Confirm.ask("Enable Speaker Recognition?", default=False) + except EOFError: + self.console.print("Using default: No") + enable_speaker = False + + if enable_speaker: + speaker_url = self.prompt_value("Speaker Recognition service URL", "http://host.docker.internal:8001") + self.config["SPEAKER_SERVICE_URL"] = speaker_url + self.console.print("[green][SUCCESS][/green] Speaker Recognition configured") + self.console.print("[blue][INFO][/blue] Start with: cd ../../extras/speaker-recognition && docker compose up -d") + + # Check if ASR service URL provided via args + if hasattr(self.args, 'parakeet_asr_url') and self.args.parakeet_asr_url: + self.config["PARAKEET_ASR_URL"] = self.args.parakeet_asr_url + self.console.print(f"[green][SUCCESS][/green] Parakeet ASR configured via args: {self.args.parakeet_asr_url}") def setup_network(self): """Configure network settings""" @@ -415,7 +427,15 @@ def run(self): def main(): """Main entry point""" - setup = FriendLiteSetup() + parser = argparse.ArgumentParser(description="Friend-Lite Advanced Backend Setup") + parser.add_argument("--speaker-service-url", + help="Speaker Recognition service URL (default: prompt user)") + parser.add_argument("--parakeet-asr-url", + help="Parakeet ASR service URL (default: prompt user)") + + args = parser.parse_args() + + setup = FriendLiteSetup(args) setup.run() diff --git a/backends/advanced/requirements.txt b/backends/advanced/setup-requirements.txt similarity index 100% rename from backends/advanced/requirements.txt rename to backends/advanced/setup-requirements.txt diff --git a/extras/asr-services/setup.sh b/extras/asr-services/setup.sh new file mode 100755 index 000000000..12ee400be --- /dev/null +++ b/extras/asr-services/setup.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -e + +echo "🎤 Offline ASR Setup" +echo "===================" + +echo "Starting Parakeet ASR service..." +echo "" + +# Start Parakeet ASR service +docker compose up parakeet -d + +echo "✅ Parakeet ASR running:" +echo " 📝 Service URL: http://host.docker.internal:8767" +echo "" +echo "💡 Configure PARAKEET_ASR_URL in your backend's transcription settings" \ No newline at end of file diff --git a/extras/openmemory-mcp/setup.sh b/extras/openmemory-mcp/setup.sh new file mode 100755 index 000000000..0a720e01d --- /dev/null +++ b/extras/openmemory-mcp/setup.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -e + +echo "🧠 OpenMemory MCP Setup" +echo "======================" + +echo "Starting OpenMemory MCP server..." +echo "" + +# Start external server +docker compose up -d + +echo "✅ OpenMemory MCP running!" +echo " 🌐 Server: http://host.docker.internal:8765" +echo " 📱 Web UI: http://localhost:8765" +echo "" +echo "💡 Set MEMORY_PROVIDER=openmemory_mcp in your backend configuration" \ No newline at end of file diff --git a/extras/speaker-recognition/setup.sh b/extras/speaker-recognition/setup.sh new file mode 100755 index 000000000..ede37cd5d --- /dev/null +++ b/extras/speaker-recognition/setup.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -e + +echo "🗣️ Speaker Recognition Setup" +echo "==============================" + +# Check if already configured +if [ -f ".env" ]; then + echo "⚠️ .env already exists. Backing up..." + cp .env .env.backup.$(date +%Y%m%d_%H%M%S) +fi + +# Start from template +cp .env.template .env + +# Prompt for required settings +echo "" +echo "🔑 Hugging Face Token (required for pyannote models)" +echo "Get yours from: https://huggingface.co/settings/tokens" +read -p "HF Token: " HF_TOKEN + +echo "" +echo "🖥️ Compute Mode" +echo " cpu - CPU-only (works everywhere)" +echo " gpu - GPU acceleration (requires NVIDIA+CUDA)" +read -p "Compute Mode [cpu/gpu]: " COMPUTE_MODE +COMPUTE_MODE=${COMPUTE_MODE:-cpu} + +# Update .env file +sed -i "s|HF_TOKEN=.*|HF_TOKEN=$HF_TOKEN|" .env +sed -i "s|COMPUTE_MODE=.*|COMPUTE_MODE=$COMPUTE_MODE|" .env + +echo "" +echo "✅ Speaker Recognition configured!" +echo "📁 Configuration saved to .env" +echo "" +echo "🚀 To start: docker compose up --build -d" \ No newline at end of file diff --git a/init.py b/init.py new file mode 100755 index 000000000..9e5d4d2c2 --- /dev/null +++ b/init.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +""" +Friend-Lite Root Setup Orchestrator +Handles service selection and delegation only - no configuration duplication +""" + +import subprocess +import sys +from pathlib import Path +from rich.console import Console +from rich.prompt import Confirm +from rich import print as rprint + +console = Console() + +SERVICES = { + 'backend': { + 'advanced': { + 'path': 'backends/advanced', + 'cmd': ['uv', 'run', '--with-requirements', 'setup-requirements.txt', 'python', 'init.py'], + 'description': 'Advanced AI backend with full feature set', + 'required': True + } + }, + 'extras': { + 'speaker-recognition': { + 'path': 'extras/speaker-recognition', + 'cmd': ['./setup.sh'], + 'description': 'Speaker identification and enrollment' + }, + 'asr-services': { + 'path': 'extras/asr-services', + 'cmd': ['./setup.sh'], + 'description': 'Offline speech-to-text (Parakeet)' + }, + 'openmemory-mcp': { + 'path': 'extras/openmemory-mcp', + 'cmd': ['./setup.sh'], + 'description': 'OpenMemory MCP server' + } + } +} + +def check_service_exists(service_name, service_config): + """Check if service directory and script exist""" + service_path = Path(service_config['path']) + if not service_path.exists(): + return False, f"Directory {service_path} does not exist" + + # For advanced backend, check if init.py exists + if service_name == 'advanced': + script_path = service_path / 'init.py' + if not script_path.exists(): + return False, f"Script {script_path} does not exist" + else: + # For extras, check if setup.sh exists + script_path = service_path / 'setup.sh' + if not script_path.exists(): + return False, f"Script {script_path} does not exist (will be created in Phase 2)" + + return True, "OK" + +def select_services(): + """Let user select which services to setup""" + console.print("🚀 [bold cyan]Friend-Lite Service Setup[/bold cyan]") + console.print("Select which services to configure:\n") + + selected = [] + + # Backend is required + console.print("📱 [bold]Backend (Required):[/bold]") + console.print(" ✅ Advanced Backend - Full AI features") + selected.append('advanced') + + # Optional extras + console.print("\n🔧 [bold]Optional Services:[/bold]") + for service_name, service_config in SERVICES['extras'].items(): + # Check if service exists + exists, msg = check_service_exists(service_name, service_config) + if not exists: + console.print(f" ⏸️ {service_config['description']} - [dim]{msg}[/dim]") + continue + + try: + enable_service = Confirm.ask(f" Setup {service_config['description']}?", default=False) + except EOFError: + console.print("Using default: No") + enable_service = False + + if enable_service: + selected.append(service_name) + + return selected + +def run_service_setup(service_name, selected_services): + """Execute individual service setup script""" + if service_name == 'advanced': + service = SERVICES['backend'][service_name] + + # For advanced backend, pass URLs of other selected services + cmd = service['cmd'].copy() + if 'speaker-recognition' in selected_services: + cmd.extend(['--speaker-service-url', 'http://host.docker.internal:8085']) + if 'asr-services' in selected_services: + cmd.extend(['--parakeet-asr-url', 'http://host.docker.internal:8767']) + else: + service = SERVICES['extras'][service_name] + cmd = service['cmd'] + + console.print(f"\n🔧 [bold]Setting up {service_name}...[/bold]") + + # Check if service exists before running + exists, msg = check_service_exists(service_name, service) + if not exists: + console.print(f"❌ {service_name} setup failed: {msg}") + return False + + try: + result = subprocess.run( + cmd, + cwd=service['path'], + check=False + ) + + if result.returncode == 0: + console.print(f"✅ {service_name} setup completed") + return True + else: + console.print(f"❌ {service_name} setup failed (exit code: {result.returncode})") + return False + + except FileNotFoundError as e: + console.print(f"❌ {service_name} setup failed: {e}") + return False + except Exception as e: + console.print(f"❌ {service_name} setup failed: {e}") + return False + +def show_service_status(): + """Show which services are available""" + console.print("\n📋 [bold]Service Status:[/bold]") + + # Check backend + exists, msg = check_service_exists('advanced', SERVICES['backend']['advanced']) + status = "✅" if exists else "❌" + console.print(f" {status} Advanced Backend - {msg}") + + # Check extras + for service_name, service_config in SERVICES['extras'].items(): + exists, msg = check_service_exists(service_name, service_config) + status = "✅" if exists else "⏸️" + console.print(f" {status} {service_config['description']} - {msg}") + +def main(): + """Main orchestration logic""" + console.print("🎉 [bold green]Welcome to Friend-Lite![/bold green]\n") + + # Show what's available + show_service_status() + + # Service Selection + selected_services = select_services() + + if not selected_services: + console.print("\n[yellow]No services selected. Exiting.[/yellow]") + return + + # Pure Delegation - Run Each Service Setup + console.print(f"\n📋 [bold]Setting up {len(selected_services)} services...[/bold]") + + success_count = 0 + failed_services = [] + + for service in selected_services: + if run_service_setup(service, selected_services): + success_count += 1 + else: + failed_services.append(service) + + # Final Summary + console.print(f"\n🎊 [bold green]Setup Complete![/bold green]") + console.print(f"✅ {success_count}/{len(selected_services)} services configured successfully") + + if failed_services: + console.print(f"❌ Failed services: {', '.join(failed_services)}") + + # Next Steps + console.print("\n📖 [bold]Next Steps:[/bold]") + if 'advanced' in selected_services and 'advanced' not in failed_services: + console.print("1. Start Advanced Backend:") + console.print(" [cyan]cd backends/advanced && docker compose up --build -d[/cyan]") + console.print("2. Access dashboard:") + console.print(" [cyan]http://localhost:5173[/cyan]") + + if 'speaker-recognition' in selected_services and 'speaker-recognition' not in failed_services: + console.print("3. Start Speaker Recognition:") + console.print(" [cyan]cd extras/speaker-recognition && docker compose up -d[/cyan]") + console.print(" Service will be available at: http://localhost:8085") + + if 'asr-services' in selected_services and 'asr-services' not in failed_services: + console.print("4. Parakeet ASR service is running:") + console.print(" Service available at: http://localhost:8767") + + if 'openmemory-mcp' in selected_services and 'openmemory-mcp' not in failed_services: + console.print("5. OpenMemory MCP server is running:") + console.print(" [cyan]Web UI: http://localhost:8765[/cyan]") + + console.print(f"\n🚀 [bold]Enjoy Friend-Lite![/bold]") + + # Show individual service usage + console.print(f"\n💡 [dim]Tip: You can also setup services individually:[/dim]") + console.print(f"[dim] cd backends/advanced && uv run --with-requirements setup-requirements.txt python init.py[/dim]") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/init.sh b/init.sh deleted file mode 100755 index 220c5b4b8..000000000 --- a/init.sh +++ /dev/null @@ -1,696 +0,0 @@ -#!/bin/bash - -# Friend-Lite Interactive Setup Script -# This script helps you configure Friend-Lite by asking a few key questions -# and setting up the environment files and Docker services. - -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -PURPLE='\033[0;35m' -NC='\033[0m' # No Color - -print_header() { - echo -e "${BLUE}================================================================${NC}" - echo -e "${BLUE}$1${NC}" - echo -e "${BLUE}================================================================${NC}" - echo -} - -print_info() { - echo -e "${BLUE}ℹ️ $1${NC}" -} - -print_success() { - echo -e "${GREEN}✅ $1${NC}" -} - -print_warning() { - echo -e "${YELLOW}⚠️ $1${NC}" -} - -print_error() { - echo -e "${RED}❌ $1${NC}" -} - -# Generate secure random string -generate_secret() { - openssl rand -hex 32 2>/dev/null || head -c 32 /dev/urandom | base64 | tr -d "=+/" | cut -c1-64 -} - -# Validate email format -validate_email() { - if [[ $1 =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then - return 0 - else - return 1 - fi -} - -# Validate URL format -validate_url() { - if [[ $1 =~ ^https?:// ]]; then - return 0 - else - return 1 - fi -} - -# Welcome message -clear -print_header "🎉 Welcome to Friend-Lite Setup!" - -cat << 'EOF' -This script will help you configure Friend-Lite, an AI-powered personal system -that captures audio from OMI-compatible devices and extracts: - -📝 Memories and conversations -🎯 Action items and tasks -🏠 Home automation triggers -🧠 Personal insights and patterns - -Let's get you set up with a few quick questions... -EOF - -echo -read -p "Press Enter to continue..." -echo - -# Backend selection -print_header "🖥️ Backend Selection" -echo "Choose your backend:" -echo "1. Advanced Backend (Recommended) - Full features with memory extraction" -echo "2. Simple Backend - Basic audio processing only" -echo - -while true; do - read -p "Select backend [1-2] (default: 1): " backend_choice - backend_choice=${backend_choice:-1} - - case $backend_choice in - 1) - BACKEND_DIR="backends/advanced" - BACKEND_NAME="Advanced Backend" - break - ;; - 2) - BACKEND_DIR="backends/simple-backend" - BACKEND_NAME="Simple Backend" - print_warning "Simple backend has limited features (no memory extraction)" - break - ;; - *) - print_error "Please select 1 or 2" - ;; - esac -done - -print_success "Selected: $BACKEND_NAME" -echo - -# LLM Provider Configuration -print_header "🤖 AI Language Model Configuration" -echo "Choose your LLM provider:" -echo "1. OpenAI (Recommended) - GPT-4o models, reliable API" -echo "2. Ollama - Local/self-hosted models, privacy-focused" -echo - -while true; do - read -p "Select LLM provider [1-2] (default: 1): " llm_choice - llm_choice=${llm_choice:-1} - - case $llm_choice in - 1) - LLM_PROVIDER="openai" - break - ;; - 2) - LLM_PROVIDER="ollama" - break - ;; - *) - print_error "Please select 1 or 2" - ;; - esac -done - -# Configure chosen LLM provider -if [[ $LLM_PROVIDER == "openai" ]]; then - print_info "OpenAI Configuration (sets LLM_PROVIDER=openai)" - echo - while true; do - read -p "Enter your OpenAI API key (sets OPENAI_API_KEY): " openai_key - if [[ ${#openai_key} -ge 20 && $openai_key == sk-* ]]; then - OPENAI_API_KEY="$openai_key" - break - else - print_error "Please enter a valid OpenAI API key (starts with 'sk-')" - fi - done - - read -p "OpenAI model (sets OPENAI_MODEL) [gpt-4o-mini]: " openai_model - OPENAI_MODEL=${openai_model:-gpt-4o-mini} - - OPENAI_BASE_URL="https://api.openai.com/v1" -else - print_info "Ollama Configuration (sets LLM_PROVIDER=ollama)" - echo - while true; do - read -p "Enter Ollama base URL (sets OPENAI_BASE_URL): " ollama_url - if validate_url "$ollama_url"; then - OPENAI_BASE_URL="$ollama_url" - break - else - print_error "Please enter a valid URL (e.g., http://localhost:11434/v1)" - fi - done - - read -p "Ollama model (sets OPENAI_MODEL) [llama3.1:latest]: " ollama_model - OPENAI_MODEL=${ollama_model:-llama3.1:latest} - - # Ollama uses dummy API key - OPENAI_API_KEY="dummy" -fi - -print_success "LLM provider configured: $LLM_PROVIDER with model $OPENAI_MODEL" -echo - -# Speech-to-Text Configuration -print_header "🎤 Speech-to-Text Configuration" -echo "Choose your transcription provider:" -echo "1. Deepgram API (Recommended) - High accuracy, cloud-based" -echo "2. Custom ASR Service - Self-hosted transcription" -echo "3. Skip - Configure later" -echo - -while true; do - read -p "Select transcription [1-3] (default: 1): " asr_choice - asr_choice=${asr_choice:-1} - - case $asr_choice in - 1) - TRANSCRIPTION_PROVIDER="deepgram" - while true; do - read -p "Enter Deepgram API key (sets DEEPGRAM_API_KEY): " deepgram_key - if [[ ${#deepgram_key} -ge 20 ]]; then - DEEPGRAM_API_KEY="$deepgram_key" - break - else - print_error "Please enter a valid Deepgram API key" - fi - done - break - ;; - 2) - TRANSCRIPTION_PROVIDER="parakeet" - read -p "Enter ASR service URL (sets PARAKEET_ASR_URL) [http://localhost:8767]: " asr_url - PARAKEET_ASR_URL=${asr_url:-http://localhost:8767} - break - ;; - 3) - print_warning "Transcription provider will need to be configured later" - break - ;; - *) - print_error "Please select 1, 2, or 3" - ;; - esac -done - -echo - -# Admin Configuration -print_header "👤 Admin Account Configuration" -while true; do - read -p "Admin email address (sets ADMIN_EMAIL): " admin_email - if validate_email "$admin_email"; then - ADMIN_EMAIL="$admin_email" - break - else - print_error "Please enter a valid email address" - fi -done - -print_info "Generating secure admin password and JWT secret (sets ADMIN_PASSWORD, AUTH_SECRET_KEY)..." -ADMIN_PASSWORD=$(generate_secret | cut -c1-20) -AUTH_SECRET_KEY=$(generate_secret) - -print_success "Admin credentials configured" -echo " 📧 Email: $ADMIN_EMAIL" -echo " 🔑 Password: $ADMIN_PASSWORD" -print_warning "Save this password - it won't be shown again!" -echo - -# Network Configuration -print_header "🌐 Network Configuration" -print_info "Configuring network access (sets HOST_IP, CORS_ORIGINS)" - -# Auto-detect IP -if command -v ip >/dev/null 2>&1; then - LOCAL_IP=$(ip route get 8.8.8.8 | awk '{print $7; exit}' 2>/dev/null || echo "localhost") -else - LOCAL_IP="localhost" -fi - -read -p "Backend host IP (sets HOST_IP) [${LOCAL_IP}]: " host_ip -HOST_IP=${host_ip:-$LOCAL_IP} - -BACKEND_PORT="8000" -WEBUI_PORT="3000" -CORS_ORIGINS="http://${HOST_IP}:${WEBUI_PORT},http://localhost:${WEBUI_PORT},http://127.0.0.1:${WEBUI_PORT}" - -print_success "Network configured for access at http://${HOST_IP}:${WEBUI_PORT}" -echo - -# Speaker Recognition Service Configuration -print_header "🗣️ Speaker Recognition Service (Optional)" -echo "Speaker recognition enables speaker identification for conversations." -echo "1. Set up speaker recognition service" -echo "2. Skip - Configure later" -echo - -while true; do - read -p "Configure speaker recognition? [1-2] (default: 2): " speaker_choice - speaker_choice=${speaker_choice:-2} - - case $speaker_choice in - 1) - SETUP_SPEAKER_RECOGNITION="yes" - - print_info "Speaker recognition requires a Hugging Face token for pyannote models" - while true; do - read -p "Enter your Hugging Face token (sets HF_TOKEN): " hf_token - if [[ ${#hf_token} -ge 10 ]]; then - HF_TOKEN="$hf_token" - break - else - print_error "Please enter a valid Hugging Face token" - fi - done - - echo "Choose compute mode for speaker recognition:" - echo "1. CPU (Recommended for development)" - echo "2. GPU (Requires NVIDIA GPU with CUDA)" - echo - - while true; do - read -p "Select compute mode [1-2] (default: 1): " compute_choice - compute_choice=${compute_choice:-1} - - case $compute_choice in - 1) - SPEAKER_COMPUTE_MODE="cpu" - break - ;; - 2) - SPEAKER_COMPUTE_MODE="gpu" - print_warning "GPU mode requires NVIDIA GPU with CUDA drivers" - break - ;; - *) - print_error "Please select 1 or 2" - ;; - esac - done - - read -p "Speaker similarity threshold (0.0-1.0, higher=more strict) [0.15]: " similarity_threshold - SIMILARITY_THRESHOLD=${similarity_threshold:-0.15} - - print_success "Speaker recognition configured with $SPEAKER_COMPUTE_MODE mode" - break - ;; - 2) - print_warning "Speaker recognition setup skipped - can be configured later" - break - ;; - *) - print_error "Please select 1 or 2" - ;; - esac -done - -echo - -# HTTPS Configuration -print_header "🔒 HTTPS Configuration (Optional)" -echo "HTTPS setup provides secure access and works well with Tailscale networks." -echo "1. Set up HTTPS with Tailscale IP" -echo "2. Set up HTTPS with localhost only" -echo "3. Skip - Use HTTP only" -echo - -while true; do - read -p "Configure HTTPS? [1-3] (default: 3): " https_choice - https_choice=${https_choice:-3} - - case $https_choice in - 1) - SETUP_HTTPS="yes" - read -p "Enter your Tailscale IP address: " tailscale_ip - if [[ $tailscale_ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then - TAILSCALE_IP="$tailscale_ip" - print_success "HTTPS will be configured for localhost and $TAILSCALE_IP" - else - print_error "Invalid IP format, using localhost only" - TAILSCALE_IP="127.0.0.1" - fi - break - ;; - 2) - SETUP_HTTPS="yes" - TAILSCALE_IP="127.0.0.1" - print_success "HTTPS will be configured for localhost only" - break - ;; - 3) - print_warning "HTTPS setup skipped - using HTTP only" - break - ;; - *) - print_error "Please select 1, 2, or 3" - ;; - esac -done - -echo - -# Configuration Summary -print_header "📋 Configuration Summary" -echo "Backend: $BACKEND_NAME" -echo "LLM Provider: $LLM_PROVIDER ($OPENAI_MODEL)" -if [[ -n $DEEPGRAM_API_KEY ]]; then - echo "Speech-to-Text: Deepgram API" -elif [[ -n $PARAKEET_ASR_URL ]]; then - echo "Speech-to-Text: Custom ASR ($PARAKEET_ASR_URL)" -else - echo "Speech-to-Text: Not configured" -fi -if [[ $SETUP_SPEAKER_RECOGNITION == "yes" ]]; then - echo "Speaker Recognition: Enabled ($SPEAKER_COMPUTE_MODE mode)" -else - echo "Speaker Recognition: Not configured" -fi -if [[ $SETUP_HTTPS == "yes" ]]; then - echo "HTTPS: Enabled (localhost + $TAILSCALE_IP)" - echo "Access URL: https://${HOST_IP} (or https://${TAILSCALE_IP})" -else - echo "HTTPS: Not configured" - echo "Access URL: http://${HOST_IP}:${WEBUI_PORT}" -fi -echo "Admin Email: $ADMIN_EMAIL" -echo - -read -p "Continue with setup? [Y/n]: " confirm -if [[ $confirm == "n" || $confirm == "N" ]]; then - print_info "Setup cancelled" - exit 0 -fi - -# File Creation -print_header "📁 Creating Configuration Files" - -# Navigate to backend directory -cd "$BACKEND_DIR" - -# Create .env file -print_info "Creating .env file with all configured environment variables..." -cat > .env << EOF -# Friend-Lite Configuration -# Generated by init.sh on $(date) - -# Authentication (sets AUTH_SECRET_KEY, ADMIN_PASSWORD, ADMIN_EMAIL) -AUTH_SECRET_KEY=$AUTH_SECRET_KEY -ADMIN_PASSWORD=$ADMIN_PASSWORD -ADMIN_EMAIL=$ADMIN_EMAIL - -# LLM Configuration (sets LLM_PROVIDER, OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_MODEL) -LLM_PROVIDER=$LLM_PROVIDER -OPENAI_API_KEY=$OPENAI_API_KEY -OPENAI_BASE_URL=$OPENAI_BASE_URL -OPENAI_MODEL=$OPENAI_MODEL - -EOF - -# Add transcription config if configured -if [[ -n $DEEPGRAM_API_KEY ]]; then - cat >> .env << EOF -# Speech-to-Text Configuration (sets DEEPGRAM_API_KEY, TRANSCRIPTION_PROVIDER) -DEEPGRAM_API_KEY=$DEEPGRAM_API_KEY -TRANSCRIPTION_PROVIDER=$TRANSCRIPTION_PROVIDER - -EOF -elif [[ -n $PARAKEET_ASR_URL ]]; then - cat >> .env << EOF -# Speech-to-Text Configuration (sets PARAKEET_ASR_URL, TRANSCRIPTION_PROVIDER) -PARAKEET_ASR_URL=$PARAKEET_ASR_URL -TRANSCRIPTION_PROVIDER=$TRANSCRIPTION_PROVIDER - -EOF -fi - -# Add standard database and network config -cat >> .env << EOF -# Database Configuration -MONGODB_URI=mongodb://mongo:27017 -QDRANT_BASE_URL=qdrant - -# Network Configuration (sets HOST_IP, BACKEND_PUBLIC_PORT, WEBUI_PORT, CORS_ORIGINS) -HOST_IP=$HOST_IP -BACKEND_PUBLIC_PORT=$BACKEND_PORT -WEBUI_PORT=$WEBUI_PORT -CORS_ORIGINS=$CORS_ORIGINS -EOF - -# Add speaker recognition config if configured -if [[ $SETUP_SPEAKER_RECOGNITION == "yes" ]]; then - cat >> .env << EOF -# Speaker Recognition Configuration (sets HF_TOKEN, SPEAKER_SERVICE_URL) -HF_TOKEN=$HF_TOKEN -SPEAKER_SERVICE_URL=http://host.docker.internal:8085 -EOF -fi - -print_success ".env file created" - -# Copy memory config for advanced backend -if [[ $BACKEND_DIR == "backends/advanced" ]]; then - if [[ -f "memory_config.yaml.template" ]]; then - print_info "Creating memory_config.yaml..." - cp memory_config.yaml.template memory_config.yaml - print_success "memory_config.yaml created from template" - fi -fi - -# Docker Setup -print_header "🐳 Starting Docker Services" -print_info "Building and starting services..." - -# Start Docker services -if docker compose up --build -d; then - print_success "Docker services started successfully!" - echo - - print_info "Waiting for services to be ready..." - sleep 10 - - # Check service health - if curl -s http://localhost:$BACKEND_PORT/health >/dev/null 2>&1; then - print_success "Backend service is healthy" - else - print_warning "Backend may still be starting up" - fi - -else - print_error "Failed to start Docker services" - print_info "You can try running 'docker compose up --build -d' manually" - exit 1 -fi - -# Additional Service Setup -cd .. # Back to project root - -# Set up speaker recognition service if requested -if [[ $SETUP_SPEAKER_RECOGNITION == "yes" ]]; then - print_header "🗣️ Setting up Speaker Recognition Service" - - if [[ -d "extras/speaker-recognition" ]]; then - cd extras/speaker-recognition - - # Create .env file for speaker recognition - print_info "Creating speaker recognition .env file..." - cat > .env << EOF -# Speaker Recognition Service Environment Configuration -# Generated by init.sh on $(date) - -# Required: Hugging Face token for pyannote models -HF_TOKEN=$HF_TOKEN - -# Docker build configuration -COMPUTE_MODE=$SPEAKER_COMPUTE_MODE - -# Speaker recognition similarity threshold -SIMILARITY_THRESHOLD=$SIMILARITY_THRESHOLD - -# Service Configuration -SPEAKER_SERVICE_HOST=speaker-service -SPEAKER_SERVICE_PORT=8085 -SPEAKER_SERVICE_URL=http://speaker-service:8085 - -# React Web UI Configuration -REACT_UI_HOST=0.0.0.0 -REACT_UI_PORT=5173 -REACT_UI_HTTPS=false - -# Optional: External Services -DEEPGRAM_API_KEY=$DEEPGRAM_API_KEY - -# Test Configuration -SPEAKER_SERVICE_TEST_PORT=8086 -EOF - - # Set up HTTPS for speaker recognition if requested - if [[ $SETUP_HTTPS == "yes" ]]; then - print_info "Setting up HTTPS for speaker recognition..." - - # Generate SSL certificates for speaker recognition - if [[ -f "ssl/generate-ssl.sh" ]]; then - chmod +x ssl/generate-ssl.sh - ./ssl/generate-ssl.sh "$TAILSCALE_IP" - print_success "SSL certificates generated for speaker recognition" - else - print_warning "SSL generation script not found for speaker recognition" - fi - - # Configure nginx.conf with Tailscale IP - if [[ -f "nginx.conf.template" ]]; then - sed "s/TAILSCALE_IP/$TAILSCALE_IP/g" nginx.conf.template > nginx.conf - print_success "nginx.conf configured for speaker recognition with Tailscale IP: $TAILSCALE_IP" - else - print_warning "nginx.conf.template not found for speaker recognition" - fi - fi - - print_info "Starting speaker recognition services..." - if docker compose up --build -d; then - print_success "Speaker recognition service started!" - else - print_warning "Failed to start speaker recognition service - check logs with 'docker compose logs'" - fi - - cd ../.. # Back to project root - else - print_error "Speaker recognition directory not found at extras/speaker-recognition" - fi -fi - -# Set up HTTPS if requested -if [[ $SETUP_HTTPS == "yes" ]]; then - print_header "🔒 Setting up HTTPS" - - cd "$BACKEND_DIR" - - # Generate SSL certificates - print_info "Generating SSL certificates..." - if [[ -f "ssl/generate-ssl.sh" ]]; then - chmod +x ssl/generate-ssl.sh - ./ssl/generate-ssl.sh "$TAILSCALE_IP" - print_success "SSL certificates generated" - else - print_error "SSL generation script not found" - fi - - # Configure nginx.conf with Tailscale IP - print_info "Configuring nginx for HTTPS..." - if [[ -f "nginx.conf.template" ]]; then - sed "s/TAILSCALE_IP/$TAILSCALE_IP/g" nginx.conf.template > nginx.conf - print_success "nginx.conf configured with Tailscale IP: $TAILSCALE_IP" - else - print_error "nginx.conf.template not found" - fi - - # Restart services to enable nginx - print_info "Restarting services to enable HTTPS..." - docker compose restart - - # Wait for services to be ready - sleep 5 - - cd .. # Back to project root -fi - -# Final Success Message -print_header "🎉 Setup Complete!" - -cat << EOF -Friend-Lite is now running! Here's how to access it: - -EOF - -if [[ $SETUP_HTTPS == "yes" ]]; then - cat << EOF -🌐 Web Interface: https://${HOST_IP} or https://${TAILSCALE_IP} -🔧 API Endpoint: https://${HOST_IP}/api or https://${TAILSCALE_IP}/api - -EOF -else - cat << EOF -🌐 Web Interface: http://${HOST_IP}:${WEBUI_PORT} -🔧 API Endpoint: http://${HOST_IP}:${BACKEND_PORT} - -EOF -fi - -cat << EOF -👤 Admin Login: - Email: ${ADMIN_EMAIL} - Password: ${ADMIN_PASSWORD} - -EOF - -if [[ $SETUP_SPEAKER_RECOGNITION == "yes" ]]; then - if [[ $SETUP_HTTPS == "yes" ]]; then - cat << EOF -🗣️ Speaker Recognition: https://${HOST_IP}:8444 or https://${TAILSCALE_IP}:8444 (Web UI) - Service API: https://${HOST_IP}:8444/api or https://${TAILSCALE_IP}:8444/api - -EOF - else - cat << EOF -🗣️ Speaker Recognition: http://localhost:5173 (Web UI) - Service API: http://localhost:8085 - -EOF - fi -fi - -cat << EOF -📖 Next Steps: - 1. Open the web interface and log in - 2. Connect your OMI device via the mobile app - 3. Start recording conversations! - -📚 Documentation: - - See CLAUDE.md for developer information - - Visit backends/advanced/Docs/ for detailed guides - -🧪 Testing: - Run './run-test.sh' to test the full pipeline - -🔧 Configuration: - - Edit .env files to modify settings - - Run 'docker compose restart' after changes - - Use 'docker compose logs' to view service logs - -EOF - -if [[ -n $DEEPGRAM_API_KEY && -n $OPENAI_API_KEY ]]; then - echo "🧪 To test the full pipeline:" - echo " cd backends/advanced && ./run-test.sh" - echo -fi - -print_success "Enjoy using Friend-Lite! 🚀" \ No newline at end of file diff --git a/setup-requirements.txt b/setup-requirements.txt new file mode 100644 index 000000000..78ba74206 --- /dev/null +++ b/setup-requirements.txt @@ -0,0 +1,2 @@ +# Dependencies for Friend-Lite root setup orchestrator +rich>=13.0.0 \ No newline at end of file From a26729cf0a19a951316b284566d010716f1f6924 Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:09:25 +0000 Subject: [PATCH 07/35] hehe --- backends/advanced/docker-compose.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backends/advanced/docker-compose.yml b/backends/advanced/docker-compose.yml index e9db4e761..dfe1553d4 100644 --- a/backends/advanced/docker-compose.yml +++ b/backends/advanced/docker-compose.yml @@ -42,8 +42,9 @@ services: - OPENMEMORY_USER_ID=${OPENMEMORY_USER_ID:-openmemory} - OPENMEMORY_TIMEOUT=${OPENMEMORY_TIMEOUT:-30} depends_on: - qdrant: - condition: service_started + # You may not want qdrant if you are using openmemory_mcp + # qdrant: + # condition: service_started mongo: condition: service_started # neo4j-mem0: From e595f3ef6498f229e4637ee4ff30c3dcfffc3d7d Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:19:16 +0000 Subject: [PATCH 08/35] hellz yea --- .gitignore | 2 ++ extras/asr-services/.env.template | 9 +++++++++ extras/asr-services/docker-compose.yml | 2 +- extras/asr-services/setup.sh | 11 ++++++++++- 4 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 extras/asr-services/.env.template diff --git a/.gitignore b/.gitignore index 5322f0926..de8d48f03 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,5 @@ backends/advanced-backend/data/speaker_model_cache/ *.bin *.sqlite3 *checkpoints + +extras/openmemory-mcp/data/* \ No newline at end of file diff --git a/extras/asr-services/.env.template b/extras/asr-services/.env.template new file mode 100644 index 000000000..f68d87e66 --- /dev/null +++ b/extras/asr-services/.env.template @@ -0,0 +1,9 @@ +# ASR Services Configuration +# Copy this file to .env and configure as needed + +# Parakeet ASR Model Selection +PARAKEET_MODEL=nvidia/parakeet-tdt-0.6b-v3 + +# Service Port Configuration +PARAKEET_HOST_PORT=8767 +PARAKEET_CONTAINER_PORT=8765 \ No newline at end of file diff --git a/extras/asr-services/docker-compose.yml b/extras/asr-services/docker-compose.yml index 9e6c63c72..5c90357b0 100644 --- a/extras/asr-services/docker-compose.yml +++ b/extras/asr-services/docker-compose.yml @@ -5,7 +5,7 @@ services: dockerfile: Dockerfile_Parakeet image: parakeet-asr:latest ports: - - "8080:8765" # Host port 8080 -> Container port 8765 + - "${PARAKEET_HOST_PORT:-8767}:${PARAKEET_CONTAINER_PORT:-8765}" volumes: # Persist Hugging Face cache (models + tokenizer) between restarts - ./model_cache:/models diff --git a/extras/asr-services/setup.sh b/extras/asr-services/setup.sh index 12ee400be..95c2280a9 100755 --- a/extras/asr-services/setup.sh +++ b/extras/asr-services/setup.sh @@ -4,11 +4,20 @@ set -e echo "🎤 Offline ASR Setup" echo "===================" +# Create .env file if it doesn't exist +if [ ! -f ".env" ]; then + echo "📄 Creating .env file from template..." + cp .env.template .env + echo "✅ .env file created" +else + echo "ℹ️ .env file already exists, using existing configuration" +fi + echo "Starting Parakeet ASR service..." echo "" # Start Parakeet ASR service -docker compose up parakeet -d +docker compose up parakeet-asr -d echo "✅ Parakeet ASR running:" echo " 📝 Service URL: http://host.docker.internal:8767" From 061258725918ab56a82b7d095599894c273ce602 Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Mon, 1 Sep 2025 13:01:46 +0000 Subject: [PATCH 09/35] fix --- Docs/init-system.md | 174 +++++++++++++++++++++++++++++++++ extras/openmemory-mcp/setup.sh | 9 ++ 2 files changed, 183 insertions(+) create mode 100644 Docs/init-system.md diff --git a/Docs/init-system.md b/Docs/init-system.md new file mode 100644 index 000000000..e21747cb1 --- /dev/null +++ b/Docs/init-system.md @@ -0,0 +1,174 @@ +# Friend-Lite Initialization System + +## Overview + +Friend-Lite uses a unified initialization system that allows you to set up services either individually or together through a root orchestrator. The system uses pure delegation - the root orchestrator handles service selection and startup coordination, while each service manages its own configuration. + +## Architecture + +### Root Orchestrator +- **Location**: `/init.py` +- **Purpose**: Service selection and delegation only +- **Does NOT**: Handle service-specific configuration or duplicate setup logic + +### Service Scripts +- **Backend**: `backends/advanced/init.py` - Complete Python-based interactive setup +- **Speaker Recognition**: `extras/speaker-recognition/setup.sh` - Simple bash setup +- **ASR Services**: `extras/asr-services/setup.sh` - Service startup script +- **OpenMemory MCP**: `extras/openmemory-mcp/setup.sh` - External server startup + +## Usage + +### Orchestrated Setup (Recommended) +Set up multiple services together with automatic URL coordination: + +```bash +# From project root +uv run --with-requirements setup-requirements.txt python init.py +``` + +The orchestrator will: +1. Show service status and availability +2. Let you select which services to configure +3. Automatically pass service URLs between services +4. Display next steps for starting services + +### Individual Service Setup +Each service can be configured independently: + +```bash +# Advanced Backend only +cd backends/advanced +uv run --with-requirements setup-requirements.txt python init.py + +# Speaker Recognition only +cd extras/speaker-recognition +./setup.sh + +# ASR Services only +cd extras/asr-services +./setup.sh + +# OpenMemory MCP only +cd extras/openmemory-mcp +./setup.sh +``` + +## Service Details + +### Advanced Backend +- **Interactive setup** for authentication, LLM, transcription, and memory providers +- **Accepts arguments**: `--speaker-service-url`, `--parakeet-asr-url` +- **Generates**: Complete `.env` file with all required configuration +- **Default ports**: Backend (8000), WebUI (5173) + +### Speaker Recognition +- **Prompts for**: Hugging Face token, compute mode (cpu/gpu) +- **Service port**: 8085 +- **WebUI port**: 5173 +- **Requires**: HF_TOKEN for pyannote models + +### ASR Services +- **Starts**: Parakeet ASR service via Docker Compose +- **Service port**: 8767 +- **Purpose**: Offline speech-to-text processing +- **No configuration required** + +### OpenMemory MCP +- **Starts**: External OpenMemory MCP server +- **Service port**: 8765 +- **WebUI**: Available at http://localhost:8765 +- **Purpose**: Cross-client memory compatibility + +## Automatic URL Coordination + +When using the orchestrated setup, service URLs are automatically configured: + +| Service Selected | Backend Gets Configured With | +|------------------|------------------------------| +| Speaker Recognition | `SPEAKER_SERVICE_URL=http://host.docker.internal:8085` | +| ASR Services | `PARAKEET_ASR_URL=http://host.docker.internal:8767` | + +This eliminates the need to manually configure service URLs when running services on the same machine. + +## Service URLs + +### Default Service Endpoints +- **Backend API**: http://localhost:8000 +- **Backend WebUI**: http://localhost:5173 +- **Speaker Recognition**: http://localhost:8085 +- **Speaker Recognition WebUI**: http://localhost:5173 +- **Parakeet ASR**: http://localhost:8767 +- **OpenMemory MCP**: http://localhost:8765 + +### Container-to-Container Communication +Services use `host.docker.internal` for inter-container communication: +- `http://host.docker.internal:8085` - Speaker Recognition +- `http://host.docker.internal:8767` - Parakeet ASR +- `http://host.docker.internal:8765` - OpenMemory MCP + +## Starting Services + +After running setup, start services with Docker Compose: + +```bash +# Start Advanced Backend +cd backends/advanced +docker compose up --build -d + +# Start Speaker Recognition +cd extras/speaker-recognition +docker compose up --build -d + +# Start ASR Services +cd extras/asr-services +docker compose up parakeet -d + +# Start OpenMemory MCP +cd extras/openmemory-mcp +docker compose up -d +``` + +## Configuration Files + +### Generated Files +- `backends/advanced/.env` - Backend configuration with all services +- `extras/speaker-recognition/.env` - Speaker service configuration +- All services backup existing `.env` files automatically + +### Required Dependencies +- **Root**: `setup-requirements.txt` (rich>=13.0.0) +- **Backend**: `setup-requirements.txt` (rich>=13.0.0, pyyaml>=6.0.0) +- **Extras**: No additional setup dependencies required + +## Troubleshooting + +### Common Issues +- **Port conflicts**: Check if services are already running on default ports +- **Permission errors**: Ensure scripts are executable (`chmod +x setup.sh`) +- **Missing dependencies**: Install uv and ensure setup-requirements.txt dependencies available +- **Service startup failures**: Check Docker is running and has sufficient resources + +### Service Health Checks +```bash +# Backend health +curl http://localhost:8000/health + +# Speaker Recognition health +curl http://localhost:8085/health + +# ASR service health +curl http://localhost:8767/health +``` + +### Logs and Debugging +```bash +# View service logs +docker compose logs [service-name] + +# Backend logs +cd backends/advanced && docker compose logs friend-backend + +# Speaker Recognition logs +cd extras/speaker-recognition && docker compose logs speaker-service +``` \ No newline at end of file diff --git a/extras/openmemory-mcp/setup.sh b/extras/openmemory-mcp/setup.sh index 0a720e01d..21905b657 100755 --- a/extras/openmemory-mcp/setup.sh +++ b/extras/openmemory-mcp/setup.sh @@ -4,6 +4,15 @@ set -e echo "🧠 OpenMemory MCP Setup" echo "======================" +# Create .env file if it doesn't exist +if [ ! -f ".env" ]; then + echo "📄 Creating .env file from template..." + cp .env.template .env + echo "✅ .env file created" +else + echo "ℹ️ .env file already exists, using existing configuration" +fi + echo "Starting OpenMemory MCP server..." echo "" From 4667ceb6eef61b3f9066fcffe3a2211387f872e0 Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Mon, 1 Sep 2025 13:14:38 +0000 Subject: [PATCH 10/35] another 1 --- Docs/init-system.md | 62 ++++++++++++++++++++++++++-------- README.md | 23 ++++++------- extras/asr-services/setup.sh | 13 +++---- extras/openmemory-mcp/setup.sh | 11 +++--- init.py | 38 +++++++++++++-------- 5 files changed, 91 insertions(+), 56 deletions(-) diff --git a/Docs/init-system.md b/Docs/init-system.md index e21747cb1..0e66ea884 100644 --- a/Docs/init-system.md +++ b/Docs/init-system.md @@ -2,7 +2,12 @@ ## Overview -Friend-Lite uses a unified initialization system that allows you to set up services either individually or together through a root orchestrator. The system uses pure delegation - the root orchestrator handles service selection and startup coordination, while each service manages its own configuration. +Friend-Lite uses a unified initialization system with clean separation of concerns: + +- **Configuration** (`init.py`) - Set up service configurations, API keys, and .env files +- **Service Management** (`services.py`) - Start, stop, and manage running services + +The root orchestrator handles service selection and delegates configuration to individual service scripts. **Setup scripts only configure - they don't start services automatically.** This prevents unnecessary resource usage and gives you control over when services actually run. ## Architecture @@ -91,6 +96,14 @@ When using the orchestrated setup, service URLs are automatically configured: This eliminates the need to manually configure service URLs when running services on the same machine. +## Key Benefits + +✅ **No Unnecessary Building** - Services are only started when you explicitly request them +✅ **Resource Efficient** - Parakeet ASR won't start if you're using cloud transcription +✅ **Clean Separation** - Configuration vs service management are separate concerns +✅ **Unified Control** - Single command to start/stop all services +✅ **Selective Starting** - Choose which services to run based on your current needs + ## Service URLs ### Default Service Endpoints @@ -107,26 +120,45 @@ Services use `host.docker.internal` for inter-container communication: - `http://host.docker.internal:8767` - Parakeet ASR - `http://host.docker.internal:8765` - OpenMemory MCP -## Starting Services +## Service Management -After running setup, start services with Docker Compose: +Friend-Lite now separates **configuration** from **service lifecycle management**: + +### Unified Service Management +Use the `services.py` script for all service operations: ```bash -# Start Advanced Backend -cd backends/advanced -docker compose up --build -d +# Start all configured services +python services.py start --all --build -# Start Speaker Recognition -cd extras/speaker-recognition -docker compose up --build -d +# Start specific services +python services.py start backend speaker-recognition + +# Check service status +python services.py status + +# Stop all services +python services.py stop --all + +# Stop specific services +python services.py stop asr-services openmemory-mcp +``` + +### Manual Service Management +You can also manage services individually: + +```bash +# Advanced Backend +cd backends/advanced && docker compose up --build -d + +# Speaker Recognition +cd extras/speaker-recognition && docker compose up --build -d -# Start ASR Services -cd extras/asr-services -docker compose up parakeet -d +# ASR Services (only if using offline transcription) +cd extras/asr-services && docker compose up --build -d -# Start OpenMemory MCP -cd extras/openmemory-mcp -docker compose up -d +# OpenMemory MCP (only if using openmemory_mcp provider) +cd extras/openmemory-mcp && docker compose up --build -d ``` ## Configuration Files diff --git a/README.md b/README.md index 7e749850a..5eff9a676 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,16 @@ ## Quick Start **Interactive setup (recommended):** -1. Go to `backends/advanced/` for the main application -2. Run `./init.sh` to configure all services with guided prompts -3. Run `docker compose up --build -d` to start the system -4. Visit `http://localhost:5173` for the React web dashboard +1. From the project root, run `uv run --with-requirements setup-requirements.txt python init.py` to configure all services with guided prompts +2. Run `python services.py start --all --build` to start all configured services +3. Visit `http://localhost:5173` for the React web dashboard **Manual setup (alternative):** -1. Copy `.env.template` to `.env` and configure your API keys manually -2. Set up transcription provider (Deepgram/Mistral/Offline), LLM provider (OpenAI/Ollama), and memory provider -3. Start services and optional extras as needed +1. Go to `backends/advanced/` and copy `.env.template` to `.env` +2. Configure your API keys and service settings manually +3. Start with `docker compose up --build -d` -**Documentation:** See `CLAUDE.md` and `backends/advanced/Docs/quickstart.md` for detailed setup +**Documentation:** See `CLAUDE.md` and `Docs/init-system.md` for detailed setup guide ## Overview Friend-Lite provides essential components for developers working with OMI-compatible audio devices: @@ -121,7 +120,7 @@ Choose one based on your needs: --- -#### **Advanced Backend** (`backends/advanced-backend/`) **RECOMMENDED** +#### **Advanced Backend** (`backends/advanced/`) **RECOMMENDED** **Use case:** Production use, full feature set **Features:** @@ -215,9 +214,9 @@ Backends and ASR services use standardized audio streaming: ## For Production Use 1. Use **Advanced Backend** for full features -2. Set up the complete stack: MongoDB + Qdrant + Ollama -3. Access the Web UI for conversation management -4. Configure speaker enrollment for multi-user scenarios +2. Run the orchestrated setup: `uv run --with-requirements setup-requirements.txt python init.py` +3. Start all services: `python services.py start --all --build` +4. Access the Web UI at http://localhost:5173 for conversation management ## For OMI Users 1. Use **OMI-Webhook-Compatible Backend** for easy migration diff --git a/extras/asr-services/setup.sh b/extras/asr-services/setup.sh index 95c2280a9..cbf7fc08c 100755 --- a/extras/asr-services/setup.sh +++ b/extras/asr-services/setup.sh @@ -13,13 +13,10 @@ else echo "ℹ️ .env file already exists, using existing configuration" fi -echo "Starting Parakeet ASR service..." +echo "✅ Parakeet ASR configured!" +echo "📁 Configuration saved to .env" echo "" - -# Start Parakeet ASR service -docker compose up parakeet-asr -d - -echo "✅ Parakeet ASR running:" -echo " 📝 Service URL: http://host.docker.internal:8767" +echo "🚀 To start: docker compose up --build -d parakeet-asr" +echo " 📝 Service will be available at: http://host.docker.internal:8767" echo "" -echo "💡 Configure PARAKEET_ASR_URL in your backend's transcription settings" \ No newline at end of file +echo "💡 Only start if you want to use offline ASR instead of cloud providers" \ No newline at end of file diff --git a/extras/openmemory-mcp/setup.sh b/extras/openmemory-mcp/setup.sh index 21905b657..102c3c245 100755 --- a/extras/openmemory-mcp/setup.sh +++ b/extras/openmemory-mcp/setup.sh @@ -13,14 +13,11 @@ else echo "ℹ️ .env file already exists, using existing configuration" fi -echo "Starting OpenMemory MCP server..." +echo "✅ OpenMemory MCP configured!" +echo "📁 Configuration saved to .env" echo "" - -# Start external server -docker compose up -d - -echo "✅ OpenMemory MCP running!" +echo "🚀 To start: docker compose up --build -d" echo " 🌐 Server: http://host.docker.internal:8765" echo " 📱 Web UI: http://localhost:8765" echo "" -echo "💡 Set MEMORY_PROVIDER=openmemory_mcp in your backend configuration" \ No newline at end of file +echo "💡 Only start if you selected openmemory_mcp as memory provider" \ No newline at end of file diff --git a/init.py b/init.py index 9e5d4d2c2..681b69ea2 100755 --- a/init.py +++ b/init.py @@ -186,24 +186,34 @@ def main(): # Next Steps console.print("\n📖 [bold]Next Steps:[/bold]") - if 'advanced' in selected_services and 'advanced' not in failed_services: - console.print("1. Start Advanced Backend:") - console.print(" [cyan]cd backends/advanced && docker compose up --build -d[/cyan]") - console.print("2. Access dashboard:") - console.print(" [cyan]http://localhost:5173[/cyan]") - if 'speaker-recognition' in selected_services and 'speaker-recognition' not in failed_services: - console.print("3. Start Speaker Recognition:") - console.print(" [cyan]cd extras/speaker-recognition && docker compose up -d[/cyan]") - console.print(" Service will be available at: http://localhost:8085") + # Service Management Commands + console.print("1. Start all configured services:") + console.print(" [cyan]uv run --with-requirements setup-requirements.txt python services.py start --all --build[/cyan]") + console.print("") + console.print("2. Or start individual services:") + configured_services = [] + if 'advanced' in selected_services and 'advanced' not in failed_services: + configured_services.append("backend") + if 'speaker-recognition' in selected_services and 'speaker-recognition' not in failed_services: + configured_services.append("speaker-recognition") if 'asr-services' in selected_services and 'asr-services' not in failed_services: - console.print("4. Parakeet ASR service is running:") - console.print(" Service available at: http://localhost:8767") - + configured_services.append("asr-services") if 'openmemory-mcp' in selected_services and 'openmemory-mcp' not in failed_services: - console.print("5. OpenMemory MCP server is running:") - console.print(" [cyan]Web UI: http://localhost:8765[/cyan]") + configured_services.append("openmemory-mcp") + + if configured_services: + service_list = " ".join(configured_services) + console.print(f" [cyan]python services.py start {service_list}[/cyan]") + + console.print("") + console.print("3. Check service status:") + console.print(" [cyan]python services.py status[/cyan]") + + console.print("") + console.print("4. Stop services when done:") + console.print(" [cyan]python services.py stop --all[/cyan]") console.print(f"\n🚀 [bold]Enjoy Friend-Lite![/bold]") From 3d5d9a2e4c3a933f456a1883cf524373b985105e Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Mon, 1 Sep 2025 13:18:34 +0000 Subject: [PATCH 11/35] services --- services.py | 218 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100755 services.py diff --git a/services.py b/services.py new file mode 100755 index 000000000..af887b795 --- /dev/null +++ b/services.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +""" +Friend-Lite Service Management +Start, stop, and manage configured services +""" + +import subprocess +import sys +import argparse +from pathlib import Path +from rich.console import Console +from rich.table import Table +from rich import print as rprint + +console = Console() + +SERVICES = { + 'backend': { + 'path': 'backends/advanced', + 'compose_file': 'docker-compose.yml', + 'description': 'Advanced Backend + WebUI', + 'ports': ['8000', '5173'] + }, + 'speaker-recognition': { + 'path': 'extras/speaker-recognition', + 'compose_file': 'docker-compose.yml', + 'description': 'Speaker Recognition Service', + 'ports': ['8085', '5173'] + }, + 'asr-services': { + 'path': 'extras/asr-services', + 'compose_file': 'docker-compose.yml', + 'description': 'Parakeet ASR Service', + 'ports': ['8767'] + }, + 'openmemory-mcp': { + 'path': 'extras/openmemory-mcp', + 'compose_file': 'docker-compose.yml', + 'description': 'OpenMemory MCP Server', + 'ports': ['8765'] + } +} + +def check_service_configured(service_name): + """Check if service is configured (has .env file)""" + service = SERVICES[service_name] + service_path = Path(service['path']) + + # Backend uses advanced init, others use .env + if service_name == 'backend': + return (service_path / '.env').exists() + else: + return (service_path / '.env').exists() + +def run_compose_command(service_name, command, build=False): + """Run docker compose command for a service""" + service = SERVICES[service_name] + service_path = Path(service['path']) + + if not service_path.exists(): + console.print(f"[red]❌ Service directory not found: {service_path}[/red]") + return False + + compose_file = service_path / service['compose_file'] + if not compose_file.exists(): + console.print(f"[red]❌ Docker compose file not found: {compose_file}[/red]") + return False + + cmd = ['docker', 'compose'] + if command == 'up': + cmd.extend(['up', '-d']) + if build: + cmd.append('--build') + elif command == 'down': + cmd.extend(['down']) + elif command == 'restart': + cmd.extend(['restart']) + elif command == 'status': + cmd.extend(['ps']) + + try: + result = subprocess.run( + cmd, + cwd=service_path, + capture_output=True, + text=True, + check=False + ) + + if result.returncode == 0: + return True + else: + console.print(f"[red]❌ Command failed: {result.stderr}[/red]") + return False + + except Exception as e: + console.print(f"[red]❌ Error running command: {e}[/red]") + return False + +def start_services(services, build=False): + """Start specified services""" + console.print(f"🚀 [bold]Starting {len(services)} services...[/bold]") + + success_count = 0 + for service_name in services: + if service_name not in SERVICES: + console.print(f"[red]❌ Unknown service: {service_name}[/red]") + continue + + if not check_service_configured(service_name): + console.print(f"[yellow]⚠️ {service_name} not configured, skipping[/yellow]") + continue + + console.print(f"\n🔧 Starting {service_name}...") + if run_compose_command(service_name, 'up', build): + console.print(f"[green]✅ {service_name} started[/green]") + success_count += 1 + else: + console.print(f"[red]❌ Failed to start {service_name}[/red]") + + console.print(f"\n[green]🎉 {success_count}/{len(services)} services started successfully[/green]") + +def stop_services(services): + """Stop specified services""" + console.print(f"🛑 [bold]Stopping {len(services)} services...[/bold]") + + success_count = 0 + for service_name in services: + if service_name not in SERVICES: + console.print(f"[red]❌ Unknown service: {service_name}[/red]") + continue + + console.print(f"\n🔧 Stopping {service_name}...") + if run_compose_command(service_name, 'down'): + console.print(f"[green]✅ {service_name} stopped[/green]") + success_count += 1 + else: + console.print(f"[red]❌ Failed to stop {service_name}[/red]") + + console.print(f"\n[green]🎉 {success_count}/{len(services)} services stopped successfully[/green]") + +def show_status(): + """Show status of all services""" + console.print("📊 [bold]Service Status:[/bold]\n") + + table = Table() + table.add_column("Service", style="cyan") + table.add_column("Configured", justify="center") + table.add_column("Description", style="dim") + table.add_column("Ports", style="green") + + for service_name, service_info in SERVICES.items(): + configured = "✅" if check_service_configured(service_name) else "❌" + ports = ", ".join(service_info['ports']) + table.add_row( + service_name, + configured, + service_info['description'], + ports + ) + + console.print(table) + + console.print("\n💡 [dim]Use 'python services.py start --all' to start all configured services[/dim]") + +def main(): + parser = argparse.ArgumentParser(description="Friend-Lite Service Management") + subparsers = parser.add_subparsers(dest='command', help='Available commands') + + # Start command + start_parser = subparsers.add_parser('start', help='Start services') + start_parser.add_argument('services', nargs='*', choices=list(SERVICES.keys()), + help='Services to start') + start_parser.add_argument('--all', action='store_true', help='Start all configured services') + start_parser.add_argument('--build', action='store_true', help='Build images before starting') + + # Stop command + stop_parser = subparsers.add_parser('stop', help='Stop services') + stop_parser.add_argument('services', nargs='*', choices=list(SERVICES.keys()), + help='Services to stop') + stop_parser.add_argument('--all', action='store_true', help='Stop all services') + + # Status command + subparsers.add_parser('status', help='Show service status') + + args = parser.parse_args() + + if not args.command: + show_status() + return + + if args.command == 'status': + show_status() + + elif args.command == 'start': + if args.all: + services = [s for s in SERVICES.keys() if check_service_configured(s)] + elif args.services: + services = args.services + else: + console.print("[red]❌ No services specified. Use --all or specify service names.[/red]") + return + + start_services(services, args.build) + + elif args.command == 'stop': + if args.all: + services = list(SERVICES.keys()) + elif args.services: + services = args.services + else: + console.print("[red]❌ No services specified. Use --all or specify service names.[/red]") + return + + stop_services(services) + +if __name__ == "__main__": + main() \ No newline at end of file From 7649948029312ab372550bf2d9b3e255f799ef3b Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Mon, 1 Sep 2025 13:19:13 +0000 Subject: [PATCH 12/35] uh whaaa --- Docs/init-system.md | 10 +++++----- README.md | 9 ++++----- init.py | 6 +++--- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/Docs/init-system.md b/Docs/init-system.md index 0e66ea884..c56781b68 100644 --- a/Docs/init-system.md +++ b/Docs/init-system.md @@ -129,19 +129,19 @@ Use the `services.py` script for all service operations: ```bash # Start all configured services -python services.py start --all --build +uv run --with-requirements setup-requirements.txt python services.py start --all --build # Start specific services -python services.py start backend speaker-recognition +uv run --with-requirements setup-requirements.txt python services.py start backend speaker-recognition # Check service status -python services.py status +uv run --with-requirements setup-requirements.txt python services.py status # Stop all services -python services.py stop --all +uv run --with-requirements setup-requirements.txt python services.py stop --all # Stop specific services -python services.py stop asr-services openmemory-mcp +uv run --with-requirements setup-requirements.txt python services.py stop asr-services openmemory-mcp ``` ### Manual Service Management diff --git a/README.md b/README.md index 5eff9a676..29d526f63 100644 --- a/README.md +++ b/README.md @@ -241,10 +241,9 @@ Backends and ASR services use standardized audio streaming: ### Single Machine (Recommended for beginners) 1. **Clone the repository** -2. **Use Advanced Backend**: `cd backends/advanced-backend` -3. **Configure .env**: Copy `.env.template` to `.env` and set required values -4. **Start services**: `docker compose up --build -d` -5. **Access WebUI**: `http://localhost:3000` (React) or `http://localhost:8501` (Streamlit) +2. **Run interactive setup**: `uv run --with-requirements setup-requirements.txt python init.py` +3. **Start all services**: `python services.py start --all --build` +4. **Access WebUI**: `http://localhost:5173` for the React web dashboard ### Distributed Setup (Advanced users with multiple machines) 1. **GPU Machine**: Deploy LLM services (Ollama, ASR, Speaker Recognition) @@ -261,7 +260,7 @@ Backends and ASR services use standardized audio streaming: 2. **Backend Machine**: Deploy lightweight services ```bash - cd backends/advanced-backend + cd backends/advanced # Configure distributed services in .env OLLAMA_BASE_URL=http://[gpu-machine-tailscale-ip]:11434 diff --git a/init.py b/init.py index 681b69ea2..9d1b07b4c 100755 --- a/init.py +++ b/init.py @@ -205,15 +205,15 @@ def main(): if configured_services: service_list = " ".join(configured_services) - console.print(f" [cyan]python services.py start {service_list}[/cyan]") + console.print(f" [cyan]uv run --with-requirements setup-requirements.txt python services.py start {service_list}[/cyan]") console.print("") console.print("3. Check service status:") - console.print(" [cyan]python services.py status[/cyan]") + console.print(" [cyan]uv run --with-requirements setup-requirements.txt python services.py status[/cyan]") console.print("") console.print("4. Stop services when done:") - console.print(" [cyan]python services.py stop --all[/cyan]") + console.print(" [cyan]uv run --with-requirements setup-requirements.txt python services.py stop --all[/cyan]") console.print(f"\n🚀 [bold]Enjoy Friend-Lite![/bold]") From 489932c0d2e76824935574887711e5ba4ab8aa9c Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Mon, 1 Sep 2025 13:45:29 +0000 Subject: [PATCH 13/35] huh losing it now --- Docs/ports-and-access.md | 138 +++++++++++++++++++++++ backends/advanced/init.py | 65 ++++++++++- extras/speaker-recognition/.env.template | 2 +- extras/speaker-recognition/setup.sh | 68 ++++++++++- services.py | 22 +++- 5 files changed, 278 insertions(+), 17 deletions(-) create mode 100644 Docs/ports-and-access.md diff --git a/Docs/ports-and-access.md b/Docs/ports-and-access.md new file mode 100644 index 000000000..d9b51cf35 --- /dev/null +++ b/Docs/ports-and-access.md @@ -0,0 +1,138 @@ +# Friend-Lite Port Configuration & User Journey + +## User Journey: Git Clone to Running Services + +### 1. Clone & Setup +```bash +git clone +cd friend-lite + +# Configure all services +uv run --with-requirements setup-requirements.txt python init.py + +# Start all configured services +uv run --with-requirements setup-requirements.txt python services.py start --all --build +``` + +### 2. Service Access Points + +## HTTP Mode (Default - No SSL Required) + +| Service | API Port | Web UI Port | Access URL | +|---------|----------|-------------|------------| +| **Advanced Backend** | 8000 | 5173 | http://localhost:8000 (API)
http://localhost:5173 (Dashboard) | +| **Speaker Recognition** | 8085 | 5174 | http://localhost:8085 (API)
http://localhost:5174 (WebUI) | +| **Parakeet ASR** | 8767 | - | http://localhost:8767 (API) | +| **OpenMemory MCP** | 8765 | 8765 | http://localhost:8765 (API + WebUI) | + +**🌐 Main Dashboard**: http://localhost:5173 +**🎤 Speaker Recognition**: http://localhost:5174 +**❌ No microphone access** - browsers require HTTPS for microphone + +--- + +## HTTPS Mode (For Microphone Access) + +| Service | HTTP Port | HTTPS Port | Access URL | Microphone Access | +|---------|-----------|------------|------------|-------------------| +| **Advanced Backend** | 80→443 | 443 | https://localhost/ (Main)
https://localhost/api/ (API) | ✅ Yes | +| **Speaker Recognition** | 8081→8444 | 8444 | https://localhost:8444/ (Main)
https://localhost:8444/api/ (API) | ✅ Yes | + +**🌐 Main Dashboard**: https://localhost/ (Advanced Backend with SSL) +**🎤 Speaker Recognition**: https://localhost:8444/ (Speaker Recognition with SSL) +**✅ Full microphone access** - both services secured with SSL + +### Port Details (HTTPS Mode) +- **Advanced Backend nginx**: Ports 80 (HTTP redirect) + 443 (HTTPS) +- **Speaker Recognition nginx**: Ports 8081 (HTTP redirect) + 8444 (HTTPS) +- **No port conflicts** - different port ranges for each service + +--- + +## Why Two Modes? + +### HTTP Mode (Default) +✅ **Simple setup** - No SSL certificates needed +✅ **Development friendly** - Quick start for testing +❌ **No microphone access** - Browsers require HTTPS for microphone + +### HTTPS Mode (Advanced) +✅ **Microphone access** - Browsers allow mic access over HTTPS +✅ **Production ready** - Secure for real deployments +❌ **Complex setup** - Requires SSL certificate generation + +--- + +## Configuration Files + +### Speaker Recognition Modes + +The speaker recognition service supports both modes via configuration: + +**HTTP Mode (.env)**: +```bash +REACT_UI_PORT=5174 +REACT_UI_HTTPS=false +``` + +**HTTPS Mode (.env)**: +```bash +REACT_UI_PORT=5175 # Internal HTTPS port +REACT_UI_HTTPS=true +# nginx provides external access on ports 8081 (HTTP) and 8444 (HTTPS) +``` + +--- + +## Service Management Commands + +```bash +# Check what's running +uv run --with-requirements setup-requirements.txt python services.py status + +# Start all services +uv run --with-requirements setup-requirements.txt python services.py start --all --build + +# Start only specific services +uv run --with-requirements setup-requirements.txt python services.py start backend speaker-recognition + +# Stop all services +uv run --with-requirements setup-requirements.txt python services.py stop --all +``` + +--- + +## Microphone Access Requirements + +For **speaker recognition** and **live audio features** to work: + +1. **Local development**: Use HTTP mode, access via `http://localhost:5174` + - Some browsers allow localhost microphone access over HTTP + +2. **Production/Remote access**: Use HTTPS mode, access via `https://localhost:8444` + - All browsers require HTTPS for microphone access over network + +3. **Mixed setup**: Keep backend on HTTP, only enable HTTPS for speaker recognition when needed + +--- + +## Port Conflict Resolution + +If you encounter port conflicts: + +1. **Check running services**: `uv run --with-requirements setup-requirements.txt python services.py status` +2. **Stop conflicting services**: `uv run --with-requirements setup-requirements.txt python services.py stop --all` +3. **Change ports in .env files** if needed +4. **Restart services**: `uv run --with-requirements setup-requirements.txt python services.py start --all` + +--- + +## Summary: Default User Experience + +After `git clone` and running init + services: + +🌐 **Main Application**: http://localhost:5173 +🎤 **Speaker Recognition**: http://localhost:5174 (HTTP) or https://localhost:8444 (HTTPS) +🔧 **Backend API**: http://localhost:8000 +📝 **ASR Service**: http://localhost:8767 +🧠 **Memory Service**: http://localhost:8765 \ No newline at end of file diff --git a/backends/advanced/init.py b/backends/advanced/init.py index a2ccc80de..3e07f0cf4 100644 --- a/backends/advanced/init.py +++ b/backends/advanced/init.py @@ -4,21 +4,22 @@ Interactive configuration for all services and API keys """ -import os -import sys +import argparse import getpass +import os import secrets import shutil -import argparse +import subprocess +import sys from datetime import datetime from pathlib import Path -from typing import Dict, Optional, Any +from typing import Any, Dict, Optional +from rich import print as rprint from rich.console import Console -from rich.prompt import Prompt, Confirm from rich.panel import Panel +from rich.prompt import Confirm, Prompt from rich.text import Text -from rich import print as rprint class FriendLiteSetup: @@ -272,6 +273,57 @@ def setup_network(self): self.config["BACKEND_PUBLIC_PORT"] = self.prompt_value("Backend port", "8000") self.config["WEBUI_PORT"] = self.prompt_value("Web UI port", "5173") + def setup_https(self): + """Configure HTTPS settings for microphone access""" + self.print_section("HTTPS Configuration (Optional)") + + try: + enable_https = Confirm.ask("Enable HTTPS for microphone access?", default=False) + except EOFError: + self.console.print("Using default: No") + enable_https = False + + if enable_https: + self.console.print("[blue][INFO][/blue] HTTPS enables microphone access in browsers") + server_ip = self.prompt_value("Server IP/Domain for SSL certificate", "localhost") + + # Generate SSL certificates + self.console.print("[blue][INFO][/blue] Generating SSL certificates...") + ssl_script = Path("ssl/generate-ssl.sh") + if ssl_script.exists(): + try: + subprocess.run([str(ssl_script), server_ip], check=True, cwd=".") + self.console.print("[green][SUCCESS][/green] SSL certificates generated") + except subprocess.CalledProcessError: + self.console.print("[yellow][WARNING][/yellow] SSL certificate generation failed") + else: + self.console.print("[yellow][WARNING][/yellow] ssl/generate-ssl.sh not found") + + # Generate nginx.conf from template + self.console.print("[blue][INFO][/blue] Creating nginx configuration...") + nginx_template = Path("nginx.conf.template") + if nginx_template.exists(): + try: + with open(nginx_template, 'r') as f: + nginx_content = f.read() + + # Replace TAILSCALE_IP with server_ip + nginx_content = nginx_content.replace('TAILSCALE_IP', server_ip) + + with open('nginx.conf', 'w') as f: + f.write(nginx_content) + + self.console.print(f"[green][SUCCESS][/green] nginx.conf created for: {server_ip}") + self.config["HTTPS_ENABLED"] = "true" + self.config["SERVER_IP"] = server_ip + + except Exception as e: + self.console.print(f"[yellow][WARNING][/yellow] nginx.conf generation failed: {e}") + else: + self.console.print("[yellow][WARNING][/yellow] nginx.conf.template not found") + else: + self.config["HTTPS_ENABLED"] = "false" + def generate_env_file(self): """Generate the .env file from configuration""" env_content = f"""# ============================================================================= @@ -398,6 +450,7 @@ def run(self): self.setup_memory() self.setup_optional_services() self.setup_network() + self.setup_https() # Generate files self.print_header("Configuration Complete!") diff --git a/extras/speaker-recognition/.env.template b/extras/speaker-recognition/.env.template index 71ec5cc26..ed93c9f35 100644 --- a/extras/speaker-recognition/.env.template +++ b/extras/speaker-recognition/.env.template @@ -25,7 +25,7 @@ SPEAKER_SERVICE_URL=http://speaker-service:8085 # React Web UI Configuration REACT_UI_HOST=0.0.0.0 -REACT_UI_PORT=5173 +REACT_UI_PORT=5174 REACT_UI_HTTPS=false # Optional: External Services diff --git a/extras/speaker-recognition/setup.sh b/extras/speaker-recognition/setup.sh index ede37cd5d..b8e4c22cf 100755 --- a/extras/speaker-recognition/setup.sh +++ b/extras/speaker-recognition/setup.sh @@ -26,12 +26,70 @@ echo " gpu - GPU acceleration (requires NVIDIA+CUDA)" read -p "Compute Mode [cpu/gpu]: " COMPUTE_MODE COMPUTE_MODE=${COMPUTE_MODE:-cpu} +echo "" +echo "🔒 HTTPS Configuration (required for microphone access)" +echo " http - Simple HTTP mode (development, localhost only)" +echo " https - HTTPS mode with SSL (production, remote access, microphone access)" +read -p "Mode [http/https]: " HTTPS_MODE +HTTPS_MODE=${HTTPS_MODE:-http} + # Update .env file sed -i "s|HF_TOKEN=.*|HF_TOKEN=$HF_TOKEN|" .env sed -i "s|COMPUTE_MODE=.*|COMPUTE_MODE=$COMPUTE_MODE|" .env -echo "" -echo "✅ Speaker Recognition configured!" -echo "📁 Configuration saved to .env" -echo "" -echo "🚀 To start: docker compose up --build -d" \ No newline at end of file +if [ "$HTTPS_MODE" = "https" ]; then + # HTTPS Configuration + echo "" + echo "🌐 Server Configuration for HTTPS" + echo "Enter your server IP/domain for SSL certificate" + echo "Examples: localhost, 192.168.1.100, your-domain.com" + read -p "Server IP/Domain (localhost): " SERVER_IP + SERVER_IP=${SERVER_IP:-localhost} + + # Update .env for HTTPS mode + sed -i "s|REACT_UI_HTTPS=.*|REACT_UI_HTTPS=true|" .env + sed -i "s|REACT_UI_PORT=.*|REACT_UI_PORT=5175|" .env + + # Generate SSL certificates + echo "" + echo "📄 Generating SSL certificates..." + if [ -f "ssl/generate-ssl.sh" ]; then + ./ssl/generate-ssl.sh "$SERVER_IP" + echo "✅ SSL certificates generated" + else + echo "⚠️ ssl/generate-ssl.sh not found, SSL setup skipped" + fi + + # Create nginx.conf from template + echo "📄 Creating nginx configuration..." + if [ -f "nginx.conf.template" ]; then + sed "s/TAILSCALE_IP/$SERVER_IP/g" nginx.conf.template > nginx.conf + echo "✅ nginx.conf created for: $SERVER_IP" + else + echo "⚠️ nginx.conf.template not found, nginx config skipped" + fi + + echo "" + echo "✅ Speaker Recognition configured (HTTPS mode)!" + echo "📁 Configuration saved to .env" + echo "🔒 HTTPS configured for: $SERVER_IP" + echo "" + echo "🚀 To start: docker compose up --build -d" + echo "🌐 HTTPS Access: https://localhost:8444/" + echo "🌐 HTTP Redirect: http://localhost:8081/ → HTTPS" + echo "📱 Service API: https://localhost:8444/api/" + echo "💡 Accept SSL certificate in browser" +else + # HTTP Configuration + sed -i "s|REACT_UI_HTTPS=.*|REACT_UI_HTTPS=false|" .env + sed -i "s|REACT_UI_PORT=.*|REACT_UI_PORT=5174|" .env + + echo "" + echo "✅ Speaker Recognition configured (HTTP mode)!" + echo "📁 Configuration saved to .env" + echo "" + echo "🚀 To start: docker compose up --build -d speaker-service web-ui" + echo "📱 Service API: http://localhost:8085" + echo "📱 Web Interface: http://localhost:5174" + echo "⚠️ Note: Microphone access may not work over HTTP on remote connections" +fi \ No newline at end of file diff --git a/services.py b/services.py index af887b795..d041be2fe 100755 --- a/services.py +++ b/services.py @@ -25,7 +25,7 @@ 'path': 'extras/speaker-recognition', 'compose_file': 'docker-compose.yml', 'description': 'Speaker Recognition Service', - 'ports': ['8085', '5173'] + 'ports': ['8085', '5174/8444'] }, 'asr-services': { 'path': 'extras/asr-services', @@ -169,15 +169,15 @@ def main(): # Start command start_parser = subparsers.add_parser('start', help='Start services') - start_parser.add_argument('services', nargs='*', choices=list(SERVICES.keys()), - help='Services to start') + start_parser.add_argument('services', nargs='*', + help='Services to start: backend, speaker-recognition, asr-services, openmemory-mcp (or use --all)') start_parser.add_argument('--all', action='store_true', help='Start all configured services') start_parser.add_argument('--build', action='store_true', help='Build images before starting') # Stop command stop_parser = subparsers.add_parser('stop', help='Stop services') - stop_parser.add_argument('services', nargs='*', choices=list(SERVICES.keys()), - help='Services to stop') + stop_parser.add_argument('services', nargs='*', + help='Services to stop: backend, speaker-recognition, asr-services, openmemory-mcp (or use --all)') stop_parser.add_argument('--all', action='store_true', help='Stop all services') # Status command @@ -196,6 +196,12 @@ def main(): if args.all: services = [s for s in SERVICES.keys() if check_service_configured(s)] elif args.services: + # Validate service names + invalid_services = [s for s in args.services if s not in SERVICES] + if invalid_services: + console.print(f"[red]❌ Invalid service names: {', '.join(invalid_services)}[/red]") + console.print(f"Available services: {', '.join(SERVICES.keys())}") + return services = args.services else: console.print("[red]❌ No services specified. Use --all or specify service names.[/red]") @@ -207,6 +213,12 @@ def main(): if args.all: services = list(SERVICES.keys()) elif args.services: + # Validate service names + invalid_services = [s for s in args.services if s not in SERVICES] + if invalid_services: + console.print(f"[red]❌ Invalid service names: {', '.join(invalid_services)}[/red]") + console.print(f"Available services: {', '.join(SERVICES.keys())}") + return services = args.services else: console.print("[red]❌ No services specified. Use --all or specify service names.[/red]") From 641e3042c2c5ac8ce04c7e1f036ca78d43b04650 Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Mon, 1 Sep 2025 13:49:37 +0000 Subject: [PATCH 14/35] fix --- backends/advanced/init.py | 4 +++- extras/speaker-recognition/setup.sh | 28 ++++++++++++++++++++-------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/backends/advanced/init.py b/backends/advanced/init.py index 3e07f0cf4..7c3584e2f 100644 --- a/backends/advanced/init.py +++ b/backends/advanced/init.py @@ -285,7 +285,9 @@ def setup_https(self): if enable_https: self.console.print("[blue][INFO][/blue] HTTPS enables microphone access in browsers") - server_ip = self.prompt_value("Server IP/Domain for SSL certificate", "localhost") + self.console.print("[blue][INFO][/blue] For distributed deployments, use your Tailscale IP (e.g., 100.64.1.2)") + self.console.print("[blue][INFO][/blue] For local-only access, use 'localhost'") + server_ip = self.prompt_value("Server IP/Domain for SSL certificate (Tailscale IP or localhost)", "localhost") # Generate SSL certificates self.console.print("[blue][INFO][/blue] Generating SSL certificates...") diff --git a/extras/speaker-recognition/setup.sh b/extras/speaker-recognition/setup.sh index b8e4c22cf..c2212b323 100755 --- a/extras/speaker-recognition/setup.sh +++ b/extras/speaker-recognition/setup.sh @@ -21,17 +21,29 @@ read -p "HF Token: " HF_TOKEN echo "" echo "🖥️ Compute Mode" -echo " cpu - CPU-only (works everywhere)" -echo " gpu - GPU acceleration (requires NVIDIA+CUDA)" -read -p "Compute Mode [cpu/gpu]: " COMPUTE_MODE -COMPUTE_MODE=${COMPUTE_MODE:-cpu} +echo " 1) CPU-only (works everywhere)" +echo " 2) GPU acceleration (requires NVIDIA+CUDA)" +read -p "Enter choice [1-2] (1): " COMPUTE_CHOICE +COMPUTE_CHOICE=${COMPUTE_CHOICE:-1} + +if [ "$COMPUTE_CHOICE" = "2" ]; then + COMPUTE_MODE="gpu" +else + COMPUTE_MODE="cpu" +fi echo "" echo "🔒 HTTPS Configuration (required for microphone access)" -echo " http - Simple HTTP mode (development, localhost only)" -echo " https - HTTPS mode with SSL (production, remote access, microphone access)" -read -p "Mode [http/https]: " HTTPS_MODE -HTTPS_MODE=${HTTPS_MODE:-http} +echo " 1) HTTP mode (development, localhost only)" +echo " 2) HTTPS mode with SSL (production, remote access, microphone access)" +read -p "Enter choice [1-2] (1): " HTTPS_CHOICE +HTTPS_CHOICE=${HTTPS_CHOICE:-1} + +if [ "$HTTPS_CHOICE" = "2" ]; then + HTTPS_MODE="https" +else + HTTPS_MODE="http" +fi # Update .env file sed -i "s|HF_TOKEN=.*|HF_TOKEN=$HF_TOKEN|" .env From e783d0151b2559e2d0165f4de713d6f2a9e18f6c Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Mon, 1 Sep 2025 13:58:46 +0000 Subject: [PATCH 15/35] fix --- backends/advanced/init.py | 40 ++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/backends/advanced/init.py b/backends/advanced/init.py index 7c3584e2f..50ed45b82 100644 --- a/backends/advanced/init.py +++ b/backends/advanced/init.py @@ -269,7 +269,6 @@ def setup_network(self): """Configure network settings""" self.print_section("Network Configuration") - self.config["HOST_IP"] = self.prompt_value("Host IP for services", "localhost") self.config["BACKEND_PUBLIC_PORT"] = self.prompt_value("Backend port", "8000") self.config["WEBUI_PORT"] = self.prompt_value("Web UI port", "5173") @@ -364,7 +363,6 @@ def generate_env_file(self): PARAKEET_ASR_URL={self.config.get('PARAKEET_ASR_URL', '')} # Network Configuration -HOST_IP={self.config.get('HOST_IP', 'localhost')} BACKEND_PUBLIC_PORT={self.config.get('BACKEND_PUBLIC_PORT', '8000')} WEBUI_PORT={self.config.get('WEBUI_PORT', '5173')} @@ -403,26 +401,42 @@ def show_summary(self): self.console.print(f"✅ Transcription: {self.config.get('TRANSCRIPTION_PROVIDER', 'Not configured')}") self.console.print(f"✅ LLM Provider: {self.config.get('LLM_PROVIDER', 'Not configured')}") self.console.print(f"✅ Memory Provider: {self.config.get('MEMORY_PROVIDER', 'friend_lite')}") - self.console.print(f"✅ Backend URL: http://{self.config.get('HOST_IP', 'localhost')}:{self.config.get('BACKEND_PUBLIC_PORT', '8000')}") - self.console.print(f"✅ Dashboard URL: http://{self.config.get('HOST_IP', 'localhost')}:{self.config.get('WEBUI_PORT', '5173')}") + # Auto-determine URLs based on HTTPS configuration + if self.config.get('HTTPS_ENABLED') == 'true': + server_ip = self.config.get('SERVER_IP', 'localhost') + self.console.print(f"✅ Backend URL: https://{server_ip}/") + self.console.print(f"✅ Dashboard URL: https://{server_ip}/") + else: + backend_port = self.config.get('BACKEND_PUBLIC_PORT', '8000') + webui_port = self.config.get('WEBUI_PORT', '5173') + self.console.print(f"✅ Backend URL: http://localhost:{backend_port}") + self.console.print(f"✅ Dashboard URL: http://localhost:{webui_port}") def show_next_steps(self): """Show next steps""" self.print_section("Next Steps") self.console.print() - host_ip = self.config.get('HOST_IP', 'localhost') - webui_port = self.config.get('WEBUI_PORT', '5173') - backend_port = self.config.get('BACKEND_PUBLIC_PORT', '8000') - self.console.print("1. Start the main services:") self.console.print(" [cyan]docker compose up --build -d[/cyan]") self.console.print() - self.console.print("2. Access the dashboard:") - self.console.print(f" [cyan]http://{host_ip}:{webui_port}[/cyan]") - self.console.print() - self.console.print("3. Check service health:") - self.console.print(f" [cyan]curl http://{host_ip}:{backend_port}/health[/cyan]") + + # Auto-determine URLs for next steps + if self.config.get('HTTPS_ENABLED') == 'true': + server_ip = self.config.get('SERVER_IP', 'localhost') + self.console.print("2. Access the dashboard:") + self.console.print(f" [cyan]https://{server_ip}/[/cyan]") + self.console.print() + self.console.print("3. Check service health:") + self.console.print(f" [cyan]curl -k https://{server_ip}/health[/cyan]") + else: + webui_port = self.config.get('WEBUI_PORT', '5173') + backend_port = self.config.get('BACKEND_PUBLIC_PORT', '8000') + self.console.print("2. Access the dashboard:") + self.console.print(f" [cyan]http://localhost:{webui_port}[/cyan]") + self.console.print() + self.console.print("3. Check service health:") + self.console.print(f" [cyan]curl http://localhost:{backend_port}/health[/cyan]") if self.config.get("MEMORY_PROVIDER") == "openmemory_mcp": self.console.print() From cb5920d2833ef7beda33395c79ba3b33051716a8 Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Mon, 1 Sep 2025 20:12:57 +0000 Subject: [PATCH 16/35] tiny fix --- init.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/init.py b/init.py index 9d1b07b4c..58d4986d2 100755 --- a/init.py +++ b/init.py @@ -92,6 +92,27 @@ def select_services(): return selected +def cleanup_unselected_services(selected_services): + """Backup and remove .env files from services that weren't selected""" + from datetime import datetime + + all_services = list(SERVICES['backend'].keys()) + list(SERVICES['extras'].keys()) + + for service_name in all_services: + if service_name not in selected_services: + if service_name == 'advanced': + service_path = Path(SERVICES['backend'][service_name]['path']) + else: + service_path = Path(SERVICES['extras'][service_name]['path']) + + env_file = service_path / '.env' + if env_file.exists(): + # Create backup with timestamp + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + backup_file = service_path / f'.env.backup.{timestamp}.unselected' + env_file.rename(backup_file) + console.print(f"🧹 [dim]Backed up {service_name} configuration to {backup_file.name} (service not selected)[/dim]") + def run_service_setup(service_name, selected_services): """Execute individual service setup script""" if service_name == 'advanced': @@ -168,6 +189,9 @@ def main(): # Pure Delegation - Run Each Service Setup console.print(f"\n📋 [bold]Setting up {len(selected_services)} services...[/bold]") + # Clean up .env files from unselected services (creates backups) + cleanup_unselected_services(selected_services) + success_count = 0 failed_services = [] From 165d2f155505791030b2919d415fec1777248309 Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Mon, 1 Sep 2025 20:23:30 +0000 Subject: [PATCH 17/35] fix: fix type error in services.py --- services.py | 57 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/services.py b/services.py index d041be2fe..03de70712 100755 --- a/services.py +++ b/services.py @@ -79,19 +79,52 @@ def run_compose_command(service_name, command, build=False): cmd.extend(['ps']) try: - result = subprocess.run( - cmd, - cwd=service_path, - capture_output=True, - text=True, - check=False - ) - - if result.returncode == 0: - return True + # For commands that need real-time output (build), stream to console + if build and command == 'up': + console.print(f"[dim]Building {service_name} containers...[/dim]") + process = subprocess.Popen( + cmd, + cwd=service_path, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1 + ) + + # Stream output line by line + assert process.stdout is not None + for line in process.stdout: + line = line.rstrip() + # Add some basic formatting for Docker output + if 'Building' in line or 'Creating' in line: + console.print(f" [cyan]{line}[/cyan]") + elif 'error' in line.lower() or 'failed' in line.lower(): + console.print(f" [red]{line}[/red]") + elif 'warning' in line.lower(): + console.print(f" [yellow]{line}[/yellow]") + elif 'Successfully' in line or 'Started' in line: + console.print(f" [green]{line}[/green]") + else: + console.print(f" [dim]{line}[/dim]") + + # Wait for process to complete + process.wait() + return process.returncode == 0 else: - console.print(f"[red]❌ Command failed: {result.stderr}[/red]") - return False + # For non-build commands, run silently unless there's an error + result = subprocess.run( + cmd, + cwd=service_path, + capture_output=True, + text=True, + check=False + ) + + if result.returncode == 0: + return True + else: + console.print(f"[red]❌ Command failed: {result.stderr}[/red]") + return False except Exception as e: console.print(f"[red]❌ Error running command: {e}[/red]") From 8904def3ef52accdada29d99d2967f359b19af0f Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Mon, 1 Sep 2025 20:29:06 +0000 Subject: [PATCH 18/35] fix services script --- services.py | 70 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 16 deletions(-) diff --git a/services.py b/services.py index 03de70712..846211015 100755 --- a/services.py +++ b/services.py @@ -4,13 +4,14 @@ Start, stop, and manage configured services """ +import argparse import subprocess import sys -import argparse from pathlib import Path + +from rich import print as rprint from rich.console import Console from rich.table import Table -from rich import print as rprint console = Console() @@ -91,25 +92,57 @@ def run_compose_command(service_name, command, build=False): bufsize=1 ) - # Stream output line by line + # Collect all output for potential error display + all_output = [] + line_count = 0 + max_display = 20 + assert process.stdout is not None for line in process.stdout: line = line.rstrip() - # Add some basic formatting for Docker output - if 'Building' in line or 'Creating' in line: - console.print(f" [cyan]{line}[/cyan]") - elif 'error' in line.lower() or 'failed' in line.lower(): - console.print(f" [red]{line}[/red]") - elif 'warning' in line.lower(): - console.print(f" [yellow]{line}[/yellow]") - elif 'Successfully' in line or 'Started' in line: - console.print(f" [green]{line}[/green]") - else: - console.print(f" [dim]{line}[/dim]") + if not line: + continue + + # Store all output + all_output.append(line) + line_count += 1 + + # Show only last 20 lines worth of output + if line_count <= max_display: + # Just print the line with simple coloring + if 'error' in line.lower() or 'failed' in line.lower(): + console.print(f" [red]{line}[/red]") + elif 'Successfully' in line or 'Started' in line or 'Created' in line: + console.print(f" [green]{line}[/green]") + elif 'Building' in line or 'Creating' in line: + console.print(f" [cyan]{line}[/cyan]") + else: + console.print(f" [dim]{line}[/dim]") # Wait for process to complete process.wait() - return process.returncode == 0 + + # If we had more than 20 lines, show the last few + if line_count > max_display: + console.print(f" [dim]... ({line_count - max_display} lines omitted) ...[/dim]") + for line in all_output[-5:]: # Show last 5 lines if we truncated + if 'error' in line.lower() or 'failed' in line.lower(): + console.print(f" [red]{line}[/red]") + elif 'Successfully' in line or 'Started' in line or 'Created' in line: + console.print(f" [green]{line}[/green]") + else: + console.print(f" [dim]{line}[/dim]") + + # If build failed, show more context + if process.returncode != 0: + console.print(f"\n[red]❌ Build failed for {service_name}[/red]") + if len(all_output) > max_display: + console.print("[red]Full error context (last 30 lines):[/red]") + for line in all_output[-30:]: + console.print(f" [dim]{line}[/dim]") + return False + + return True else: # For non-build commands, run silently unless there's an error result = subprocess.run( @@ -123,7 +156,12 @@ def run_compose_command(service_name, command, build=False): if result.returncode == 0: return True else: - console.print(f"[red]❌ Command failed: {result.stderr}[/red]") + console.print(f"[red]❌ Command failed[/red]") + if result.stderr: + console.print("[red]Error output:[/red]") + # Show error output line by line for better readability + for line in result.stderr.splitlines()[-20:]: # Last 20 lines + console.print(f" [dim]{line}[/dim]") return False except Exception as e: From 33f5c1737ea5f33fc37f3ab57f0406146b8659a1 Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Mon, 1 Sep 2025 20:37:29 +0000 Subject: [PATCH 19/35] update --- services.py | 49 ++++++++++++++++++------------------------------- 1 file changed, 18 insertions(+), 31 deletions(-) diff --git a/services.py b/services.py index 846211015..a0d823873 100755 --- a/services.py +++ b/services.py @@ -92,10 +92,8 @@ def run_compose_command(service_name, command, build=False): bufsize=1 ) - # Collect all output for potential error display + # Simply stream all output with coloring all_output = [] - line_count = 0 - max_display = 20 assert process.stdout is not None for line in process.stdout: @@ -103,42 +101,31 @@ def run_compose_command(service_name, command, build=False): if not line: continue - # Store all output + # Store for error context all_output.append(line) - line_count += 1 - # Show only last 20 lines worth of output - if line_count <= max_display: - # Just print the line with simple coloring - if 'error' in line.lower() or 'failed' in line.lower(): - console.print(f" [red]{line}[/red]") - elif 'Successfully' in line or 'Started' in line or 'Created' in line: - console.print(f" [green]{line}[/green]") - elif 'Building' in line or 'Creating' in line: - console.print(f" [cyan]{line}[/cyan]") - else: - console.print(f" [dim]{line}[/dim]") + # Print with appropriate coloring + if 'error' in line.lower() or 'failed' in line.lower(): + console.print(f" [red]{line}[/red]") + elif 'Successfully' in line or 'Started' in line or 'Created' in line: + console.print(f" [green]{line}[/green]") + elif 'Building' in line or 'Creating' in line: + console.print(f" [cyan]{line}[/cyan]") + elif 'warning' in line.lower(): + console.print(f" [yellow]{line}[/yellow]") + else: + console.print(f" [dim]{line}[/dim]") # Wait for process to complete process.wait() - # If we had more than 20 lines, show the last few - if line_count > max_display: - console.print(f" [dim]... ({line_count - max_display} lines omitted) ...[/dim]") - for line in all_output[-5:]: # Show last 5 lines if we truncated - if 'error' in line.lower() or 'failed' in line.lower(): - console.print(f" [red]{line}[/red]") - elif 'Successfully' in line or 'Started' in line or 'Created' in line: - console.print(f" [green]{line}[/green]") - else: - console.print(f" [dim]{line}[/dim]") - - # If build failed, show more context + # If build failed, show error summary if process.returncode != 0: console.print(f"\n[red]❌ Build failed for {service_name}[/red]") - if len(all_output) > max_display: - console.print("[red]Full error context (last 30 lines):[/red]") - for line in all_output[-30:]: + # Show last few lines as context if we have a lot of output + if len(all_output) > 50: + console.print("[red]Last 20 lines of output:[/red]") + for line in all_output[-20:]: console.print(f" [dim]{line}[/dim]") return False From 16d30e0c76beea0fbaa5575ed2bb726696afd573 Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Mon, 1 Sep 2025 20:49:04 +0000 Subject: [PATCH 20/35] ugh --- extras/openmemory-mcp/setup.sh | 49 +++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/extras/openmemory-mcp/setup.sh b/extras/openmemory-mcp/setup.sh index 102c3c245..7f798cc55 100755 --- a/extras/openmemory-mcp/setup.sh +++ b/extras/openmemory-mcp/setup.sh @@ -4,20 +4,49 @@ set -e echo "🧠 OpenMemory MCP Setup" echo "======================" -# Create .env file if it doesn't exist -if [ ! -f ".env" ]; then - echo "📄 Creating .env file from template..." - cp .env.template .env - echo "✅ .env file created" -else - echo "ℹ️ .env file already exists, using existing configuration" +# Check if already configured +if [ -f ".env" ]; then + echo "⚠️ .env already exists. Backing up..." + cp .env .env.backup.$(date +%Y%m%d_%H%M%S) fi +# Start from template +cp .env.template .env + +# Clone the custom fork of mem0 with OpenMemory fixes +echo "" +echo "📦 Setting up custom mem0 fork with OpenMemory..." +if [ -d "cache/mem0" ]; then + echo " Removing existing mem0 directory..." + rm -rf cache/mem0 +fi + +echo " Cloning mem0 fork from AnkushMalaker/mem0..." +mkdir -p cache +git clone https://github.com/AnkushMalaker/mem0.git cache/mem0 +cd cache/mem0 +echo " Checking out fix/get-endpoint branch..." +git checkout fix/get-endpoint +cd ../.. + +echo "✅ Custom mem0 fork ready with OpenMemory improvements" + +# Prompt for OpenAI API key +echo "" +echo "🔑 OpenAI API Key (required for memory extraction)" +echo "Get yours from: https://platform.openai.com/api-keys" +read -p "OpenAI API Key: " OPENAI_API_KEY + +# Update .env file +sed -i "s|OPENAI_API_KEY=.*|OPENAI_API_KEY=$OPENAI_API_KEY|" .env + +echo "" echo "✅ OpenMemory MCP configured!" echo "📁 Configuration saved to .env" echo "" echo "🚀 To start: docker compose up --build -d" -echo " 🌐 Server: http://host.docker.internal:8765" -echo " 📱 Web UI: http://localhost:8765" +echo "🌐 MCP Server: http://localhost:8765" +echo "📱 Web Interface: http://localhost:8765" +echo "🔧 UI (optional): docker compose --profile ui up -d" echo "" -echo "💡 Only start if you selected openmemory_mcp as memory provider" \ No newline at end of file +echo "💡 Note: Using custom mem0 fork from AnkushMalaker/mem0:fix/get-endpoint" \ No newline at end of file From 7ce256b00f63dfb5a9e671106fed947d1f0d6325 Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Mon, 1 Sep 2025 20:53:53 +0000 Subject: [PATCH 21/35] fix --- services.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/services.py b/services.py index a0d823873..088bf95e3 100755 --- a/services.py +++ b/services.py @@ -122,11 +122,6 @@ def run_compose_command(service_name, command, build=False): # If build failed, show error summary if process.returncode != 0: console.print(f"\n[red]❌ Build failed for {service_name}[/red]") - # Show last few lines as context if we have a lot of output - if len(all_output) > 50: - console.print("[red]Last 20 lines of output:[/red]") - for line in all_output[-20:]: - console.print(f" [dim]{line}[/dim]") return False return True @@ -146,8 +141,8 @@ def run_compose_command(service_name, command, build=False): console.print(f"[red]❌ Command failed[/red]") if result.stderr: console.print("[red]Error output:[/red]") - # Show error output line by line for better readability - for line in result.stderr.splitlines()[-20:]: # Last 20 lines + # Show all error output + for line in result.stderr.splitlines(): console.print(f" [dim]{line}[/dim]") return False From 87c9931a961358c045297b5545a6103cf9e511c2 Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Mon, 1 Sep 2025 21:19:38 +0000 Subject: [PATCH 22/35] ssl gen --- backends/advanced/init.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/backends/advanced/init.py b/backends/advanced/init.py index 50ed45b82..7c13b0d9a 100644 --- a/backends/advanced/init.py +++ b/backends/advanced/init.py @@ -290,19 +290,22 @@ def setup_https(self): # Generate SSL certificates self.console.print("[blue][INFO][/blue] Generating SSL certificates...") - ssl_script = Path("ssl/generate-ssl.sh") + # Use path relative to this script's directory + script_dir = Path(__file__).parent + ssl_script = script_dir / "ssl" / "generate-ssl.sh" if ssl_script.exists(): try: - subprocess.run([str(ssl_script), server_ip], check=True, cwd=".") + # Run from the backend directory so paths work correctly + subprocess.run([str(ssl_script), server_ip], check=True, cwd=str(script_dir)) self.console.print("[green][SUCCESS][/green] SSL certificates generated") except subprocess.CalledProcessError: self.console.print("[yellow][WARNING][/yellow] SSL certificate generation failed") else: - self.console.print("[yellow][WARNING][/yellow] ssl/generate-ssl.sh not found") + self.console.print(f"[yellow][WARNING][/yellow] SSL script not found at {ssl_script}") # Generate nginx.conf from template self.console.print("[blue][INFO][/blue] Creating nginx configuration...") - nginx_template = Path("nginx.conf.template") + nginx_template = script_dir / "nginx.conf.template" if nginx_template.exists(): try: with open(nginx_template, 'r') as f: From 4c9d552fb16c2debd8df5ad0e530e2adc72ecf8d Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Mon, 1 Sep 2025 21:29:08 +0000 Subject: [PATCH 23/35] how many times can I do this before the universe stops me, before someone tells me, 'your commits dont make sense, and that, is what is wrong with the world'? --- backends/advanced/init.py | 35 ++++++++++----- extras/speaker-recognition/setup.sh | 66 ++++++++++++++++++++++------- init.py | 52 +++++++++++++++++++++-- 3 files changed, 122 insertions(+), 31 deletions(-) diff --git a/backends/advanced/init.py b/backends/advanced/init.py index 7c13b0d9a..c4f50382f 100644 --- a/backends/advanced/init.py +++ b/backends/advanced/init.py @@ -274,19 +274,28 @@ def setup_network(self): def setup_https(self): """Configure HTTPS settings for microphone access""" - self.print_section("HTTPS Configuration (Optional)") - - try: - enable_https = Confirm.ask("Enable HTTPS for microphone access?", default=False) - except EOFError: - self.console.print("Using default: No") - enable_https = False + # Check if HTTPS configuration provided via command line + if hasattr(self.args, 'enable_https') and self.args.enable_https: + enable_https = True + server_ip = getattr(self.args, 'server_ip', 'localhost') + self.console.print(f"[green][SUCCESS][/green] HTTPS configured via command line: {server_ip}") + else: + # Interactive configuration + self.print_section("HTTPS Configuration (Optional)") + + try: + enable_https = Confirm.ask("Enable HTTPS for microphone access?", default=False) + except EOFError: + self.console.print("Using default: No") + enable_https = False + + if enable_https: + self.console.print("[blue][INFO][/blue] HTTPS enables microphone access in browsers") + self.console.print("[blue][INFO][/blue] For distributed deployments, use your Tailscale IP (e.g., 100.64.1.2)") + self.console.print("[blue][INFO][/blue] For local-only access, use 'localhost'") + server_ip = self.prompt_value("Server IP/Domain for SSL certificate (Tailscale IP or localhost)", "localhost") if enable_https: - self.console.print("[blue][INFO][/blue] HTTPS enables microphone access in browsers") - self.console.print("[blue][INFO][/blue] For distributed deployments, use your Tailscale IP (e.g., 100.64.1.2)") - self.console.print("[blue][INFO][/blue] For local-only access, use 'localhost'") - server_ip = self.prompt_value("Server IP/Domain for SSL certificate (Tailscale IP or localhost)", "localhost") # Generate SSL certificates self.console.print("[blue][INFO][/blue] Generating SSL certificates...") @@ -504,6 +513,10 @@ def main(): help="Speaker Recognition service URL (default: prompt user)") parser.add_argument("--parakeet-asr-url", help="Parakeet ASR service URL (default: prompt user)") + parser.add_argument("--enable-https", action="store_true", + help="Enable HTTPS configuration (default: prompt user)") + parser.add_argument("--server-ip", + help="Server IP/domain for SSL certificate (default: prompt user)") args = parser.parse_args() diff --git a/extras/speaker-recognition/setup.sh b/extras/speaker-recognition/setup.sh index c2212b323..5a996eca4 100755 --- a/extras/speaker-recognition/setup.sh +++ b/extras/speaker-recognition/setup.sh @@ -1,6 +1,27 @@ #!/bin/bash set -e +# Parse command line arguments +ENABLE_HTTPS=false +SERVER_IP="" + +while [[ $# -gt 0 ]]; do + case $1 in + --enable-https) + ENABLE_HTTPS=true + shift + ;; + --server-ip) + SERVER_IP="$2" + shift 2 + ;; + *) + echo "Unknown argument: $1" + exit 1 + ;; + esac +done + echo "🗣️ Speaker Recognition Setup" echo "==============================" @@ -32,17 +53,28 @@ else COMPUTE_MODE="cpu" fi -echo "" -echo "🔒 HTTPS Configuration (required for microphone access)" -echo " 1) HTTP mode (development, localhost only)" -echo " 2) HTTPS mode with SSL (production, remote access, microphone access)" -read -p "Enter choice [1-2] (1): " HTTPS_CHOICE -HTTPS_CHOICE=${HTTPS_CHOICE:-1} - -if [ "$HTTPS_CHOICE" = "2" ]; then +# HTTPS Configuration +if [ "$ENABLE_HTTPS" = true ]; then + # Use command line arguments HTTPS_MODE="https" + if [ -z "$SERVER_IP" ]; then + SERVER_IP="localhost" + fi + echo "✅ HTTPS configured via command line: $SERVER_IP" else - HTTPS_MODE="http" + # Interactive configuration + echo "" + echo "🔒 HTTPS Configuration (required for microphone access)" + echo " 1) HTTP mode (development, localhost only)" + echo " 2) HTTPS mode with SSL (production, remote access, microphone access)" + read -p "Enter choice [1-2] (1): " HTTPS_CHOICE + HTTPS_CHOICE=${HTTPS_CHOICE:-1} + + if [ "$HTTPS_CHOICE" = "2" ]; then + HTTPS_MODE="https" + else + HTTPS_MODE="http" + fi fi # Update .env file @@ -50,13 +82,15 @@ sed -i "s|HF_TOKEN=.*|HF_TOKEN=$HF_TOKEN|" .env sed -i "s|COMPUTE_MODE=.*|COMPUTE_MODE=$COMPUTE_MODE|" .env if [ "$HTTPS_MODE" = "https" ]; then - # HTTPS Configuration - echo "" - echo "🌐 Server Configuration for HTTPS" - echo "Enter your server IP/domain for SSL certificate" - echo "Examples: localhost, 192.168.1.100, your-domain.com" - read -p "Server IP/Domain (localhost): " SERVER_IP - SERVER_IP=${SERVER_IP:-localhost} + # Get SERVER_IP if not already set (from command line) + if [ -z "$SERVER_IP" ]; then + echo "" + echo "🌐 Server Configuration for HTTPS" + echo "Enter your server IP/domain for SSL certificate" + echo "Examples: localhost, 192.168.1.100, your-domain.com" + read -p "Server IP/Domain (localhost): " SERVER_IP + SERVER_IP=${SERVER_IP:-localhost} + fi # Update .env for HTTPS mode sed -i "s|REACT_UI_HTTPS=.*|REACT_UI_HTTPS=true|" .env diff --git a/init.py b/init.py index 58d4986d2..f3d695bc5 100755 --- a/init.py +++ b/init.py @@ -113,20 +113,29 @@ def cleanup_unselected_services(selected_services): env_file.rename(backup_file) console.print(f"🧹 [dim]Backed up {service_name} configuration to {backup_file.name} (service not selected)[/dim]") -def run_service_setup(service_name, selected_services): +def run_service_setup(service_name, selected_services, https_enabled=False, server_ip=None): """Execute individual service setup script""" if service_name == 'advanced': service = SERVICES['backend'][service_name] - # For advanced backend, pass URLs of other selected services + # For advanced backend, pass URLs of other selected services and HTTPS config cmd = service['cmd'].copy() if 'speaker-recognition' in selected_services: cmd.extend(['--speaker-service-url', 'http://host.docker.internal:8085']) if 'asr-services' in selected_services: cmd.extend(['--parakeet-asr-url', 'http://host.docker.internal:8767']) + + # Add HTTPS configuration + if https_enabled and server_ip: + cmd.extend(['--enable-https', '--server-ip', server_ip]) + else: service = SERVICES['extras'][service_name] - cmd = service['cmd'] + cmd = service['cmd'].copy() + + # Add HTTPS configuration for services that support it + if service_name == 'speaker-recognition' and https_enabled and server_ip: + cmd.extend(['--enable-https', '--server-ip', server_ip]) console.print(f"\n🔧 [bold]Setting up {service_name}...[/bold]") @@ -186,6 +195,41 @@ def main(): console.print("\n[yellow]No services selected. Exiting.[/yellow]") return + # HTTPS Configuration (for services that need it) + https_enabled = False + server_ip = None + + # Check if we have services that benefit from HTTPS + https_services = {'advanced', 'speaker-recognition'} + needs_https = bool(https_services.intersection(selected_services)) + + if needs_https: + console.print("\n🔒 [bold cyan]HTTPS Configuration[/bold cyan]") + console.print("HTTPS enables microphone access in browsers and secure connections") + + try: + https_enabled = Confirm.ask("Enable HTTPS for selected services?", default=False) + except EOFError: + console.print("Using default: No") + https_enabled = False + + if https_enabled: + console.print("\n[blue][INFO][/blue] For distributed deployments, use your Tailscale IP") + console.print("[blue][INFO][/blue] For local-only access, use 'localhost'") + console.print("Examples: localhost, 100.64.1.2, your-domain.com") + + while True: + try: + server_ip = console.input("Server IP/Domain for SSL certificates [localhost]: ").strip() + if not server_ip: + server_ip = "localhost" + break + except EOFError: + server_ip = "localhost" + break + + console.print(f"[green]✅[/green] HTTPS configured for: {server_ip}") + # Pure Delegation - Run Each Service Setup console.print(f"\n📋 [bold]Setting up {len(selected_services)} services...[/bold]") @@ -196,7 +240,7 @@ def main(): failed_services = [] for service in selected_services: - if run_service_setup(service, selected_services): + if run_service_setup(service, selected_services, https_enabled, server_ip): success_count += 1 else: failed_services.append(service) From 4de7ec0e16fc7d9901e2bbff349a514f2baf62e6 Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Mon, 1 Sep 2025 21:39:44 +0000 Subject: [PATCH 24/35] hm --- backends/advanced/ssl/generate-ssl.sh | 81 +++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100755 backends/advanced/ssl/generate-ssl.sh diff --git a/backends/advanced/ssl/generate-ssl.sh b/backends/advanced/ssl/generate-ssl.sh new file mode 100755 index 000000000..fb83db5b9 --- /dev/null +++ b/backends/advanced/ssl/generate-ssl.sh @@ -0,0 +1,81 @@ +#!/bin/bash +set -e + +# Generate self-signed SSL certificate for Friend-Lite Advanced Backend +# Supports localhost and custom Tailscale IP + +TAILSCALE_IP="$1" + +if [ -z "$TAILSCALE_IP" ]; then + echo "Usage: $0 " + echo "Example: $0 100.83.66.30" + exit 1 +fi + +# Validate IP format +if ! echo "$TAILSCALE_IP" | grep -E '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$' > /dev/null; then + echo "Error: Invalid IP format. Expected format: xxx.xxx.xxx.xxx" + exit 1 +fi + +echo "🔐 Generating SSL certificate for localhost and $TAILSCALE_IP" + +# Determine the output directory - we should be in backends/advanced when running +SSL_DIR="ssl" +if [ -d "$SSL_DIR" ]; then + # We're in the parent directory + OUTPUT_DIR="$SSL_DIR" +else + # We're already in the ssl directory + OUTPUT_DIR="." +fi + +# Create certificate configuration with Subject Alternative Names +cat > $OUTPUT_DIR/server.conf << EOF +[req] +distinguished_name = req_distinguished_name +req_extensions = v3_req +prompt = no + +[req_distinguished_name] +C = US +ST = CA +L = SF +O = Dev +CN = localhost + +[v3_req] +keyUsage = critical, digitalSignature, keyEncipherment +extendedKeyUsage = serverAuth, clientAuth +subjectAltName = @alt_names + +[alt_names] +DNS.1 = localhost +DNS.2 = *.localhost +IP.1 = 127.0.0.1 +IP.2 = $TAILSCALE_IP +EOF + +# Generate private key +openssl genrsa -out $OUTPUT_DIR/server.key 2048 + +# Generate certificate signing request +openssl req -new -key $OUTPUT_DIR/server.key -out $OUTPUT_DIR/server.csr -config $OUTPUT_DIR/server.conf + +# Generate self-signed certificate +openssl x509 -req -in $OUTPUT_DIR/server.csr -signkey $OUTPUT_DIR/server.key -out $OUTPUT_DIR/server.crt -days 365 -extensions v3_req -extfile $OUTPUT_DIR/server.conf + +# Clean up +rm $OUTPUT_DIR/server.csr $OUTPUT_DIR/server.conf + +# Set appropriate permissions +chmod 600 $OUTPUT_DIR/server.key +chmod 644 $OUTPUT_DIR/server.crt + +echo "✅ SSL certificate generated successfully" +echo " - Certificate: $OUTPUT_DIR/server.crt" +echo " - Private key: $OUTPUT_DIR/server.key" +echo " - Valid for: localhost, *.localhost, 127.0.0.1, $TAILSCALE_IP" +echo "" +echo "Certificate Details:" +openssl x509 -in $OUTPUT_DIR/server.crt -text -noout | grep -A 1 "Subject Alternative Name" || echo " (Certificate generated successfully)" \ No newline at end of file From ec53ca3e04c8d8b674de31020de4e6d23dfbd459 Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Mon, 1 Sep 2025 21:40:02 +0000 Subject: [PATCH 25/35] template --- backends/advanced/.dockerignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backends/advanced/.dockerignore b/backends/advanced/.dockerignore index 95c32edd0..0ea8a946e 100644 --- a/backends/advanced/.dockerignore +++ b/backends/advanced/.dockerignore @@ -11,4 +11,7 @@ !diarization_config.json.template !diarization_config.json !uv.lock -!webui \ No newline at end of file +!webui +!ssl +!nginx.conf +!nginx.conf.template \ No newline at end of file From dffcc2d2804c7ceec1f410f08117a517259854ae Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Tue, 2 Sep 2025 00:09:04 +0000 Subject: [PATCH 26/35] the universe doesnt care? --- .../controllers/memory_controller.py | 4 +- .../controllers/system_controller.py | 6 + .../src/advanced_omi_backend/memory/base.py | 8 +- .../memory/compat_service.py | 5 +- .../memory/memory_service.py | 5 +- .../providers/openmemory_mcp_service.py | 4 +- .../memory/providers/vector_stores.py | 35 ++- .../routers/modules/memory_routes.py | 5 +- .../transcription_providers.py | 9 + .../advanced/webui/src/pages/Memories.tsx | 205 +++++++++++------- backends/advanced/webui/src/services/api.ts | 9 +- 11 files changed, 196 insertions(+), 99 deletions(-) diff --git a/backends/advanced/src/advanced_omi_backend/controllers/memory_controller.py b/backends/advanced/src/advanced_omi_backend/controllers/memory_controller.py index 135dbeb0f..436a047a5 100644 --- a/backends/advanced/src/advanced_omi_backend/controllers/memory_controller.py +++ b/backends/advanced/src/advanced_omi_backend/controllers/memory_controller.py @@ -79,7 +79,7 @@ async def get_memories_with_transcripts(user: User, limit: int, user_id: Optiona ) -async def search_memories(query: str, user: User, limit: int, user_id: Optional[str] = None): +async def search_memories(query: str, user: User, limit: int, score_threshold: float = 0.0, user_id: Optional[str] = None): """Search memories by text query. Users can only search their own memories, admins can search all or filter by user.""" try: memory_service = get_memory_service() @@ -90,7 +90,7 @@ async def search_memories(query: str, user: User, limit: int, user_id: Optional[ target_user_id = user_id # Execute search directly (now async) - search_results = await memory_service.search_memories(query, target_user_id, limit) + search_results = await memory_service.search_memories(query, target_user_id, limit, score_threshold) return { "query": query, diff --git a/backends/advanced/src/advanced_omi_backend/controllers/system_controller.py b/backends/advanced/src/advanced_omi_backend/controllers/system_controller.py index da524cf0d..b56866be7 100644 --- a/backends/advanced/src/advanced_omi_backend/controllers/system_controller.py +++ b/backends/advanced/src/advanced_omi_backend/controllers/system_controller.py @@ -6,6 +6,7 @@ import io import json import logging +import os import shutil import time import wave @@ -30,9 +31,14 @@ async def get_current_metrics(): """Get current system metrics.""" try: + # Get memory provider configuration + memory_provider = os.getenv("MEMORY_PROVIDER", "friend_lite").lower() + # Get basic system metrics metrics = { "timestamp": int(time.time()), + "memory_provider": memory_provider, + "memory_provider_supports_threshold": memory_provider == "friend_lite", } return metrics diff --git a/backends/advanced/src/advanced_omi_backend/memory/base.py b/backends/advanced/src/advanced_omi_backend/memory/base.py index 48afd0245..97ad5e3db 100644 --- a/backends/advanced/src/advanced_omi_backend/memory/base.py +++ b/backends/advanced/src/advanced_omi_backend/memory/base.py @@ -102,7 +102,8 @@ async def search_memories( self, query: str, user_id: str, - limit: int = 10 + limit: int = 10, + score_threshold: float = 0.0 ) -> List[MemoryEntry]: """Search memories using semantic similarity. @@ -110,6 +111,7 @@ async def search_memories( query: Search query text user_id: User identifier to filter memories limit: Maximum number of results to return + score_threshold: Minimum similarity score (0.0 = no threshold) Returns: List of matching MemoryEntry objects ordered by relevance @@ -293,7 +295,8 @@ async def search_memories( self, query_embedding: List[float], user_id: str, - limit: int + limit: int, + score_threshold: float = 0.0 ) -> List[MemoryEntry]: """Search memories using vector similarity. @@ -301,6 +304,7 @@ async def search_memories( query_embedding: Query vector for similarity search user_id: User identifier to filter results limit: Maximum number of results to return + score_threshold: Minimum similarity score (0.0 = no threshold) Returns: List of matching MemoryEntry objects with similarity scores diff --git a/backends/advanced/src/advanced_omi_backend/memory/compat_service.py b/backends/advanced/src/advanced_omi_backend/memory/compat_service.py index 79cfd1efa..e3cb9827a 100644 --- a/backends/advanced/src/advanced_omi_backend/memory/compat_service.py +++ b/backends/advanced/src/advanced_omi_backend/memory/compat_service.py @@ -161,13 +161,14 @@ async def get_all_memories_unfiltered(self, user_id: str, limit: int = 100) -> L """ return await self.get_all_memories(user_id, limit) - async def search_memories(self, query: str, user_id: str, limit: int = 10) -> List[Dict[str, Any]]: + async def search_memories(self, query: str, user_id: str, limit: int = 10, score_threshold: float = 0.0) -> List[Dict[str, Any]]: """Search memories using semantic similarity - returns dict format for compatibility. Args: query: Search query text user_id: User identifier to filter memories limit: Maximum number of results to return + score_threshold: Minimum similarity score (0.0 = no threshold) Returns: List of memory dictionaries in legacy format ordered by relevance @@ -175,7 +176,7 @@ async def search_memories(self, query: str, user_id: str, limit: int = 10) -> Li if not self._initialized: await self.initialize() - memories = await self._service.search_memories(query, user_id, limit) + memories = await self._service.search_memories(query, user_id, limit, score_threshold) # Convert MemoryEntry objects to dict format for compatibility with normalized content return [ diff --git a/backends/advanced/src/advanced_omi_backend/memory/memory_service.py b/backends/advanced/src/advanced_omi_backend/memory/memory_service.py index 5cba626c0..dc5bc21e2 100644 --- a/backends/advanced/src/advanced_omi_backend/memory/memory_service.py +++ b/backends/advanced/src/advanced_omi_backend/memory/memory_service.py @@ -212,7 +212,7 @@ async def add_memory( memory_logger.error(f"❌ Add memory failed for {source_id}: {e}") raise e - async def search_memories(self, query: str, user_id: str, limit: int = 10) -> List[MemoryEntry]: + async def search_memories(self, query: str, user_id: str, limit: int = 10, score_threshold: float = 0.0) -> List[MemoryEntry]: """Search memories using semantic similarity. Generates an embedding for the query and searches the vector store @@ -222,6 +222,7 @@ async def search_memories(self, query: str, user_id: str, limit: int = 10) -> Li query: Search query text user_id: User identifier to filter memories limit: Maximum number of results to return + score_threshold: Minimum similarity score (0.0 = no threshold) Returns: List of matching MemoryEntry objects ordered by relevance @@ -238,7 +239,7 @@ async def search_memories(self, query: str, user_id: str, limit: int = 10) -> Li # Search in vector store results = await self.vector_store.search_memories( - query_embeddings[0], user_id, limit + query_embeddings[0], user_id, limit, score_threshold ) memory_logger.info(f"🔍 Found {len(results)} memories for query '{query}' (user: {user_id})") diff --git a/backends/advanced/src/advanced_omi_backend/memory/providers/openmemory_mcp_service.py b/backends/advanced/src/advanced_omi_backend/memory/providers/openmemory_mcp_service.py index 1b853c3f6..11231903b 100644 --- a/backends/advanced/src/advanced_omi_backend/memory/providers/openmemory_mcp_service.py +++ b/backends/advanced/src/advanced_omi_backend/memory/providers/openmemory_mcp_service.py @@ -181,7 +181,8 @@ async def search_memories( self, query: str, user_id: str, - limit: int = 10 + limit: int = 10, + score_threshold: float = 0.0 ) -> List[MemoryEntry]: """Search memories using semantic similarity. @@ -192,6 +193,7 @@ async def search_memories( query: Search query text user_id: User identifier to filter memories limit: Maximum number of results to return + score_threshold: Minimum similarity score (ignored - OpenMemory MCP server controls filtering) Returns: List of matching MemoryEntry objects ordered by relevance diff --git a/backends/advanced/src/advanced_omi_backend/memory/providers/vector_stores.py b/backends/advanced/src/advanced_omi_backend/memory/providers/vector_stores.py index 0e60ac0b9..50e95150f 100644 --- a/backends/advanced/src/advanced_omi_backend/memory/providers/vector_stores.py +++ b/backends/advanced/src/advanced_omi_backend/memory/providers/vector_stores.py @@ -148,8 +148,15 @@ async def add_memories(self, memories: List[MemoryEntry]) -> List[str]: memory_logger.error(f"Qdrant add memories failed: {e}") return [] - async def search_memories(self, query_embedding: List[float], user_id: str, limit: int) -> List[MemoryEntry]: - """Search memories in Qdrant.""" + async def search_memories(self, query_embedding: List[float], user_id: str, limit: int, score_threshold: float = 0.0) -> List[MemoryEntry]: + """Search memories in Qdrant with configurable similarity threshold filtering. + + Args: + query_embedding: Query vector for similarity search + user_id: User identifier to filter results + limit: Maximum number of results to return + score_threshold: Minimum similarity score (0.0 = no threshold, 1.0 = exact match) + """ try: # Filter by user_id search_filter = Filter( @@ -161,12 +168,20 @@ async def search_memories(self, query_embedding: List[float], user_id: str, limi ] ) - results = await self.client.search( - collection_name=self.collection_name, - query_vector=query_embedding, - query_filter=search_filter, - limit=limit - ) + # Apply similarity threshold if provided + # For cosine similarity, scores range from -1 to 1, where 1 is most similar + search_params = { + "collection_name": self.collection_name, + "query_vector": query_embedding, + "query_filter": search_filter, + "limit": limit + } + + if score_threshold > 0.0: + search_params["score_threshold"] = score_threshold + memory_logger.debug(f"Using similarity threshold: {score_threshold}") + + results = await self.client.search(**search_params) memories = [] for result in results: @@ -179,7 +194,11 @@ async def search_memories(self, query_embedding: List[float], user_id: str, limi created_at=result.payload.get("created_at") ) memories.append(memory) + # Log similarity scores for debugging + memory_logger.debug(f"Retrieved memory with score {result.score:.3f}: {result.payload.get('content', '')[:50]}...") + threshold_msg = f"threshold {score_threshold}" if score_threshold > 0.0 else "no threshold" + memory_logger.info(f"Found {len(memories)} memories with {threshold_msg} for user {user_id}") return memories except Exception as e: diff --git a/backends/advanced/src/advanced_omi_backend/routers/modules/memory_routes.py b/backends/advanced/src/advanced_omi_backend/routers/modules/memory_routes.py index 424ca9514..4d71ce6d4 100644 --- a/backends/advanced/src/advanced_omi_backend/routers/modules/memory_routes.py +++ b/backends/advanced/src/advanced_omi_backend/routers/modules/memory_routes.py @@ -43,10 +43,11 @@ async def search_memories( query: str = Query(..., description="Search query"), current_user: User = Depends(current_active_user), limit: int = Query(default=20, ge=1, le=100), + score_threshold: float = Query(default=0.0, ge=0.0, le=1.0, description="Minimum similarity score (0.0 = no threshold)"), user_id: Optional[str] = Query(default=None, description="User ID filter (admin only)"), ): - """Search memories by text query. Users can only search their own memories, admins can search all or filter by user.""" - return await memory_controller.search_memories(query, current_user, limit, user_id) + """Search memories by text query with configurable similarity threshold. Users can only search their own memories, admins can search all or filter by user.""" + return await memory_controller.search_memories(query, current_user, limit, score_threshold, user_id) @router.delete("/{memory_id}") diff --git a/backends/advanced/src/advanced_omi_backend/transcription_providers.py b/backends/advanced/src/advanced_omi_backend/transcription_providers.py index 53c48eaaa..662fc57c7 100644 --- a/backends/advanced/src/advanced_omi_backend/transcription_providers.py +++ b/backends/advanced/src/advanced_omi_backend/transcription_providers.py @@ -773,5 +773,14 @@ def get_transcription_provider( ) logger.info(f"Using Parakeet transcription provider in {mode} mode") return ParakeetProvider(parakeet_url) + + elif provider_name == "offline": + # "offline" is an alias for Parakeet ASR + if not parakeet_url: + raise RuntimeError( + "Offline transcription provider requested but PARAKEET_ASR_URL not configured" + ) + logger.info(f"Using offline Parakeet transcription provider in {mode} mode") + return ParakeetProvider(parakeet_url) else: return None diff --git a/backends/advanced/webui/src/pages/Memories.tsx b/backends/advanced/webui/src/pages/Memories.tsx index ce17772e4..ce6882498 100644 --- a/backends/advanced/webui/src/pages/Memories.tsx +++ b/backends/advanced/webui/src/pages/Memories.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react' import { Brain, Search, RefreshCw, Trash2, Calendar, Tag, X, Target } from 'lucide-react' -import { memoriesApi } from '../services/api' +import { memoriesApi, systemApi } from '../services/api' import { useAuth } from '../contexts/AuthContext' import '../styles/slider.css' @@ -32,8 +32,28 @@ export default function Memories() { const [semanticLoading, setSemanticLoading] = useState(false) const [relevanceThreshold, setRelevanceThreshold] = useState(0) // 0-100 percentage + // System configuration state + const [memoryProviderSupportsThreshold, setMemoryProviderSupportsThreshold] = useState(false) + const [systemLoading, setSystemLoading] = useState(true) + const { user } = useAuth() + const loadSystemConfig = async () => { + try { + setSystemLoading(true) + const response = await systemApi.getMetrics() + const supports = response.data.memory_provider_supports_threshold || false + setMemoryProviderSupportsThreshold(supports) + console.log('🔧 Memory provider supports threshold:', supports) + } catch (err: any) { + console.error('❌ Failed to load system config:', err) + // Default to false if we can't determine + setMemoryProviderSupportsThreshold(false) + } finally { + setSystemLoading(false) + } + } + const loadMemories = async () => { if (!user?.id) return @@ -69,6 +89,10 @@ export default function Memories() { } } + useEffect(() => { + loadSystemConfig() + }, []) + useEffect(() => { loadMemories() }, [user?.id, showUnfiltered]) @@ -79,7 +103,21 @@ export default function Memories() { try { setSemanticLoading(true) - const response = await memoriesApi.search(searchQuery.trim(), user.id, 50) + + // Use current threshold for server-side filtering if memory provider supports it + const thresholdToUse = memoryProviderSupportsThreshold + ? relevanceThreshold + : undefined + + const response = await memoriesApi.search( + searchQuery.trim(), + user.id, + 50, + thresholdToUse + ) + + console.log('🔍 Search response:', response.data) + console.log('🎯 Used threshold:', thresholdToUse) setSemanticResults(response.data.results || []) setSemanticQuery(searchQuery.trim()) @@ -116,11 +154,11 @@ export default function Memories() { } } - // Update filtering logic for dual-layer search with relevance threshold + // Update filtering logic with client-side threshold filtering after search const currentMemories = isSemanticFilterActive ? semanticResults : memories - // Apply relevance threshold filter for semantic results - const relevanceFilteredMemories = isSemanticFilterActive + // Apply relevance threshold filter (client-side for all providers after search) + const thresholdFilteredMemories = isSemanticFilterActive && relevanceThreshold > 0 ? currentMemories.filter(memory => { if (!memory.score) return true // If no score, show it const relevancePercentage = memory.score * 100 @@ -129,7 +167,7 @@ export default function Memories() { : currentMemories // Apply text search filter - const filteredMemories = relevanceFilteredMemories.filter(memory => + const filteredMemories = thresholdFilteredMemories.filter(memory => memory.memory.toLowerCase().includes(searchQuery.toLowerCase()) || (memory.category?.toLowerCase() || '').includes(searchQuery.toLowerCase()) ) @@ -242,38 +280,90 @@ export default function Memories() { {/* Search */} {memories.length > 0 && ( -
- - setSearchQuery(e.target.value)} - placeholder="Search memories..." - className="w-full pl-10 pr-32 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500" - onKeyPress={(e) => e.key === 'Enter' && handleSemanticSearch()} - /> - +
+
+ + setSearchQuery(e.target.value)} + placeholder="Search memories..." + className="w-full pl-10 pr-32 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500" + onKeyPress={(e) => e.key === 'Enter' && handleSemanticSearch()} + /> + +
+ + {/* Initial Search Threshold Slider - Show for Friend-Lite provider */} + {memoryProviderSupportsThreshold && ( +
+
+ +
+ + {relevanceThreshold}% + +
+
+
+ setRelevanceThreshold(Number(e.target.value))} + className="flex-1 h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer slider" + style={{ + ['--progress' as any]: `${relevanceThreshold}%`, + background: `linear-gradient(to right, #3B82F6 0%, #3B82F6 ${relevanceThreshold}%, #E5E7EB ${relevanceThreshold}%, #E5E7EB 100%)` + }} + disabled={semanticLoading} + /> + +
+

+ {isSemanticFilterActive ? ( + relevanceThreshold > 0 ? + `Filtering loaded results: showing memories with ≥ ${relevanceThreshold}% relevance` : + 'Showing all loaded results' + ) : ( + relevanceThreshold > 0 ? + `Next search will filter server-side: memories with ≥ ${relevanceThreshold}% relevance` : + 'Next search will return all results (no server-side filtering)' + )} +

+
+ )}
)}
- {/* Semantic Filter Indicator with Relevance Slider */} + {/* Semantic Filter Indicator */} {isSemanticFilterActive && ( -
+
- Semantic filter active: "{semanticQuery}" + Semantic search active: "{semanticQuery}"
- - {/* Relevance Threshold Slider */} -
-
- -
- - {relevanceThreshold}% - - {relevanceThreshold > 0 && ( - - ({relevanceFilteredMemories.length} of {semanticResults.length} shown) - - )} -
-
-
- setRelevanceThreshold(Number(e.target.value))} - className="flex-1 h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer slider" - style={{ - ['--progress' as any]: `${relevanceThreshold}%`, - background: `linear-gradient(to right, #3B82F6 0%, #3B82F6 ${relevanceThreshold}%, #E5E7EB ${relevanceThreshold}%, #E5E7EB 100%)` - }} - /> - -
- {relevanceThreshold > 0 && ( -

- Showing memories with relevance ≥ {relevanceThreshold}% -

- )} -
)} @@ -341,15 +386,15 @@ export default function Memories() { {isSemanticFilterActive ? ( relevanceThreshold > 0 ? ( searchQuery ? ( - `Showing ${filteredMemories.length} memories (relevance ≥ ${relevanceThreshold}% and matching "${searchQuery}")` + `Showing ${filteredMemories.length} of ${semanticResults.length} semantic matches (filtered by ≥${relevanceThreshold}% relevance + "${searchQuery}")` ) : ( - `Showing ${relevanceFilteredMemories.length} of ${semanticResults.length} semantic matches (relevance ≥ ${relevanceThreshold}%)` + `Showing ${thresholdFilteredMemories.length} of ${semanticResults.length} semantic matches (filtered by ≥${relevanceThreshold}% relevance)` ) ) : ( searchQuery ? ( - `Showing ${filteredMemories.length} of ${semanticResults.length} semantic matches filtered by "${searchQuery}"` + `Showing ${filteredMemories.length} of ${semanticResults.length} semantic matches (filtered by "${searchQuery}")` ) : ( - `Showing ${semanticResults.length} semantic matches for "${semanticQuery}"` + `Showing all ${semanticResults.length} semantic matches for "${semanticQuery}"` ) ) ) : ( @@ -386,7 +431,11 @@ export default function Memories() {

{isSemanticFilterActive ? ( relevanceThreshold > 0 ? ( - `Filtered to ${filteredMemories.length} memories (threshold: ${relevanceThreshold}%${searchQuery ? `, text: "${searchQuery}"` : ''})` + searchQuery ? ( + `Relevance filtered (≥${relevanceThreshold}%) + text filtered: ${filteredMemories.length} results` + ) : ( + `Relevance filtered (≥${relevanceThreshold}%): ${thresholdFilteredMemories.length} results` + ) ) : ( searchQuery ? ( `Found ${filteredMemories.length} semantic results matching "${searchQuery}"` diff --git a/backends/advanced/webui/src/services/api.ts b/backends/advanced/webui/src/services/api.ts index 56b487785..19f778d0c 100644 --- a/backends/advanced/webui/src/services/api.ts +++ b/backends/advanced/webui/src/services/api.ts @@ -79,9 +79,14 @@ export const conversationsApi = { export const memoriesApi = { getAll: (userId?: string) => api.get('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/api/memories', { params: userId ? { user_id: userId } : {} }), getUnfiltered: (userId?: string) => api.get('/api/memories/unfiltered', { params: userId ? { user_id: userId } : {} }), - search: (query: string, userId?: string, limit: number = 20) => + search: (query: string, userId?: string, limit: number = 20, scoreThreshold?: number) => api.get('/api/memories/search', { - params: { query, ...(userId && { user_id: userId }), limit } + params: { + query, + ...(userId && { user_id: userId }), + limit, + ...(scoreThreshold !== undefined && { score_threshold: scoreThreshold / 100 }) // Convert percentage to decimal + } }), delete: (id: string) => api.delete(`/api/memories/${id}`), } From 8a02f65010b7ae3ee4a360f533cf3517643e177d Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Tue, 2 Sep 2025 00:09:34 +0000 Subject: [PATCH 27/35] ignore .env backupds --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index de8d48f03..0252fb581 100644 --- a/.gitignore +++ b/.gitignore @@ -66,4 +66,5 @@ backends/advanced-backend/data/speaker_model_cache/ *.sqlite3 *checkpoints -extras/openmemory-mcp/data/* \ No newline at end of file +extras/openmemory-mcp/data/* +.env.backup.* \ No newline at end of file From d25d6c6786c87747909d0070394ddb2900cff6d6 Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Tue, 2 Sep 2025 06:33:35 +0000 Subject: [PATCH 28/35] fix test --- backends/advanced/pyproject.toml | 5 +++++ backends/advanced/webui/src/pages/Memories.tsx | 4 ---- backends/advanced/webui/src/pages/System.tsx | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/backends/advanced/pyproject.toml b/backends/advanced/pyproject.toml index d366403da..369039a68 100644 --- a/backends/advanced/pyproject.toml +++ b/backends/advanced/pyproject.toml @@ -51,6 +51,11 @@ line-length = 100 [tool.uv.sources] mem0ai = { git = "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/AnkushMalaker/mem0.git", rev = "async-client-unbound-var-fix" } +[tool.pytest.ini_options] +markers = [ + "integration: marks tests as integration tests", +] + [dependency-groups] dev = [ "black>=25.1.0", diff --git a/backends/advanced/webui/src/pages/Memories.tsx b/backends/advanced/webui/src/pages/Memories.tsx index ce6882498..0d83b15e2 100644 --- a/backends/advanced/webui/src/pages/Memories.tsx +++ b/backends/advanced/webui/src/pages/Memories.tsx @@ -34,13 +34,11 @@ export default function Memories() { // System configuration state const [memoryProviderSupportsThreshold, setMemoryProviderSupportsThreshold] = useState(false) - const [systemLoading, setSystemLoading] = useState(true) const { user } = useAuth() const loadSystemConfig = async () => { try { - setSystemLoading(true) const response = await systemApi.getMetrics() const supports = response.data.memory_provider_supports_threshold || false setMemoryProviderSupportsThreshold(supports) @@ -49,8 +47,6 @@ export default function Memories() { console.error('❌ Failed to load system config:', err) // Default to false if we can't determine setMemoryProviderSupportsThreshold(false) - } finally { - setSystemLoading(false) } } diff --git a/backends/advanced/webui/src/pages/System.tsx b/backends/advanced/webui/src/pages/System.tsx index 14ba2d09b..b399ad5bc 100644 --- a/backends/advanced/webui/src/pages/System.tsx +++ b/backends/advanced/webui/src/pages/System.tsx @@ -609,7 +609,7 @@ function SpeakerConfiguration() { setMessage('') try { - const response = await speakerApi.updateSpeakerConfiguration(primarySpeakers) + await speakerApi.updateSpeakerConfiguration(primarySpeakers) setMessage(`✅ Saved! ${primarySpeakers.length} primary speakers configured.`) // Auto-hide success message after 3 seconds From 69d5524279c69ff4f8c98718d4b13f28e92294d1 Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Fri, 5 Sep 2025 06:49:41 +0530 Subject: [PATCH 29/35] Update backends/advanced/init.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- backends/advanced/init.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/backends/advanced/init.py b/backends/advanced/init.py index c4f50382f..fd3dfc708 100644 --- a/backends/advanced/init.py +++ b/backends/advanced/init.py @@ -358,11 +358,10 @@ def generate_env_file(self): # LLM Provider LLM_PROVIDER={self.config.get('LLM_PROVIDER', '')} OPENAI_API_KEY={self.config.get('OPENAI_API_KEY', '')} -OPENAI_MODEL={self.config.get('OPENAI_MODEL', 'gpt-4o-mini')} -OPENAI_BASE_URL={self.config.get('OPENAI_BASE_URL', 'https://api.openai.com/v1')} +OPENAI_MODEL={self.config.get('OPENAI_MODEL', '')} +OPENAI_BASE_URL={self.config.get('OPENAI_BASE_URL', '')} OLLAMA_BASE_URL={self.config.get('OLLAMA_BASE_URL', '')} OLLAMA_MODEL={self.config.get('OLLAMA_MODEL', '')} - # Memory Provider MEMORY_PROVIDER={self.config.get('MEMORY_PROVIDER', 'friend_lite')} QDRANT_BASE_URL={self.config.get('QDRANT_BASE_URL', 'qdrant')} From 939c2a153bc12a2e993894bfa537e3294aa44665 Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Fri, 5 Sep 2025 02:52:47 +0000 Subject: [PATCH 30/35] refactor based on coderabbit suggestions --- backends/advanced/Docs/architecture.md | 24 ++++---- backends/advanced/SETUP_SCRIPTS.md | 34 ++++-------- backends/advanced/init.py | 7 ++- backends/advanced/setup-https.sh | 55 ++++++++++++++++--- .../controllers/memory_controller.py | 9 +-- .../controllers/system_controller.py | 5 +- .../advanced/src/advanced_omi_backend/main.py | 4 +- .../src/advanced_omi_backend/memory/base.py | 10 ++-- .../memory/providers/vector_stores.py | 11 ++-- .../src/advanced_omi_backend/processors.py | 33 +++++++---- extras/speaker-recognition/setup.sh | 34 ++++++++++-- init.py | 14 +++++ 12 files changed, 158 insertions(+), 82 deletions(-) diff --git a/backends/advanced/Docs/architecture.md b/backends/advanced/Docs/architecture.md index c0264528e..78435ef1e 100644 --- a/backends/advanced/Docs/architecture.md +++ b/backends/advanced/Docs/architecture.md @@ -455,7 +455,7 @@ The `close_conversation()` method in ClientState now delegates to ConversationMa 3. **Manual API Call** - `/api/conversations/{client_id}/close` endpoint - User-initiated closure - - UI button in Streamlit dashboard + - UI button in React dashboard 4. **Conversation Timeout** (Not shown in diagram) - 1.5 minute silence detection @@ -583,7 +583,7 @@ stateDiagram-v2 graph LR subgraph "Docker Network" Backend[friend-backend
uv + FastAPI] - Streamlit[streamlit
Dashboard UI] + WebUI[webui
React Dashboard] Proxy[nginx
Load Balancer] Mongo[mongo:4.4.18
Primary Database] Qdrant[qdrant
Vector Store] @@ -599,12 +599,12 @@ graph LR AudioClient[Audio Client
Mobile/Desktop] end - WebBrowser -->|Port 8501| Streamlit - WebBrowser -->|Port 80| Proxy + WebBrowser -->|Port 5173 (dev)| WebUI + WebBrowser -->|Port 80 (prod)| Proxy AudioClient -->|Port 8000| Backend Proxy --> Backend - Proxy --> Streamlit + Proxy --> WebUI Backend --> Mongo Backend --> Qdrant Backend -.->|Optional| Ollama @@ -620,17 +620,19 @@ graph LR - **Health Checks**: Automated readiness and liveness probes - **Environment**: All configuration via environment variables -#### Streamlit Container (`streamlit`) -- **Purpose**: Web dashboard interface -- **Dependencies**: Streamlit, requests, pandas for data visualization -- **Backend Integration**: HTTP API client with authentication +#### React WebUI Container (`webui`) +- **Purpose**: Modern React-based web dashboard interface +- **Dependencies**: React, TypeScript, Vite for development, Nginx for production +- **Backend Integration**: HTTP API client with JWT authentication - **Configuration**: Backend URL configuration for API calls +- **Development**: Vite dev server on port 5173 +- **Production**: Nginx serving built React app on port 80 #### Infrastructure Containers - **MongoDB 4.4.18**: Primary data storage with persistence - **Qdrant Latest**: Vector database for memory embeddings - **Neo4j 5.15**: Graph database for memory relationships and entity connections -- **Nginx Alpine**: Reverse proxy and load balancing +- **Nginx Alpine**: Reverse proxy and load balancing (serves React app in production, proxies API calls to backend) ## Detailed Data Flow Architecture @@ -643,7 +645,7 @@ graph LR flowchart TB %% External Clients Client[📱 Audio Client
Mobile/Desktop/HAVPE] - WebUI[🌐 Web Dashboard
Streamlit Interface] + WebUI[🌐 Web Dashboard
React Web UI] %% Authentication Gateway subgraph "🔐 Authentication Layer" diff --git a/backends/advanced/SETUP_SCRIPTS.md b/backends/advanced/SETUP_SCRIPTS.md index 1c04e29bb..0e1817282 100644 --- a/backends/advanced/SETUP_SCRIPTS.md +++ b/backends/advanced/SETUP_SCRIPTS.md @@ -6,10 +6,10 @@ This document explains the different setup scripts available in Friend-Lite and | Script | Purpose | When to Use | |--------|---------|-------------| -| `init.sh` | **Main interactive setup wizard** | **Recommended for all users** - First time setup with guided configuration | +| `init.py` | **Main interactive setup wizard** | **Recommended for all users** - First time setup with guided configuration (located at repo root) | | `setup-https.sh` | HTTPS certificate generation | **Optional** - When you need secure connections for microphone access | -## Main Setup Script: `init.sh` +## Main Setup Script: `init.py` **Purpose**: Interactive wizard that configures all services with guided prompts. @@ -24,8 +24,8 @@ This document explains the different setup scripts available in Friend-Lite and ### Usage: ```bash -cd backends/advanced -./init.sh +# From repository root +python backends/advanced/init.py ``` ### Example Flow: @@ -118,22 +118,11 @@ https://localhost/ https://100.83.66.30/ # Your configured IP ``` -## Migration from Old Scripts - -### Old vs New Script Names: - -| Old Name | New Name | Purpose | -|----------|----------|---------| -| `init.sh` (old) | `setup-https.sh` | HTTPS setup only | -| `setup.sh` (created) | `init.sh` (new) | Main interactive setup | - -### If you have old scripts: -The old `init.sh` has been renamed to `setup-https.sh` to avoid confusion. The new `init.sh` is the main setup wizard. ## Recommended Setup Flow ### New Users (Recommended): -1. **Run main setup**: `./init.sh` +1. **Run main setup**: `python backends/advanced/init.py` 2. **Start services**: `docker compose up --build -d` 3. **Optional HTTPS**: `./setup-https.sh your-ip` (if needed) @@ -144,13 +133,14 @@ The old `init.sh` has been renamed to `setup-https.sh` to avoid confusion. The n ## Script Locations -All setup scripts are located in: +Setup scripts are located as follows: ``` -backends/advanced/ -├── init.sh # Main interactive setup wizard -├── setup-https.sh # HTTPS certificate generation -├── .env.template # Environment template -└── docker-compose.yml +. # Project root +├── init.py # Main interactive setup wizard (repo root) +└── backends/advanced/ + ├── setup-https.sh # HTTPS certificate generation + ├── .env.template # Environment template + └── docker-compose.yml ``` ## Getting Help diff --git a/backends/advanced/init.py b/backends/advanced/init.py index fd3dfc708..bb1099e2c 100644 --- a/backends/advanced/init.py +++ b/backends/advanced/init.py @@ -388,10 +388,13 @@ def generate_env_file(self): LOG_LEVEL=INFO """ - with open(".env", "w") as f: + # Create .env file with secure permissions (owner read/write only) + env_path = ".env" + fd = os.open(env_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode=0o600) + with os.fdopen(fd, 'w') as f: f.write(env_content) - self.console.print("[green][SUCCESS][/green] .env file created successfully") + self.console.print("[green][SUCCESS][/green] .env file created successfully with secure permissions") def copy_config_templates(self): """Copy other configuration files""" diff --git a/backends/advanced/setup-https.sh b/backends/advanced/setup-https.sh index 943469b4f..38f5a5b0d 100755 --- a/backends/advanced/setup-https.sh +++ b/backends/advanced/setup-https.sh @@ -174,9 +174,54 @@ print_header "Step 5: Optional Services (extras/)" echo "Configure additional services from extras/:" echo "" +# Helper function to update or add environment variable in .env file +update_env_var() { + local key=$1 + local value=$2 + + # Use Python to safely update the .env file + python3 -c " +import sys +import re + +key = '$key' +value = '$value' +env_file = '.env' + +# Read existing .env file +try: + with open(env_file, 'r') as f: + lines = f.readlines() +except FileNotFoundError: + lines = [] + +# Check if key exists (uncommented) +updated = False +for i, line in enumerate(lines): + # Skip comments + if line.strip().startswith('#'): + continue + # Check for existing key + if re.match(f'^\\s*{re.escape(key)}=', line): + lines[i] = f'{key}={value}\\n' + updated = True + break + +# If not found, append to end +if not updated: + if lines and not lines[-1].endswith('\\n'): + lines.append('\\n') + lines.append(f'{key}={value}\\n') + +# Write back to file +with open(env_file, 'w') as f: + f.writelines(lines) +" +} + # OpenMemory MCP (Memory Provider) if prompt_yes_no "Use OpenMemory MCP for memory management? (cross-client compatible)" "n"; then - sed -i 's/MEMORY_PROVIDER=.*/MEMORY_PROVIDER=openmemory_mcp/' .env + update_env_var "MEMORY_PROVIDER" "openmemory_mcp" print_success "Configured for OpenMemory MCP" OPENMEMORY_ENABLED=true else @@ -185,9 +230,7 @@ fi # Parakeet ASR (Offline Transcription) if prompt_yes_no "Use Parakeet for offline transcription? (requires GPU)" "n"; then - if ! grep -q "PARAKEET_ASR_URL" .env; then - echo "PARAKEET_ASR_URL=http://host.docker.internal:8767" >> .env - fi + update_env_var "PARAKEET_ASR_URL" "http://host.docker.internal:8767" print_success "Configured for Parakeet ASR" PARAKEET_ENABLED=true else @@ -196,9 +239,7 @@ fi # Speaker Recognition if prompt_yes_no "Enable Speaker Recognition service?" "n"; then - if ! grep -q "SPEAKER_SERVICE_URL" .env; then - echo "SPEAKER_SERVICE_URL=http://host.docker.internal:8001" >> .env - fi + update_env_var "SPEAKER_SERVICE_URL" "http://host.docker.internal:8001" print_success "Configured for Speaker Recognition" SPEAKER_ENABLED=true else diff --git a/backends/advanced/src/advanced_omi_backend/controllers/memory_controller.py b/backends/advanced/src/advanced_omi_backend/controllers/memory_controller.py index 436a047a5..e5f576c27 100644 --- a/backends/advanced/src/advanced_omi_backend/controllers/memory_controller.py +++ b/backends/advanced/src/advanced_omi_backend/controllers/memory_controller.py @@ -28,13 +28,8 @@ async def get_memories(user: User, limit: int, user_id: Optional[str] = None): # Execute memory retrieval directly (now async) memories = await memory_service.get_all_memories(target_user_id, limit) - # Get total count if supported (graceful fallback) - total_count = None - try: - if hasattr(memory_service, 'count_memories'): - total_count = await memory_service.count_memories(target_user_id) - except Exception as e: - audio_logger.warning(f"Count memories not supported or failed: {e}") + # Get total count (service returns None on failure) + total_count = await memory_service.count_memories(target_user_id) return { "memories": memories, diff --git a/backends/advanced/src/advanced_omi_backend/controllers/system_controller.py b/backends/advanced/src/advanced_omi_backend/controllers/system_controller.py index b56866be7..97c46e891 100644 --- a/backends/advanced/src/advanced_omi_backend/controllers/system_controller.py +++ b/backends/advanced/src/advanced_omi_backend/controllers/system_controller.py @@ -10,6 +10,7 @@ import shutil import time import wave +from datetime import UTC, datetime from pathlib import Path import numpy as np @@ -952,9 +953,9 @@ async def update_speaker_configuration(user: User, primary_speakers: list[dict]) status_code=400, content={"error": f"Missing required field: {field}"} ) - # Add timestamp to each speaker - from datetime import UTC, datetime + # Enforce server-side user_id and add timestamp to each speaker for speaker in primary_speakers: + speaker["user_id"] = user.user_id # Override client-supplied user_id speaker["selected_at"] = datetime.now(UTC).isoformat() # Update user model diff --git a/backends/advanced/src/advanced_omi_backend/main.py b/backends/advanced/src/advanced_omi_backend/main.py index 7fcea4b8c..6d4a6a934 100644 --- a/backends/advanced/src/advanced_omi_backend/main.py +++ b/backends/advanced/src/advanced_omi_backend/main.py @@ -970,7 +970,7 @@ async def health_check(): if memory_provider == "friend_lite": try: # Test Friend-Lite memory service connection with timeout - test_success = await memory_service.test_connection() + test_success = await asyncio.wait_for(memory_service.test_connection(), timeout=8.0) if test_success: health_status["services"]["memory_service"] = { "status": "✅ Friend-Lite Memory Connected", @@ -988,7 +988,7 @@ async def health_check(): overall_healthy = False except asyncio.TimeoutError: health_status["services"]["memory_service"] = { - "status": "⚠️ Friend-Lite Memory Timeout (60s) - Check Qdrant", + "status": "⚠️ Friend-Lite Memory Timeout (8s) - Check Qdrant", "healthy": False, "provider": "friend_lite", "critical": False, diff --git a/backends/advanced/src/advanced_omi_backend/memory/base.py b/backends/advanced/src/advanced_omi_backend/memory/base.py index 97ad5e3db..65d39d750 100644 --- a/backends/advanced/src/advanced_omi_backend/memory/base.py +++ b/backends/advanced/src/advanced_omi_backend/memory/base.py @@ -324,19 +324,19 @@ async def get_memories(self, user_id: str, limit: int) -> List[MemoryEntry]: """ pass - async def count_memories(self, user_id: str) -> int: + async def count_memories(self, user_id: str) -> Optional[int]: """Count total number of memories for a user. - Default implementation returns 0. Vector stores should override - this method to provide efficient counting if supported. + Default implementation returns None to indicate counting is unsupported. + Vector stores should override this method to provide efficient counting if supported. Args: user_id: User identifier Returns: - Total count of memories for the user + Total count of memories for the user, or None if counting is not supported by this store """ - return 0 + return None @abstractmethod async def update_memory( diff --git a/backends/advanced/src/advanced_omi_backend/memory/providers/vector_stores.py b/backends/advanced/src/advanced_omi_backend/memory/providers/vector_stores.py index 50e95150f..5db7b6e63 100644 --- a/backends/advanced/src/advanced_omi_backend/memory/providers/vector_stores.py +++ b/backends/advanced/src/advanced_omi_backend/memory/providers/vector_stores.py @@ -11,11 +11,15 @@ import uuid from typing import Any, Dict, List +from qdrant_client import AsyncQdrantClient from qdrant_client.models import ( + Distance, FieldCondition, Filter, FilterSelector, MatchValue, + PointStruct, + VectorParams, ) from ..base import MemoryEntry, VectorStoreBase @@ -58,9 +62,6 @@ async def initialize(self) -> None: RuntimeError: If initialization fails """ try: - from qdrant_client import AsyncQdrantClient - from qdrant_client.models import Distance, VectorParams - self.client = AsyncQdrantClient(host=self.host, port=self.port) # Check if collection exists and get its info @@ -119,8 +120,6 @@ async def initialize(self) -> None: async def add_memories(self, memories: List[MemoryEntry]) -> List[str]: """Add memories to Qdrant.""" try: - from qdrant_client.models import PointStruct - points = [] for memory in memories: if memory.embedding: @@ -317,8 +316,6 @@ async def update_memory( ) -> bool: """Update (upsert) an existing memory in Qdrant.""" try: - from qdrant_client.models import PointStruct - payload = { "content": new_content, "metadata": new_metadata, diff --git a/backends/advanced/src/advanced_omi_backend/processors.py b/backends/advanced/src/advanced_omi_backend/processors.py index 20c2e921b..cbedc117f 100644 --- a/backends/advanced/src/advanced_omi_backend/processors.py +++ b/backends/advanced/src/advanced_omi_backend/processors.py @@ -19,6 +19,7 @@ from advanced_omi_backend.audio_cropping_utils import ( _process_audio_cropping_with_relative_timestamps, ) +from advanced_omi_backend.users import get_user_by_id from advanced_omi_backend.client_manager import get_client_manager from advanced_omi_backend.database import AudioChunksRepository from advanced_omi_backend.memory import get_memory_service @@ -179,8 +180,6 @@ async def _should_process_memory(self, user_id: str, audio_uuid: str) -> tuple[b """ try: # Get user's primary speaker configuration - from advanced_omi_backend.users import get_user_by_id - user = await get_user_by_id(user_id) if not user or not user.primary_speakers: return True, "No primary speakers configured - processing all conversations" @@ -192,30 +191,40 @@ async def _should_process_memory(self, user_id: str, audio_uuid: str) -> tuple[b if not chunk or not chunk.get('transcript'): return True, "No transcript data available - processing conversation" - # Extract speakers from transcript segments + # Extract speakers from transcript segments (normalized for comparison) transcript_speakers = set() + transcript_speaker_originals = {} # Keep original names for logging total_segments = 0 identified_segments = 0 for segment in chunk['transcript']: total_segments += 1 if 'identified_as' in segment and segment['identified_as'] and segment['identified_as'] != 'Unknown': - transcript_speakers.add(segment['identified_as']) + original_name = segment['identified_as'] + normalized_name = original_name.strip().lower() + transcript_speakers.add(normalized_name) + transcript_speaker_originals[normalized_name] = original_name identified_segments += 1 if not transcript_speakers: return True, f"No speakers identified in transcript ({identified_segments}/{total_segments} segments) - processing conversation" - # Check if any primary speakers are present - primary_speaker_names = {ps['name'] for ps in user.primary_speakers} - found_primary_speakers = transcript_speakers.intersection(primary_speaker_names) + # Check if any primary speakers are present (normalized comparison) + primary_speaker_names = {ps['name'].strip().lower() for ps in user.primary_speakers} + primary_speaker_originals = {ps['name'].strip().lower(): ps['name'] for ps in user.primary_speakers} + found_primary_speakers_normalized = transcript_speakers.intersection(primary_speaker_names) - if found_primary_speakers: - audio_logger.info(f"✅ Primary speakers found in conversation: {found_primary_speakers} - processing memory") - return True, f"Primary speakers detected: {', '.join(found_primary_speakers)}" + if found_primary_speakers_normalized: + # Convert back to original names for display + found_primary_originals = [primary_speaker_originals[name] for name in found_primary_speakers_normalized] + audio_logger.info(f"✅ Primary speakers found in conversation: {found_primary_originals} - processing memory") + return True, f"Primary speakers detected: {', '.join(found_primary_originals)}" else: - audio_logger.info(f"❌ No primary speakers found - transcript speakers: {transcript_speakers}, primary speakers: {primary_speaker_names} - skipping memory processing") - return False, f"Only non-primary speakers found: {', '.join(transcript_speakers)}" + # Show original names in logs + transcript_originals = [transcript_speaker_originals[name] for name in transcript_speakers] + primary_originals = [primary_speaker_originals[name] for name in primary_speaker_names] + audio_logger.info(f"❌ No primary speakers found - transcript speakers: {transcript_originals}, primary speakers: {primary_originals} - skipping memory processing") + return False, f"Only non-primary speakers found: {', '.join(transcript_originals)}" except Exception as e: # On any error, default to processing (fail-safe) diff --git a/extras/speaker-recognition/setup.sh b/extras/speaker-recognition/setup.sh index 5a996eca4..7c2554352 100755 --- a/extras/speaker-recognition/setup.sh +++ b/extras/speaker-recognition/setup.sh @@ -1,5 +1,10 @@ #!/bin/bash -set -e + +# Enable strict error handling +set -euo pipefail + +# Error trap for better diagnostics +trap 'echo "Error on line $LINENO"; exit 1' ERR # Parse command line arguments ENABLE_HTTPS=false @@ -28,17 +33,36 @@ echo "==============================" # Check if already configured if [ -f ".env" ]; then echo "⚠️ .env already exists. Backing up..." - cp .env .env.backup.$(date +%Y%m%d_%H%M%S) + cp .env ".env.backup.$(date +%Y%m%d_%H%M%S)" +fi + +# Start from template - check existence first +if [ ! -r ".env.template" ]; then + echo "Error: .env.template not found or not readable" >&2 + exit 1 fi -# Start from template -cp .env.template .env +# Copy template and set secure permissions +if ! cp .env.template .env; then + echo "Error: Failed to copy .env.template to .env" >&2 + exit 1 +fi + +# Set restrictive permissions (owner read/write only) +chmod 600 .env # Prompt for required settings echo "" echo "🔑 Hugging Face Token (required for pyannote models)" echo "Get yours from: https://huggingface.co/settings/tokens" -read -p "HF Token: " HF_TOKEN +read -s -r -p "HF Token: " HF_TOKEN +echo # Print newline after silent input + +# Validate token is not empty +if [ -z "$HF_TOKEN" ]; then + echo "Error: HF token is required" >&2 + exit 1 +fi echo "" echo "🖥️ Compute Mode" diff --git a/init.py b/init.py index f3d695bc5..152e3f326 100755 --- a/init.py +++ b/init.py @@ -162,6 +162,20 @@ def run_service_setup(service_name, selected_services, https_enabled=False, serv except FileNotFoundError as e: console.print(f"❌ {service_name} setup failed: {e}") return False + except subprocess.TimeoutExpired as e: + console.print(f"❌ {service_name} setup timed out after {e.timeout} seconds") + if e.stdout: + console.print(f" stdout: {e.stdout.decode()}") + if e.stderr: + console.print(f" stderr: {e.stderr.decode()}") + return False + except subprocess.CalledProcessError as e: + console.print(f"❌ {service_name} setup failed with exit code {e.returncode}") + if e.stdout: + console.print(f" stdout: {e.stdout.decode()}") + if e.stderr: + console.print(f" stderr: {e.stderr.decode()}") + return False except Exception as e: console.print(f"❌ {service_name} setup failed: {e}") return False From 6a442d398e8a4e6740c4fb9ad2f4507cf7622e91 Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Fri, 5 Sep 2025 03:27:40 +0000 Subject: [PATCH 31/35] More fixes based on suggestions, add backup for .env files properly --- backends/advanced/init.py | 4 +- backends/advanced/setup-https.sh | 69 +++++++++--- .../advanced/src/advanced_omi_backend/main.py | 100 +++++++++++++++++- .../webui/src/contexts/AuthContext.tsx | 28 ++++- .../advanced/webui/src/pages/LoginPage.tsx | 27 ++++- extras/openmemory-mcp/setup.sh | 41 +++++-- .../scripts/enroll_speaker.py | 2 +- init.py | 13 ++- services.py | 6 +- 9 files changed, 251 insertions(+), 39 deletions(-) diff --git a/backends/advanced/init.py b/backends/advanced/init.py index bb1099e2c..0b0f2b5f7 100644 --- a/backends/advanced/init.py +++ b/backends/advanced/init.py @@ -305,8 +305,10 @@ def setup_https(self): if ssl_script.exists(): try: # Run from the backend directory so paths work correctly - subprocess.run([str(ssl_script), server_ip], check=True, cwd=str(script_dir)) + subprocess.run([str(ssl_script), server_ip], check=True, cwd=str(script_dir), timeout=180) self.console.print("[green][SUCCESS][/green] SSL certificates generated") + except subprocess.TimeoutExpired: + self.console.print("[yellow][WARNING][/yellow] SSL certificate generation timed out after 3 minutes") except subprocess.CalledProcessError: self.console.print("[yellow][WARNING][/yellow] SSL certificate generation failed") else: diff --git a/backends/advanced/setup-https.sh b/backends/advanced/setup-https.sh index 38f5a5b0d..39fac9b2f 100755 --- a/backends/advanced/setup-https.sh +++ b/backends/advanced/setup-https.sh @@ -36,6 +36,37 @@ print_header() { echo "" } +# Reusable backup helper function +backup_with_timestamp() { + local filepath="$1" + + # Verify the file exists + if [ ! -f "$filepath" ]; then + print_error "Cannot backup '$filepath': file does not exist" + return 1 + fi + + # Generate timestamp (POSIX-safe fallback if needed) + local timestamp + if command -v date >/dev/null 2>&1; then + timestamp=$(date +%Y%m%d_%H%M%S 2>/dev/null) || timestamp=$(date +%Y%m%d_%H%M%S) + else + # POSIX fallback - use current time in seconds since epoch + timestamp="$(date +%s)" + fi + + local backup_path="${filepath}.${timestamp}.backup" + + # Create the backup + if cp "$filepath" "$backup_path"; then + echo "$backup_path" + return 0 + else + print_error "Failed to create backup of '$filepath'" + return 1 + fi +} + # Check if we're in the right directory if [ ! -f "pyproject.toml" ] || [ ! -d "src" ]; then print_error "Please run this script from the backends/advanced directory" @@ -73,11 +104,15 @@ print_header "Step 1: Environment Configuration" if [ -f ".env" ]; then print_info ".env file already exists" if prompt_yes_no "Do you want to update it from template?" "n"; then - cp .env .env.backup - print_info "Backed up existing .env to .env.backup" - cp .env.template .env - print_success ".env created from template" - print_warning "Please edit .env to add your API keys and configuration" + backup_path=$(backup_with_timestamp ".env") + if [ $? -eq 0 ]; then + print_info "Backed up existing .env to $backup_path" + cp .env.template .env + print_success ".env created from template" + print_warning "Please edit .env to add your API keys and configuration" + else + print_error "Failed to backup .env file, aborting update" + fi fi else if [ -f ".env.template" ]; then @@ -102,10 +137,14 @@ print_header "Step 2: Memory Configuration" if [ -f "memory_config.yaml" ]; then print_info "memory_config.yaml already exists" if prompt_yes_no "Do you want to reset it from template?" "n"; then - cp memory_config.yaml memory_config.yaml.backup - print_info "Backed up existing memory_config.yaml to memory_config.yaml.backup" - cp memory_config.yaml.template memory_config.yaml - print_success "memory_config.yaml reset from template" + backup_path=$(backup_with_timestamp "memory_config.yaml") + if [ $? -eq 0 ]; then + print_info "Backed up existing memory_config.yaml to $backup_path" + cp memory_config.yaml.template memory_config.yaml + print_success "memory_config.yaml reset from template" + else + print_error "Failed to backup memory_config.yaml file, aborting reset" + fi fi else if [ -f "memory_config.yaml.template" ]; then @@ -122,10 +161,14 @@ print_header "Step 3: Diarization Configuration" if [ -f "diarization_config.json" ]; then print_info "diarization_config.json already exists" if prompt_yes_no "Do you want to reset it from template?" "n"; then - cp diarization_config.json diarization_config.json.backup - print_info "Backed up existing diarization_config.json to diarization_config.json.backup" - cp diarization_config.json.template diarization_config.json - print_success "diarization_config.json reset from template" + backup_path=$(backup_with_timestamp "diarization_config.json") + if [ $? -eq 0 ]; then + print_info "Backed up existing diarization_config.json to $backup_path" + cp diarization_config.json.template diarization_config.json + print_success "diarization_config.json reset from template" + else + print_error "Failed to backup diarization_config.json file, aborting reset" + fi fi else if [ -f "diarization_config.json.template" ]; then diff --git a/backends/advanced/src/advanced_omi_backend/main.py b/backends/advanced/src/advanced_omi_backend/main.py index 6d4a6a934..ae2433533 100644 --- a/backends/advanced/src/advanced_omi_backend/main.py +++ b/backends/advanced/src/advanced_omi_backend/main.py @@ -28,12 +28,13 @@ # Import Beanie for user management from beanie import init_beanie from dotenv import load_dotenv -from fastapi import FastAPI, Query, WebSocket, WebSocketDisconnect +from fastapi import FastAPI, HTTPException, Query, Request, WebSocket, WebSocketDisconnect from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles from friend_lite.decoder import OmiOpusDecoder from motor.motor_asyncio import AsyncIOMotorClient +from pymongo.errors import ConnectionFailure, PyMongoError from wyoming.audio import AudioChunk from wyoming.client import AsyncTcpClient @@ -379,6 +380,103 @@ async def lifespan(app: FastAPI): allow_headers=["*"], ) + +############################################################################### +# GLOBAL EXCEPTION HANDLERS +############################################################################### + +@app.exception_handler(ConnectionFailure) +@app.exception_handler(PyMongoError) +async def database_exception_handler(request: Request, exc: Exception): + """Handle database connection failures and return structured error response.""" + logger.error(f"Database connection error: {type(exc).__name__}: {exc}") + return JSONResponse( + status_code=500, + content={ + "detail": "Unable to connect to server. Please check your connection and try again.", + "error_type": "connection_failure", + "error_category": "database" + } + ) + + +@app.exception_handler(ConnectionError) +async def connection_exception_handler(request: Request, exc: ConnectionError): + """Handle general connection errors and return structured error response.""" + logger.error(f"Connection error: {exc}") + return JSONResponse( + status_code=500, + content={ + "detail": "Unable to connect to server. Please check your connection and try again.", + "error_type": "connection_failure", + "error_category": "network" + } + ) + + +@app.exception_handler(HTTPException) +async def http_exception_handler(request: Request, exc: HTTPException): + """Handle HTTP exceptions with structured error response.""" + # For authentication failures (401), add error_type + if exc.status_code == 401: + return JSONResponse( + status_code=exc.status_code, + content={ + "detail": exc.detail, + "error_type": "authentication_failure" + }, + headers=getattr(exc, "headers", None), + ) + + # For other HTTP exceptions, return as-is + return JSONResponse( + status_code=exc.status_code, + content={"detail": exc.detail}, + headers=getattr(exc, "headers", None), + ) + + +############################################################################### +# HEALTH CHECK ENDPOINTS +############################################################################### + +@app.get("/api/auth/health") +async def auth_health_check(): + """Pre-flight health check for authentication service connectivity.""" + try: + # Test database connectivity + await mongo_client.admin.command("ping") + + # Test memory service if available + if memory_service: + try: + await asyncio.wait_for(memory_service.test_connection(), timeout=2.0) + memory_status = "ok" + except Exception as e: + logger.warning(f"Memory service health check failed: {e}") + memory_status = "degraded" + else: + memory_status = "unavailable" + + return { + "status": "ok", + "database": "ok", + "memory_service": memory_status, + "timestamp": int(time.time()) + } + except Exception as e: + logger.error(f"Auth health check failed: {e}") + return JSONResponse( + status_code=500, + content={ + "status": "error", + "detail": "Service connectivity check failed", + "error_type": "connection_failure", + "timestamp": int(time.time()) + } + ) + + app.mount("/audio", StaticFiles(directory=CHUNK_DIR), name="audio") # Add authentication routers diff --git a/backends/advanced/webui/src/contexts/AuthContext.tsx b/backends/advanced/webui/src/contexts/AuthContext.tsx index 1811bc341..6515412fb 100644 --- a/backends/advanced/webui/src/contexts/AuthContext.tsx +++ b/backends/advanced/webui/src/contexts/AuthContext.tsx @@ -11,7 +11,7 @@ interface User { interface AuthContextType { user: User | null token: string | null - login: (email: string, password: string) => Promise + login: (email: string, password: string) => Promise<{success: boolean, error?: string, errorType?: string}> logout: () => void isLoading: boolean isAdmin: boolean @@ -58,7 +58,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { initAuth() }, []) - const login = async (email: string, password: string): Promise => { + const login = async (email: string, password: string): Promise<{success: boolean, error?: string, errorType?: string}> => { try { const response = await authApi.login(email, password) @@ -70,10 +70,28 @@ export function AuthProvider({ children }: { children: ReactNode }) { const userResponse = await authApi.getMe() setUser(userResponse.data) - return true - } catch (error) { + return { success: true } + } catch (error: any) { console.error('Login failed:', error) - return false + + // Parse structured error response from backend + let errorMessage = 'Login failed. Please try again.' + let errorType = 'unknown' + + if (error.response?.data) { + const errorData = error.response.data + errorMessage = errorData.detail || errorMessage + errorType = errorData.error_type || errorType + } else if (error.code === 'ERR_NETWORK') { + errorMessage = 'Unable to connect to server. Please check your connection and try again.' + errorType = 'connection_failure' + } + + return { + success: false, + error: errorMessage, + errorType: errorType + } } } diff --git a/backends/advanced/webui/src/pages/LoginPage.tsx b/backends/advanced/webui/src/pages/LoginPage.tsx index 98777717d..c6d6d0d52 100644 --- a/backends/advanced/webui/src/pages/LoginPage.tsx +++ b/backends/advanced/webui/src/pages/LoginPage.tsx @@ -1,6 +1,7 @@ import React, { useState } from 'react' import { Navigate } from 'react-router-dom' import { useAuth } from '../contexts/AuthContext' +import { BACKEND_URL } from '../services/api' import { Music, Eye, EyeOff } from 'lucide-react' export default function LoginPage() { @@ -22,9 +23,29 @@ export default function LoginPage() { setIsLoading(true) setError('') - const success = await login(email, password) - if (!success) { - setError('Invalid email or password') + // Pre-flight connection check + try { + const healthUrl = BACKEND_URL ? `${BACKEND_URL}/api/auth/health` : '/api/auth/health' + const healthResponse = await fetch(healthUrl, { timeout: 5000 }) + if (!healthResponse.ok) { + throw new Error('Health check failed') + } + } catch (healthError) { + setError('Unable to connect to server. Please check your connection and try again.') + setIsLoading(false) + return + } + + const result = await login(email, password) + if (!result.success) { + // Show specific error message based on error type + if (result.errorType === 'connection_failure') { + setError('Unable to connect to server. Please check your connection and try again.') + } else if (result.errorType === 'authentication_failure') { + setError('Invalid email or password') + } else { + setError(result.error || 'Login failed. Please try again.') + } } setIsLoading(false) } diff --git a/extras/openmemory-mcp/setup.sh b/extras/openmemory-mcp/setup.sh index 7f798cc55..38edd3631 100755 --- a/extras/openmemory-mcp/setup.sh +++ b/extras/openmemory-mcp/setup.sh @@ -1,5 +1,7 @@ #!/bin/bash -set -e + +# Enable strict error handling +set -euo pipefail echo "🧠 OpenMemory MCP Setup" echo "======================" @@ -10,8 +12,20 @@ if [ -f ".env" ]; then cp .env .env.backup.$(date +%Y%m%d_%H%M%S) fi -# Start from template -cp .env.template .env +# Start from template - check existence first +if [ ! -r ".env.template" ]; then + echo "Error: .env.template not found or not readable" >&2 + exit 1 +fi + +# Copy template and set secure permissions +if ! cp .env.template .env; then + echo "Error: Failed to copy .env.template to .env" >&2 + exit 1 +fi + +# Set restrictive permissions (owner read/write only) +chmod 600 .env # Clone the custom fork of mem0 with OpenMemory fixes echo "" @@ -31,14 +45,27 @@ cd ../.. echo "✅ Custom mem0 fork ready with OpenMemory improvements" -# Prompt for OpenAI API key +# Prompt for OpenAI API key with validation echo "" echo "🔑 OpenAI API Key (required for memory extraction)" echo "Get yours from: https://platform.openai.com/api-keys" -read -p "OpenAI API Key: " OPENAI_API_KEY +while true; do + read -s -r -p "OpenAI API Key: " OPENAI_API_KEY + echo # Print newline after silent input + if [ -n "$OPENAI_API_KEY" ]; then + break + fi + echo "Error: OpenAI API Key cannot be empty. Please try again." +done -# Update .env file -sed -i "s|OPENAI_API_KEY=.*|OPENAI_API_KEY=$OPENAI_API_KEY|" .env +# Update .env file safely using awk - replace existing line or append if missing +temp_file=$(mktemp) +awk -v key="$OPENAI_API_KEY" ' + /^OPENAI_API_KEY=/ { print "OPENAI_API_KEY=" key; found=1; next } + { print } + END { if (!found) print "OPENAI_API_KEY=" key } +' .env > "$temp_file" +mv "$temp_file" .env echo "" echo "✅ OpenMemory MCP configured!" diff --git a/extras/speaker-recognition/scripts/enroll_speaker.py b/extras/speaker-recognition/scripts/enroll_speaker.py index c18314e33..f73e4634f 100755 --- a/extras/speaker-recognition/scripts/enroll_speaker.py +++ b/extras/speaker-recognition/scripts/enroll_speaker.py @@ -193,7 +193,7 @@ def download_youtube_audio(url: str, start: Optional[float] = None, end: Optiona '-c', 'copy', segment_path, '-y' ] - subprocess.run(cmd, capture_output=True) + subprocess.run(cmd, capture_output=True, check=True, timeout=60) os.unlink(output_path) return segment_path diff --git a/init.py b/init.py index 152e3f326..71ae404ac 100755 --- a/init.py +++ b/init.py @@ -149,15 +149,14 @@ def run_service_setup(service_name, selected_services, https_enabled=False, serv result = subprocess.run( cmd, cwd=service['path'], - check=False + check=True, + capture_output=True, + text=True, + timeout=300 # 5 minute timeout for service setup ) - if result.returncode == 0: - console.print(f"✅ {service_name} setup completed") - return True - else: - console.print(f"❌ {service_name} setup failed (exit code: {result.returncode})") - return False + console.print(f"✅ {service_name} setup completed") + return True except FileNotFoundError as e: console.print(f"❌ {service_name} setup failed: {e}") diff --git a/services.py b/services.py index 088bf95e3..9fcf0bc5f 100755 --- a/services.py +++ b/services.py @@ -132,7 +132,8 @@ def run_compose_command(service_name, command, build=False): cwd=service_path, capture_output=True, text=True, - check=False + check=False, + timeout=120 # 2 minute timeout for service status checks ) if result.returncode == 0: @@ -146,6 +147,9 @@ def run_compose_command(service_name, command, build=False): console.print(f" [dim]{line}[/dim]") return False + except subprocess.TimeoutExpired: + console.print(f"[red]❌ Command timed out after 2 minutes for {service_name}[/red]") + return False except Exception as e: console.print(f"[red]❌ Error running command: {e}[/red]") return False From ffce26400abfe2f9c644f013187efcadef144e19 Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Fri, 5 Sep 2025 03:35:40 +0000 Subject: [PATCH 32/35] fix --- backends/advanced/webui/src/pages/LoginPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backends/advanced/webui/src/pages/LoginPage.tsx b/backends/advanced/webui/src/pages/LoginPage.tsx index c6d6d0d52..717bb61d4 100644 --- a/backends/advanced/webui/src/pages/LoginPage.tsx +++ b/backends/advanced/webui/src/pages/LoginPage.tsx @@ -26,7 +26,7 @@ export default function LoginPage() { // Pre-flight connection check try { const healthUrl = BACKEND_URL ? `${BACKEND_URL}/api/auth/health` : '/api/auth/health' - const healthResponse = await fetch(healthUrl, { timeout: 5000 }) + const healthResponse = await fetch(healthUrl) if (!healthResponse.ok) { throw new Error('Health check failed') } From d3f53f138f8a4cd9ca37b8062249e1b2181d840f Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Fri, 5 Sep 2025 09:14:13 +0530 Subject: [PATCH 33/35] Update Docs/init-system.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- Docs/init-system.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Docs/init-system.md b/Docs/init-system.md index c56781b68..3b24aa6eb 100644 --- a/Docs/init-system.md +++ b/Docs/init-system.md @@ -89,12 +89,13 @@ cd extras/openmemory-mcp When using the orchestrated setup, service URLs are automatically configured: -| Service Selected | Backend Gets Configured With | -|------------------|------------------------------| -| Speaker Recognition | `SPEAKER_SERVICE_URL=http://host.docker.internal:8085` | -| ASR Services | `PARAKEET_ASR_URL=http://host.docker.internal:8767` | +| Service Selected | Backend Gets Configured With | +|----------------------|-------------------------------------------------------------------| +| Speaker Recognition | `SPEAKER_SERVICE_URL=http://host.docker.internal:8085` | +| ASR Services | `PARAKEET_ASR_URL=http://host.docker.internal:8767` | This eliminates the need to manually configure service URLs when running services on the same machine. +Note (Linux): If `host.docker.internal` is unavailable, add `extra_hosts: - "host.docker.internal:host-gateway"` to the relevant services in `docker-compose.yml`. ## Key Benefits From f9ef977bf55b20d849567d846e0215be08729538 Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Fri, 5 Sep 2025 11:23:12 +0530 Subject: [PATCH 34/35] Update Docs/init-system.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- Docs/init-system.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Docs/init-system.md b/Docs/init-system.md index 3b24aa6eb..30eaf6a6d 100644 --- a/Docs/init-system.md +++ b/Docs/init-system.md @@ -7,7 +7,7 @@ Friend-Lite uses a unified initialization system with clean separation of concer - **Configuration** (`init.py`) - Set up service configurations, API keys, and .env files - **Service Management** (`services.py`) - Start, stop, and manage running services -The root orchestrator handles service selection and delegates configuration to individual service scripts. **Setup scripts only configure - they don't start services automatically.** This prevents unnecessary resource usage and gives you control over when services actually run. +The root orchestrator handles service selection and delegates configuration to individual service scripts. In general, setup scripts only configure and do not start services automatically. Exceptions: `extras/asr-services` and `extras/openmemory-mcp` are startup scripts. This prevents unnecessary resource usage and gives you control over when services actually run. ## Architecture From f7101930c0b087cee6cc41ab595bfdd0d8ba07f4 Mon Sep 17 00:00:00 2001 From: Ankush Malaker <43288948+AnkushMalaker@users.noreply.github.com> Date: Fri, 5 Sep 2025 06:04:29 +0000 Subject: [PATCH 35/35] lastfixes --- backends/advanced/init.py | 2 +- .../advanced_omi_backend/memory/providers/vector_stores.py | 3 ++- backends/advanced/webui/src/pages/Memories.tsx | 4 ++-- init.py | 5 +++-- services.py | 3 ++- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/backends/advanced/init.py b/backends/advanced/init.py index 0b0f2b5f7..357764301 100644 --- a/backends/advanced/init.py +++ b/backends/advanced/init.py @@ -126,12 +126,12 @@ def setup_transcription(self): choice = self.prompt_choice("Choose your transcription provider:", choices, "1") if choice == "1": - self.config["TRANSCRIPTION_PROVIDER"] = "deepgram" self.console.print("[blue][INFO][/blue] Deepgram selected") self.console.print("Get your API key from: https://console.deepgram.com/") api_key = self.prompt_value("Deepgram API key (leave empty to skip)", "") if api_key: + self.config["TRANSCRIPTION_PROVIDER"] = "deepgram" self.config["DEEPGRAM_API_KEY"] = api_key self.console.print("[green][SUCCESS][/green] Deepgram configured") else: diff --git a/backends/advanced/src/advanced_omi_backend/memory/providers/vector_stores.py b/backends/advanced/src/advanced_omi_backend/memory/providers/vector_stores.py index 5db7b6e63..a3d041008 100644 --- a/backends/advanced/src/advanced_omi_backend/memory/providers/vector_stores.py +++ b/backends/advanced/src/advanced_omi_backend/memory/providers/vector_stores.py @@ -194,7 +194,8 @@ async def search_memories(self, query_embedding: List[float], user_id: str, limi ) memories.append(memory) # Log similarity scores for debugging - memory_logger.debug(f"Retrieved memory with score {result.score:.3f}: {result.payload.get('content', '')[:50]}...") + score_str = f"{result.score:.3f}" if result.score is not None else "None" + memory_logger.debug(f"Retrieved memory with score {score_str}: {result.payload.get('content', '')[:50]}...") threshold_msg = f"threshold {score_threshold}" if score_threshold > 0.0 else "no threshold" memory_logger.info(f"Found {len(memories)} memories with {threshold_msg} for user {user_id}") diff --git a/backends/advanced/webui/src/pages/Memories.tsx b/backends/advanced/webui/src/pages/Memories.tsx index 0d83b15e2..c99089ccc 100644 --- a/backends/advanced/webui/src/pages/Memories.tsx +++ b/backends/advanced/webui/src/pages/Memories.tsx @@ -472,13 +472,13 @@ export default function Memories() {

)} - {memory.score && isSemanticFilterActive && ( + {memory.score != null && isSemanticFilterActive && (
Relevance: {(memory.score * 100).toFixed(1)}%
)} - {memory.score && !isSemanticFilterActive && ( + {memory.score != null && !isSemanticFilterActive && ( Score: {memory.score.toFixed(3)} diff --git a/init.py b/init.py index 71ae404ac..0bd22f561 100755 --- a/init.py +++ b/init.py @@ -6,10 +6,12 @@ import subprocess import sys +from datetime import datetime from pathlib import Path + +from rich import print as rprint from rich.console import Console from rich.prompt import Confirm -from rich import print as rprint console = Console() @@ -94,7 +96,6 @@ def select_services(): def cleanup_unselected_services(selected_services): """Backup and remove .env files from services that weren't selected""" - from datetime import datetime all_services = list(SERVICES['backend'].keys()) + list(SERVICES['extras'].keys()) diff --git a/services.py b/services.py index 9fcf0bc5f..95966c003 100755 --- a/services.py +++ b/services.py @@ -95,7 +95,8 @@ def run_compose_command(service_name, command, build=False): # Simply stream all output with coloring all_output = [] - assert process.stdout is not None + if process.stdout is None: + raise RuntimeError("Process stdout is None - unable to read command output") for line in process.stdout: line = line.rstrip() if not line: