Memora — diario y crónica de vida asistidos por IA
Una app para escribir tu vida (o la de quien quieras) sin tener que escribirla. Hablas, subes una foto o reenvías un audio de WhatsApp. Memora transcribe, ordena por fecha y por personas, deja que invites a coautores y testigos, y un asistente conversacional ("Ámbar") busca, calcula edades, propone ediciones y guarda recuerdos por ti — siempre con tu permiso.
Producción: https://memora.cadences.app · Stack: Astro + React + Cloudflare Pages + D1 + R2 + Workers AI + Vectorize + DeepSeek
1. ¿Qué problema resuelve?
Casi nadie escribe un diario con constancia. Y casi nadie va a pasarse seis meses haciéndole biografía a su abuela. El esfuerzo de redactar, ordenar y enriquecer un recuerdo es lo que mata la práctica.
Memora ataca eso por tres flancos:
- Captura sin fricción: voz, foto, audio reenviado de WhatsApp, conversación con un entrevistador IA. Si lo dices, queda guardado.
- Estructura emergente: el sistema infiere fechas, lugares, personas, capítulos, periodos vitales. No te pide formularios; te enseña lo que ya tienes y te deja afinarlo.
- Memoria colaborativa: invitas a tu hermana, a tu padre, a un amigo. Cada uno aporta su versión. Memora muestra el efecto Rashomon: un mismo evento contado por varias voces.
Hay dos modos coexistentes en el mismo libro:
- Diario — entradas cortas, hoy, día a día.
- Crónica — sesiones largas de voz para contar una época entera (la mili, el primer trabajo, un viaje). Se trocean automáticamente en entradas atómicas con sus fechas.
2. Mapa funcional
2.1 Onboarding y libros
- Login con Google (Identity Services + verificación JWKS server-side, sin SDKs de terceros).
- Cada usuario tiene uno o varios libros (proyectos): "Mi vida", "Vida de mamá", "Familia López".
- Por cada libro: rol del usuario (
owner/coauthor/reader/witness), nivel de privacidad por defecto, personalidad del libro (afecta al tono del entrevistador IA). - Onboarding guiado con tour interactivo y banner persistente hasta completar perfil + primer recuerdo.
2.2 Capturar un recuerdo
| Composer | Para qué |
|---|---|
| Diario | texto libre + adjuntos (fotos, vídeos, audios). Botón "Audio" dedicado: subes un .opus/.m4a/.mp3/.ogg/.wav y Memora lo transcribe con Whisper y mete el texto. |
| Crónica | grabación larga partida en chunks de 60s, transcripción incremental, "Subir audio existente" para importar de WhatsApp y dejar que el motor de splitting genere N entries con fechas inferidas. |
| Entrevistador IA | panel conversacional que te pregunta sobre tu vida, escucha y ofrece guardar la respuesta como entrada con un click. |
Cada entrada lleva:
body_md(markdown),body_plain(para FTS).occurred_on+occurred_precision(exact|day|month|year|era).location_textopcional + lat/lng (geocodificable).intimacy_level:private(solo tú) →intimate(coautores) →circle(lectores invitados) →public.source:manual,voice,photo,chronicle_session,witness_response,assistant_recall.
2.3 Estructura del libro
- Periodos / Eras: bloques de tiempo aproximado ("Infancia", "Universidad"). Pueden ser exactos o difusos.
- Capítulos: agrupación temática u ordenada manualmente con
position. Cada capítulo tiene su propio nivel de intimidad por defecto. - Personas: extraídas y vinculadas a entradas (
memora_entry_person). Tienen alias y relación. Galería de personas con menciones cruzadas. - Timeline: vista cronológica con badges de fuente, fecha y nivel de intimidad.
- Mapa: las entradas geocodificadas se ven en una capa Leaflet del libro.
- Hoy: feed de entradas capturadas en el día.
2.4 Coautoría y testigos
- Invitaciones por email o enlace mágico a coautor/lector. El coautor aporta sus propias entradas; los
privateajenos no son visibles para nadie más. - Testigos externos: una persona sin cuenta recibe un link
/w/[token](firmado HMAC) con un prompt personalizado: "Cuéntanos cómo conociste a Marta". Su respuesta crea una entrada consource='witness_response'y autor "testigo externo". El testigo nunca ve el resto del libro. - Los testigos pueden estar acotados a un capítulo concreto (mig 0017).
2.5 Vistas avanzadas
- Rashomon: al filtrar por una persona, panel que agrupa eventos donde aparecen varias voces contando lo mismo (mismo periodo o mismo año). Cada voz se renderiza con un color estable derivado del
author_user_id. - Suggestions / Nudges: el sistema detecta huecos ("hace 4 semanas que no escribes de los 80s"), entradas sin fecha, sin persona, sin capítulo, y los muestra como tarjetas "Ámbar te propone…".
- Clarify panel: cuando una entrada queda ambigua (sin fecha, persona dudosa), Ámbar abre un panel con preguntas concretas.
2.6 Buscador
- Barra de búsqueda con autocompletado de personas y filtros por rango de años.
- Detrás: híbrido BM25 (FTS5 sobre
body_plain) + denso (Vectorize, embeddingsbge-m31024d, cosine) + estructurado (filtros por fecha, persona, periodo) + fusión RRF.
2.7 Ámbar — el asistente conversacional
Drawer lateral (⌘K) con scope "este libro" o "todos mis libros".
Lectura (10 tools):
search_memories, get_entry_full, compute_age, list_persons, list_periods, count_in_period, dates_around, timeline_summary, find_orphans, get_person_entries.
Mutaciones (modelo propose + confirm, 6 tools):
propose_edit_entry, propose_set_entry_period, propose_rename_person, propose_merge_persons, propose_create_entry, propose_delete_entry.
Ámbar no toca la base de datos directamente: cada propuesta se persiste en memora_chat_pending_action (estado pending) y aparece como tarjeta amarilla "Ámbar propone" con botones Aplicar / Descartar / Deshacer. El historial vive en una pestaña "Actividad de Ámbar".
Ejemplos:
- "¿Qué edad tenía cuando conocí a Marta?" →
search_memories+get_entry_full+compute_age. Responde con cita[#ent_abc]. - "Cambia la fecha de ese recuerdo a julio del 89" →
propose_edit_entry({occurredOn:'1989-07-01', occurredPrecision:'month'}). - "Guarda esto: hoy he ido al puerto con mi padre" →
propose_create_entry. Lo escribe en primera persona, lo etiquetaassistant_recall, te pide confirmar. - "Lola y Lola Pérez son la misma" →
propose_merge_persons(solo owner; reasigna entries y absorbe alias).
2.8 Privacidad por defecto
Toda entrada nace private salvo que el capítulo o el libro hayan establecido otro default. Las búsquedas y tools respetan el rol:
reader: vepublicycircle.coauthor: vepublic,circle,intimatey sus propiasprivate.witness: solo ve el prompt para el que fue invitado.owner: ve todo del libro.
3. Arquitectura técnica
3.1 Edge-only en Cloudflare
Browser ──┬── Astro (SSG + islas React)
│
└── /api/memora/** (Cloudflare Pages Functions, V8 isolates)
│
├── D1 (SQLite, 18 migraciones)
├── R2 (audio + foto + vídeo)
├── Vectorize (1024d cosine, índice memora-entries)
├── Workers AI (Whisper, bge-m3, Llama fallback)
├── DeepSeek Chat v3 (LLM principal con tool calling)
└── Groq (fallback / TTS futuro)
- 0 servidores, 0 procesos largos. Todo cold-start de ~5ms.
- Coste base: cero. Solo se paga DeepSeek (≈ $0.27/M tokens input) y Workers AI más allá del free tier.
3.2 Esquema de datos (D1)
18 migraciones; las tablas principales:
memora_user,memora_user_profile— cuenta + datos biográficos (fecha de nacimiento, etc.).memora_project,memora_project_member,memora_project_personality— libro + roles + tono del entrevistador.memora_period,memora_chapter,memora_chapter_entry— eras y capítulos.memora_entry— corazón del modelo. CHECKs estrictos enintimacy_level,source,status,occurred_precision,author_type.embedding_dirtypara reembeber.memora_entry_person,memora_person— personas + menciones + alias JSON.memora_asset— fotos / audios / vídeos en R2, conentry_idopcional (los huérfanos se vinculan al guardar).memora_chronicle_session,memora_chronicle_chunk— sesiones largas de voz y sus chunks transcritos.memora_witness,memora_invitation— testigos externos (HMAC token) y coautores invitados.memora_chat_session,memora_chat_message,memora_chat_pending_action— historial de Ámbar y propuestas pendientes.memora_assistant_nudge— sugerencias proactivas.memora_embedding_job,memora_search_cache,memora_geocode_cache— colas y cachés.- FTS5 virtual table sobre
body_plainpara BM25.
3.3 Pipeline de retrieval híbrido
src/lib/retrieval/hybridSearch() es la única puerta:
parseQuery— extrae año(s), persona(s) conocida(s), límite.- Cache lookup (
memora_search_cache, TTL configurable). - 3 canales en paralelo:
- Dense: embedding del query con
@cf/baai/bge-m3→ top-k de Vectorize. - Sparse: FTS5 con BM25 sobre
body_plain. - Structured: SQL con filtros de fecha/persona/periodo.
- Dense: embedding del query con
- RRF fusion (Reciprocal Rank Fusion) con pesos por canal.
- Visibility filter según rol del usuario.
- Hidratación: añade
occurred_on,intimacy_level, snippet recortado. - Cache write.
3.4 Pipeline de escritura
Cada INSERT en memora_entry marca embedding_dirty=1 y crea/actualiza un memora_embedding_job. Un endpoint /api/memora/jobs/embeddings/drain (protegido por CRON_SECRET, llamado desde un Worker o cron externo) procesa la cola en lotes:
- Recoge
body_plain. - Lo embebe con
bge-m3. - Hace
upserten Vectorize con metadata mínima (project, year, intimacy). - Marca
embedding_dirty=0y guarda hash de contenido.
El borrado/archivado limpia Vectorize por id (deleteByIds).
3.5 Pipeline de voz
- Diario / Audio: front lee el archivo, lo manda a
/api/memora/stt(Whisper en Workers AI), recibe texto y lo inserta en el composer + sube el original a R2 (/api/memora/assets/upload) como adjunto. - Crónica: grabación en chunks de 60s con
MediaRecorder. Cada chunk sube a R2 + se transcribe en background. Al cerrar la sesión, un splitter LLM convierte la transcripción en N entries con fechas inferidas y los une porchronicle_session_id.
3.6 Ámbar — function calling con DeepSeek
POST /api/memora/chat/stream— turno conversacional con SSE.- System prompt minimalista (calidez, español, sin emojis, citas con
[#entryId], tools obligatorias para hechos). - Loop con DeepSeek Chat v3, hasta N pasos de tool calling.
- Cada
tool_callse loggea como evento SSE para que el front lo muestre en vivo ("Buscando recuerdos…"). - Las mutaciones se separan por convención de nombre
propose_*y se enrutan arunMutationTool. El resultado llevaconfirmation_required:true,action_id,summaryy un diffbefore/changesque el front pinta como tarjeta de propuesta. applyActionejecuta la mutación real.undoActionconsume elbefore_snapshot(excepto paramerge_persons, que es destructiva).
3.7 Seguridad
- Auth: Google ID Token verificado server-side contra JWKS (RSASSA-PKCS1-v1_5 con WebCrypto), sin librerías; emisión de JWT de sesión con
JWT_SECRETcadences-wide. - Tokens de testigo: HMAC-SHA256 firmado con
WITNESS_TOKEN_SECRET, expira y revocable. - Permisos: helper
resolveRoleInProjectse llama en cada endpoint mutador. Coautor solo puede mutar entradas suyas vía Ámbar;merge_personssolo lo puede hacer el owner. - Prompt injection: el system prompt instruye explícitamente a Ámbar a tratar los recuerdos como contenido, no como instrucciones.
- CORS y middleware de inyección de sesión en
/functions/_middleware.ts. - Vectorize y D1 filtran por
project_idantes de intersectar con resultados de embeddings (evita filtrado solo en el modelo).
3.8 Frontend
- Astro 4 con islas React 18 hidratadas selectivamente (rendimiento de SPA solo donde toca).
- TailwindCSS con paleta propia (
inkcálidos,embernaranja-rojo,paper-creampara fondos tipo cuaderno). - lucide-react para iconografía.
- Modales y paneles unificados con clase
.card(look papel + sombra "sandstone"). - Drawer chat con scope toggle, atajos teclado, scroll anclado, citas clicables tipo
#ent_abc.
4. Algunas decisiones interesantes
- Soft-delete reversible desde Ámbar (
status='archived'+ saca de Vectorize), borrado físico solo desde la UI a propósito. La IA no destruye nada por accidente. occurred_precisioncon 5 niveles (incluidoerapara "los años 80"). Permite mostrar fechas borrosas con honestidad en vez de inventar día concreto.- Modelo "propose + confirm" en vez de auto-apply. Construye confianza al usuario y permite explicar el cambio antes de hacerlo. Coste: un click más; beneficio: cero arrepentimiento.
- Rashomon view invertida tras feedback: ahora por defecto enseña todos los recuerdos de una persona, y un toggle "Solo multi-voz" filtra los compartidos. Antes salía vacío y desconcertaba.
- Coexistencia Diario + Crónica sin obligar a elegir. Algunos usuarios escriben hoy en Diario y, una tarde de domingo, abren una sesión de Crónica para una época entera.
- Audio de WhatsApp como ciudadano de primera. La fricción real para un usuario mayor no es escribir: es que ya tiene su vida en notas de voz reenviadas. Memora se las traga y las convierte en libro.
- Stack 100% serverless en Cloudflare: el coste marginal de un usuario más es prácticamente cero hasta volúmenes considerables. El TTV (time-to-value) técnico también: deploy completo en ~30s con
wrangler pages deploy.
5. Lo que viene
- Audiolibro: TTS por capítulo (Workers AI Aura → Groq fallback) con voz consistente.
- Perspectiva Studio: análisis cualitativo profundo (varios pases LLM, comparativas históricas) sin contaminar la UX del chat diario.
- Cron de asistencia: Ámbar te recuerda los domingos qué época has dejado callada, qué personas no aparecen desde hace meses, qué recuerdos quedan sin fecha.
- Generación de "ediciones": PDF con portada, capítulos y fotos, lista para imprimir.
- Voz reconocida del autor: tono y muletillas se infieren de las primeras horas grabadas y el entrevistador adapta su pregunta.
6. Para el artículo de LinkedIn — ángulos posibles
- "Construí una app que escribe la biografía de mi madre mientras ella habla" — historia personal, demo en vídeo, foco en captura de voz + Whisper.
- "Edge-only AI: 0 servidores, 6 servicios de Cloudflare, una app real" — técnico, foco en el pipeline híbrido y en el coste marginal cero.
- "Por qué mi asistente de IA NO escribe en la base de datos" — el patrón propose + confirm como diseño de producto, no como limitación técnica.
- "Rashomon view: cuando varias memorias cuentan el mismo evento" — ángulo narrativo + UX, con capturas.
- "Diario + Crónica: dos modos, un solo libro" — diferenciación frente a apps tipo Day One.
Cualquiera de los cinco encaja con un mismo CTA al final: "Si quieres probarlo, está en memora.cadences.app."