Arquitectura de Espejo Automatizado con Google Apps Script para procesar Big Data sin límites
NotebookLM tiene un límite de procesamiento: archivos con más de 500,000 palabras o Sheets con demasiadas filas simplemente no cargan, se bloquean o producen análisis incompletos.
Esto es un problema real para empresas con reportes de millones de transacciones, inventarios de miles de productos, logs de sistemas, o bases de datos de clientes. Y para autores con archivos de investigación masivos — notas de años, bibliografías enormes, transcripciones de entrevistas.
La solución incorrecta es intentar subir el archivo completo y esperar que funcione. La solución correcta es no subir el archivo grande nunca.
La solución es crear un sistema de tres capas donde NotebookLM nunca ve el archivo grande. Solo ve un archivo pequeño, limpio y actualizado automáticamente que contiene exactamente lo que necesita para responder bien.
Como un ingeniero, no buscamos que la IA lea más, sino que lea mejor. Filtramos el ruido para potenciar la señal.
Este script va en tu archivo grande (Google Sheets) y se ejecuta automáticamente cada hora. Lee las hojas que tú definas, extrae las últimas 500 filas de cada una, y las escribe en el archivo pequeño que NotebookLM tiene conectado.
// ═══════════════════════════════════════════════════════════ // SINCRONIZADOR PARA NOTEBOOKLM — por Yoniliman Galvis // Arquitectura de Espejo Automatizado // ═══════════════════════════════════════════════════════════ // ── CONFIGURACIÓN (solo edita esta sección) ────────────── var CONFIG = { // ID del Google Sheet PEQUEÑO donde NotebookLM se conecta // Lo encuentras en la URL: docs.google.com/spreadsheets/d/ESTE_ID/edit idDestino: "PEGA_AQUI_EL_ID_DEL_ARCHIVO_PEQUEÑO", // Hojas del archivo GRANDE que quieres sincronizar hojasAProcesar: ["Ventas", "Inventario", "Logs"], // Máximo de filas recientes por hoja (500 es seguro para NotebookLM) maxFilasPorHoja: 500, // ¿Incluir resumen estadístico? (recomendado para datos numéricos) incluirResumen: true }; // ───────────────────────────────────────────────────────── function sincronizarParaNotebookLM() { var ssOrigen = SpreadsheetApp.getActiveSpreadsheet(); var ssDestino = SpreadsheetApp.openById(CONFIG.idDestino); var hojaDestino = ssDestino.getSheets()[0]; var datosFinales = []; var fechaSync = new Date().toLocaleString("es-CO"); // Encabezado con metadata de sincronización datosFinales.push([ "=== CONTEXTO PARA NOTEBOOKLM ===", "Sincronizado: " + fechaSync, "Archivo: " + ssOrigen.getName() ]); CONFIG.hojasAProcesar.forEach(function(nombreHoja) { var hoja = ssOrigen.getSheetByName(nombreHoja); if (!hoja) { console.log("Hoja no encontrada: " + nombreHoja); return; } var ultimaFila = hoja.getLastRow(); var ultimaCol = hoja.getLastColumn(); if (ultimaFila < 2) return; // Hoja vacía var numFilas = Math.min(ultimaFila - 1, CONFIG.maxFilasPorHoja); var filaInicio = ultimaFila - numFilas + 1; // Encabezados de la hoja var encabezados = hoja.getRange(1, 1, 1, ultimaCol).getValues()[0]; // Datos recientes var datos = hoja.getRange(filaInicio, 1, numFilas, ultimaCol).getValues(); // Separador de sección datosFinales.push(["--- HOJA: " + nombreHoja + " | Filas: " + numFilas + " de " + (ultimaFila - 1) + " ---"]); datosFinales.push(encabezados); datosFinales = datosFinales.concat(datos); // Resumen estadístico (suma y promedio de columnas numéricas) if (CONFIG.incluirResumen) { var resumen = ["RESUMEN " + nombreHoja + ":"]; encabezados.forEach(function(col, i) { var vals = datos.map(function(r) { return parseFloat(r[i]); }).filter(function(v) { return !isNaN(v); }); if (vals.length > 0) { var suma = vals.reduce(function(a, b) { return a + b; }, 0); var prom = (suma / vals.length).toFixed(2); resumen.push(col + ": suma=" + suma.toFixed(2) + " | promedio=" + prom); } }); datosFinales.push(resumen); } }); // Escritura segura (evita error si datosFinales está vacío) hojaDestino.clear(); if (datosFinales.length > 0 && datosFinales[0].length > 0) { var maxCols = datosFinales.reduce(function(max, row) { return Math.max(max, row.length); }, 0); // Normaliza todas las filas al mismo ancho var datosNorm = datosFinales.map(function(row) { while (row.length < maxCols) row.push(""); return row; }); hojaDestino.getRange(1, 1, datosNorm.length, maxCols).setValues(datosNorm); } console.log("✓ Sincronización completada: " + fechaSync); } // ── CREAR EL DISPARADOR AUTOMÁTICO ──────────────────────── // Ejecuta esta función UNA SOLA VEZ desde el menú Run // Después el trigger se activa solo cada hora function crearTriggerHorario() { // Elimina triggers existentes para evitar duplicados ScriptApp.getProjectTriggers().forEach(function(t) { ScriptApp.deleteTrigger(t); }); // Crea el nuevo trigger ScriptApp.newTrigger('sincronizarParaNotebookLM') .timeBased() .everyHours(1) .create(); console.log("✓ Trigger creado: sincronización cada hora"); } // ── VARIANTE: RESUMEN ESTADÍSTICO PURO ──────────────────── // Úsala cuando los datos son demasiados incluso filtrados // Envía solo totales y promedios — ideal para gerencia function sincronizarSoloResumen() { var ssOrigen = SpreadsheetApp.getActiveSpreadsheet(); var ssDestino = SpreadsheetApp.openById(CONFIG.idDestino); var hojaDestino = ssDestino.getSheets()[0]; var datosFinales = [["Hoja", "Métrica", "Valor", "Período"]]; var fecha = new Date().toLocaleDateString("es-CO"); CONFIG.hojasAProcesar.forEach(function(nombreHoja) { var hoja = ssOrigen.getSheetByName(nombreHoja); if (!hoja) return; var datos = hoja.getDataRange().getValues(); var encabezados = datos[0]; encabezados.forEach(function(col, i) { var vals = datos.slice(1).map(function(r) { return parseFloat(r[i]); }).filter(function(v) { return !isNaN(v); }); if (vals.length > 0) { var suma = vals.reduce(function(a, b) { return a + b; }, 0); datosFinales.push([nombreHoja, col + " (suma)", suma.toFixed(2), fecha]); datosFinales.push([nombreHoja, col + " (promedio)", (suma/vals.length).toFixed(2), fecha]); datosFinales.push([nombreHoja, col + " (registros)", vals.length, fecha]); } }); }); hojaDestino.clear(); if (datosFinales.length > 1) { hojaDestino.getRange(1, 1, datosFinales.length, 4).setValues(datosFinales); } console.log("✓ Resumen estadístico sincronizado"); }
URL: docs.google.com/spreadsheets/d/ → ESTE_ES_EL_ID → /edit
idDestino: "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms" // ejemplo
| Tu situación | Función a usar | Resultado en NotebookLM | ¿Cuándo? |
|---|---|---|---|
| Archivo con miles de filas, necesitas ver datos recientes | sincronizarParaNotebookLM() |
Últimas 500 filas de cada hoja + encabezados | Operativo diario |
| Datos demasiado grandes incluso filtrados | sincronizarSoloResumen() |
Solo sumas, promedios y conteos por columna | Gerencia / KPIs |
| Datos que cambian poco (contratos, políticas) | Trigger semanal en vez de horario | Actualización cada 7 días | Documentos |
| Necesitas análisis por período (mes, trimestre) | Modificar el script para filtrar por fecha | Solo filas del período seleccionado | Reportes periódicos |
| Múltiples archivos grandes como fuente | Un script por archivo, todos apuntan al mismo destino | Un solo archivo de contexto con todo consolidado | Multifuente |
| Datos en tiempo real (cada minuto) | Apps Script no puede — usar Zapier o Make.com | Actualización casi en tiempo real | Tiempo real |
sincronizarSoloResumen() para el análisis gerencial (KPIs, tendencias, decisiones estratégicas) y sincronizarParaNotebookLM() para el equipo operativo que necesita ver casos específicos recientes. Crea dos notebooks separados en NotebookLM — uno por función.Ingeniero Industrial e Investigador de IA Explicativa (XAI). Este flujo ha sido optimizado para potenciar la toma de decisiones basada en hechos verificables y datos reales de tu propia industria.