33800 Docs

← Retour

LifeTracker AI - Architecture Technique

Date : 20/12/2025 Priorite : MOYENNE - FIL ROUGE Status : Draft Projet : Outil de tracking personnel avec IA


1. Vue d'ensemble

┌─────────────────────────────────────────────────────────────────────┐
│                         CLIENTS                                      │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐              │
│  │   App Web    │  │  App Mobile  │  │   Widget     │              │
│  │    (Qwik)    │  │   (PWA)      │  │  Desktop     │              │
│  └──────────────┘  └──────────────┘  └──────────────┘              │
└─────────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│                           API                                        │
│                      Elysia (Bun)                                   │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐  │
│  │ /entries │ │ /media   │ │ /search  │ │/insights │ │/reminders│  │
│  └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘  │
└─────────────────────────────────────────────────────────────────────┘
         │              │              │              │
         ▼              ▼              ▼              ▼
┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐
│  Supabase   │  │   Redis     │  │   Ollama    │  │  Whisper    │
│  PostgreSQL │  │   Cache     │  │  Insights   │  │   Voice     │
│  + pgvector │  │  Sessions   │  │  Summaries  │  │   Notes     │
│  + Storage  │  │             │  │             │  │             │
└─────────────┘  └─────────────┘  └─────────────┘  └─────────────┘

2. Concept

Qu'est-ce qu'une "Entry" ?

Une entry = un moment de vie capturé avec :

Flux utilisateur

1. Capture rapide
   └─> Photo/Texte/Audio → API → Supabase
                              └─> Queue Redis → Ollama (tags auto)
                                             → Whisper (si audio)

2. Consultation
   └─> Timeline chronologique
   └─> Recherche sémantique ("quand ai-je vu X ?")
   └─> Filtres (lieu, date, tags, mood)

3. Insights
   └─> Résumé journalier/hebdo/mensuel (Ollama)
   └─> Patterns détectés (lieux fréquents, activités)
   └─> Suggestions (rappels, habitudes)

3. Stack Technique

Frontend

Techno Usage
Qwik App web + PWA
TailwindCSS Styling
Capacitor Wrapper natif iOS/Android (optionnel)
Leaflet Cartes (visualisation lieux)

Backend

Techno Usage
Elysia (Bun) API REST + WebSocket
Supabase Auth, DB, Storage fichiers
Redis Cache, queue jobs IA

IA

Techno Usage
Ollama (mistral:7b) Résumés, tags auto, insights
pgvector Recherche sémantique
Whisper Notes vocales → texte

4. Schéma Base de Données

-- Extensions
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "vector";
CREATE EXTENSION IF NOT EXISTS "postgis"; -- Pour les coordonnées GPS

-- ============================================
-- UTILISATEUR
-- ============================================

