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 :
- Quand : timestamp automatique
- Où : coordonnées GPS (optionnel)
- Quoi : texte, photo, vidéo, audio
- Tags : catégorisation manuelle ou IA
- Mood : état émotionnel (optionnel)
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)
- [ ] Setup Qwik PWA + Elysia
- [ ] Auth Supabase
- [ ] CRUD entries (texte uniquement)
- [ ] Timeline basique
- [ ] Géolocalisation
Phase 2 : Media (2 semaines)
- [ ] Upload photos
- [ ] Upload audio + transcription Whisper
- [ ] Galerie
- [ ] Thumbnails
Phase 3 : IA (2 semaines)
- [ ] Auto-tags Ollama
- [ ] Embeddings pgvector
- [ ] Recherche sémantique
- [ ] Résumés journaliers
Phase 4 : Insights (1-2 semaines)
- [ ] Dashboard insights
- [ ] Patterns
- [ ] Statistiques
- [ ] Export données
Phase 5 : Polish (1 semaine)
- [ ] Notifications
- [ ] Mode offline (PWA)
- [ ] Widgets
- [ ] Thème sombre
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