-- Profil utilisateur (extend Supabase auth.users)
CREATE TABLE profiles (
    id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
    display_name VARCHAR(100),
    timezone VARCHAR(50) DEFAULT 'Europe/Paris',
    settings JSONB DEFAULT '{}',
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- ============================================
-- ENTRIES (MOMENTS)
-- ============================================

CREATE TABLE entries (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID REFERENCES profiles(id) ON DELETE CASCADE,

    -- Contenu
    content TEXT, -- Texte principal
    content_type VARCHAR(20) DEFAULT 'text', -- text, photo, video, audio, mixed

    -- Localisation
    location GEOGRAPHY(POINT, 4326), -- Coordonnées GPS
    location_name VARCHAR(300), -- Nom du lieu (reverse geocoding)
    location_address TEXT,

    -- Métadonnées
    captured_at TIMESTAMPTZ NOT NULL, -- Moment de capture
    timezone VARCHAR(50),

    -- État émotionnel (optionnel)
    mood VARCHAR(20), -- great, good, neutral, bad, terrible
    energy INTEGER CHECK (energy BETWEEN 1 AND 5),

    -- Tags et catégories
    tags TEXT[] DEFAULT '{}',
    category VARCHAR(50), -- work, personal, health, social, travel, etc
    auto_tags TEXT[] DEFAULT '{}', -- Tags générés par IA

    -- IA
    embedding vector(384),
    ai_summary TEXT, -- Résumé court généré
    ai_processed BOOLEAN DEFAULT false,

    -- Métadonnées système
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Index
CREATE INDEX idx_entries_user ON entries(user_id);
CREATE INDEX idx_entries_captured ON entries(captured_at DESC);
CREATE INDEX idx_entries_location ON entries USING GIST(location);
CREATE INDEX idx_entries_embedding ON entries USING ivfflat (embedding vector_cosine_ops);
CREATE INDEX idx_entries_tags ON entries USING GIN(tags);

-- ============================================
-- MÉDIAS
-- ============================================

CREATE TABLE media (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    entry_id UUID REFERENCES entries(id) ON DELETE CASCADE,
    user_id UUID REFERENCES profiles(id) ON DELETE CASCADE,

    -- Fichier
    file_path TEXT NOT NULL, -- Chemin Supabase Storage
    file_type VARCHAR(20), -- image, video, audio
    mime_type VARCHAR(100),
    file_size INTEGER, -- bytes

    -- Métadonnées image/vidéo
    width INTEGER,
    height INTEGER,
    duration INTEGER, -- secondes pour audio/vidéo

    -- Thumbnail
    thumbnail_path TEXT,

    -- Transcription (audio/vidéo)
    transcription TEXT,
    transcription_status VARCHAR(20) DEFAULT 'pending', -- pending, processing, done, failed

    -- EXIF data (photos)
    exif_data JSONB,

    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_media_entry ON media(entry_id);

-- ============================================
-- LIEUX FAVORIS
-- ============================================

CREATE TABLE places (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID REFERENCES profiles(id) ON DELETE CASCADE,

    name VARCHAR(200) NOT NULL,
    location GEOGRAPHY(POINT, 4326),
    radius INTEGER DEFAULT 100, -- mètres
    category VARCHAR(50), -- home, work, gym, etc
    icon VARCHAR(50),

    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- ============================================
-- RAPPELS / REMINDERS
-- ============================================

CREATE TABLE reminders (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID REFERENCES profiles(id) ON DELETE CASCADE,

    title VARCHAR(300) NOT NULL,
    description TEXT,

    -- Déclencheur
    trigger_type VARCHAR(20), -- time, location, recurring
    trigger_time TIMESTAMPTZ,
    trigger_location UUID REFERENCES places(id),
    recurrence_rule TEXT, -- RRULE format (iCal)

    -- Lien avec entry
    related_entry_id UUID REFERENCES entries(id),

    -- Status
    active BOOLEAN DEFAULT true,
    completed_at TIMESTAMPTZ,

    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- ============================================
-- INSIGHTS / RÉSUMÉS
-- ============================================

CREATE TABLE insights (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID REFERENCES profiles(id) ON DELETE CASCADE,

    period_type VARCHAR(20), -- daily, weekly, monthly, yearly
    period_start DATE,
    period_end DATE,

    -- Contenu généré par IA
    summary TEXT,
    highlights TEXT[], -- Points marquants
    stats JSONB, -- { entries_count, photos_count, places_visited, etc }

    -- Patterns détectés
    patterns JSONB, -- { frequent_places: [], common_activities: [], mood_trends: {} }

    generated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE UNIQUE INDEX idx_insights_period ON insights(user_id, period_type, period_start);

-- ============================================
-- JOBS QUEUE
-- ============================================

CREATE TABLE jobs (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    type VARCHAR(50), -- transcribe, embed, summarize, insight
    payload JSONB,
    status VARCHAR(20) DEFAULT 'pending',
    attempts INTEGER DEFAULT 0,
    error TEXT,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    started_at TIMESTAMPTZ,
    completed_at TIMESTAMPTZ
);

CREATE INDEX idx_jobs_pending ON jobs(status, created_at) WHERE status = 'pending';

5. API Endpoints

Entries

POST   /api/entries                    # Créer une entry
GET    /api/entries                    # Liste (pagination, filtres)
GET    /api/entries/:id                # Détail
PATCH  /api/entries/:id                # Modifier
DELETE /api/entries/:id                # Supprimer

GET    /api/entries/timeline           # Timeline groupée par jour
GET    /api/entries/map                # Entries avec coordonnées

Media

POST   /api/media/upload               # Upload fichier
GET    /api/media/:id                  # Télécharger
GET    /api/media/:id/thumbnail        # Thumbnail
DELETE /api/media/:id                  # Supprimer

Recherche

POST   /api/search                     # Recherche sémantique
       Body: { "query": "restaurant italien avec Marie" }

GET    /api/search/suggestions         # Suggestions autocomplétion

Insights

GET    /api/insights/today             # Résumé du jour
GET    /api/insights/week              # Résumé semaine
GET    /api/insights/month/:year/:month
GET    /api/insights/patterns          # Patterns détectés

Reminders

POST   /api/reminders                  # Créer
GET    /api/reminders                  # Liste
PATCH  /api/reminders/:id              # Modifier
DELETE /api/reminders/:id              # Supprimer
POST   /api/reminders/:id/complete     # Marquer terminé

Voice

POST   /api/voice/transcribe           # Transcrire audio
POST   /api/voice/command              # Commande vocale (créer entry par voix)

6. Workers IA

Worker Transcription

// /workers/transcribe.ts
async function processTranscription(mediaId: string) {
    const media = await supabase.from('media').select('*').eq('id', mediaId).single();

    if (!media.data) return;

    // Télécharger le fichier audio
    const { data: fileData } = await supabase.storage
        .from('media')
        .download(media.data.file_path);

    // Envoyer à Whisper
    const formData = new FormData();
    formData.append('audio_file', fileData);
    formData.append('language', 'fr');

    const response = await fetch('http://192.168.1.11:9100/asr', {
        method: 'POST',
        body: formData
    });

    const result = await response.json();

    // Mettre à jour
    await supabase.from('media').update({
        transcription: result.text,
        transcription_status: 'done'
    }).eq('id', mediaId);

    // Mettre à jour l'entry avec le texte
    await supabase.from('entries').update({
        content: result.text
    }).eq('id', media.data.entry_id);
}

Worker Embeddings

// /workers/embed.ts
async function generateEmbedding(entryId: string) {
    const entry = await supabase.from('entries').select('*').eq('id', entryId).single();

    if (!entry.data?.content) return;

    // Générer embedding via Ollama
    const response = await fetch('http://192.168.1.30:11434/api/embeddings', {
        method: 'POST',
        body: JSON.stringify({
            model: 'nomic-embed-text',
            prompt: entry.data.content
        })
    });

    const { embedding } = await response.json();

    await supabase.from('entries').update({
        embedding: embedding,
        ai_processed: true
    }).eq('id', entryId);
}

Worker Auto-Tags

// /workers/auto-tag.ts
async function generateAutoTags(entryId: string) {
    const entry = await supabase.from('entries').select('*').eq('id', entryId).single();

    const prompt = `
    Analyse ce texte et génère des tags pertinents (max 5).
    Texte: "${entry.data.content}"

    Réponds uniquement avec un JSON: {"tags": ["tag1", "tag2"], "category": "work|personal|health|social|travel|other"}
    `;

    const response = await fetch('http://192.168.1.30:11434/api/generate', {
        method: 'POST',
        body: JSON.stringify({ model: 'mistral:7b', prompt })
    });

    const result = JSON.parse(await response.text());

    await supabase.from('entries').update({
        auto_tags: result.tags,
        category: result.category
    }).eq('id', entryId);
}

Worker Daily Insight

// /workers/daily-insight.ts (cron: tous les jours à 23h)
async function generateDailyInsight(userId: string, date: Date) {
    const entries = await supabase
        .from('entries')
        .select('*')
        .eq('user_id', userId)
        .gte('captured_at', startOfDay(date))
        .lte('captured_at', endOfDay(date));

    if (entries.data.length === 0) return;

    const prompt = `
    Résume cette journée en 2-3 phrases:

    ${entries.data.map(e => `- ${e.captured_at}: ${e.content}`).join('\n')}

    Identifie aussi 1-3 moments marquants.
    Format JSON: {"summary": "...", "highlights": ["...", "..."]}
    `;

    const response = await fetch('http://192.168.1.30:11434/api/generate', {
        method: 'POST',
        body: JSON.stringify({ model: 'mistral:7b', prompt })
    });

    const result = JSON.parse(await response.text());

    await supabase.from('insights').upsert({
        user_id: userId,
        period_type: 'daily',
        period_start: date,
        period_end: date,
        summary: result.summary,
        highlights: result.highlights,
        stats: {
            entries_count: entries.data.length,
            photos_count: entries.data.filter(e => e.content_type === 'photo').length,
            // etc
        }
    });
}

7. Interface Utilisateur

Écrans principaux

┌─────────────────────────────────────────┐
│  LifeTracker                    [+] [🔍]│
├─────────────────────────────────────────┤
│                                         │
│  📅 Aujourd'hui - 20 décembre 2025      │
│  ─────────────────────────────────────  │
│                                         │
│  09:15  ☕ Café au bureau               │
│         📍 WeWork Paris                 │
│                                         │
│  12:30  🍕 [Photo]                      │
│         Déjeuner avec l'équipe          │
│         📍 Pizzeria Roma                │
│                                         │
│  15:00  💻 Réunion projet X             │
│         #work #meeting                  │
│                                         │
│  19:00  🏃 Running 5km                  │
│         📍 Parc Monceau                 │
│         😊 Great                        │
│                                         │
├─────────────────────────────────────────┤
│  [Timeline] [Map] [Search] [Insights]   │
└─────────────────────────────────────────┘

Quick Capture (FAB)

┌─────────────────────────────────────────┐
│                                         │
│    ┌─────┐  ┌─────┐  ┌─────┐  ┌─────┐  │
│    │ 📝  │  │ 📷  │  │ 🎤  │  │ 📍  │  │
│    │Text │  │Photo│  │Voice│  │Place│  │
│    └─────┘  └─────┘  └─────┘  └─────┘  │
│                                         │
│              [ ✕ Close ]                │
└─────────────────────────────────────────┘

8. MVP - Phases

Phase 1 : Core (2 semaines)

Phase 2 : Media (2 semaines)

Phase 3 : IA (2 semaines)

Phase 4 : Insights (1-2 semaines)

Phase 5 : Polish (1 semaine)


9. Déploiement

# docker-compose.yml
services:
  lifetracker-app:
    build: ./app
    ports:
      - "4700:3000"
    environment:
      - SUPABASE_URL=${SUPABASE_PROD_URL}
      - SUPABASE_ANON_KEY=${SUPABASE_PROD_ANON_KEY}

  lifetracker-api:
    build: ./api
    ports:
      - "4701:3001"
    environment:
      - SUPABASE_URL=${SUPABASE_PROD_URL}
      - SUPABASE_KEY=${SUPABASE_PROD_SERVICE_ROLE_KEY}
      - REDIS_URL=redis://prod-redis.local:6379
      - OLLAMA_URL=http://192.168.1.30:11434
      - WHISPER_URL=http://192.168.1.11:9100

  lifetracker-workers:
    build: ./workers
    environment:
      - SUPABASE_URL=${SUPABASE_PROD_URL}
      - SUPABASE_KEY=${SUPABASE_PROD_SERVICE_ROLE_KEY}

URLs

Service URL
App https://lifetracker.33800.nowhere84.com
API https://lifetracker-api.33800.nowhere84.com