Initial commit — Sistema Generador de Guiones V4.0

Pipeline completo: URL → Whisper → GPT-4o → pgvector → Supabase
Frontend Vue 3 + Tailwind, Backend Express + Vercel serverless functions
This commit is contained in:
2026-03-28 16:02:59 -05:00
commit 7695dd0be6
47 changed files with 7552 additions and 0 deletions

View File

@ -0,0 +1,189 @@
-- ============================================================
-- FASE 1 — SCHEMA PRINCIPAL
-- Sistema de Ingeniería de Guiones V4.0
-- Ejecutar en Supabase SQL Editor en este orden exacto
-- ============================================================
-- 1. EXTENSIÓN VECTORIAL
create extension if not exists vector;
-- ============================================================
-- 2. ENUMS (tipos controlados)
-- ============================================================
create type plataforma_enum as enum ('tiktok', 'reels', 'shorts');
create type estructura_narrativa_enum as enum (
'AIDA', 'PAS', 'hero_journey', 'storybrand', 'antes_despues', 'otra'
);
create type gancho_tipo_enum as enum (
'pregunta', 'declaracion_shock', 'dato_estadistica', 'historia', 'controversia', 'promesa_directa'
);
create type desarrollo_tipo_enum as enum (
'problema_solucion', 'lista', 'demostracion', 'testimonio', 'tutorial', 'storytelling_puro'
);
create type cta_tipo_enum as enum (
'seguir', 'comentar', 'compartir', 'comprar', 'visitar_link', 'guardar', 'ninguno'
);
create type pacing_enum as enum ('lento', 'medio', 'rapido', 'variable');
create type trigger_emocional_enum as enum (
'miedo', 'esperanza', 'curiosidad', 'ira', 'orgullo', 'tristeza', 'sorpresa', 'humor'
);
create type atencion_visual_enum as enum (
'zoom_agresivo', 'corte_rapido', 'texto_pantalla', 'cara_camara', 'broll_dinamico', 'ninguno'
);
create type dolor_placer_enum as enum ('apela_dolor', 'apela_placer', 'ambos');
create type carga_cognitiva_enum as enum ('baja', 'media', 'alta');
create type velocidad_locucion_enum as enum ('lenta', 'normal', 'rapida', 'muy_rapida');
create type tono_enum as enum (
'educativo', 'entretenimiento', 'inspiracional', 'controversial', 'informativo', 'humoristico'
);
create type persona_narradora_enum as enum (
'primera_persona', 'segunda_persona', 'tercera_persona', 'mixta'
);
create type nivel_especificidad_enum as enum ('generico', 'especifico', 'ultra_especifico');
create type idioma_enum as enum ('es', 'en', 'pt', 'fr', 'otro');
-- ============================================================
-- 3. TABLA CLIENTES
-- ============================================================
create table clientes (
id uuid primary key default gen_random_uuid(),
nombre text not null,
industria text not null,
sub_industrias text[], -- nichos específicos del cliente
mercados text[], -- países/regiones que atiende
activo boolean default true,
notas text,
fecha_alta timestamp with time zone default now()
);
-- ============================================================
-- 4. TABLA PRINCIPAL: GUIONES
-- ============================================================
create table guiones (
-- ── BLOQUE 0: Identificadores ──────────────────────────────
id uuid primary key default gen_random_uuid(),
cliente_id uuid references clientes(id) on delete set null,
-- ── BLOQUE 1: Organización de Agencia ─────────────────────
niche text not null,
sub_niche text,
mercado_objetivo text,
idioma idioma_enum default 'es',
proyecto_nombre text,
competidor_referente boolean default false, -- video analizado de la competencia
-- ── BLOQUE 2: Metadata del Video ──────────────────────────
url_origen text,
plataforma plataforma_enum,
duracion_segundos integer,
vistas bigint,
likes bigint,
compartidos bigint,
fecha_publicacion date,
-- ── BLOQUE 3: Storytelling ─────────────────────────────────
estructura_narrativa estructura_narrativa_enum,
gancho_tipo gancho_tipo_enum,
gancho_texto text, -- primeras 3-7 palabras del video
gancho_duracion_seg integer,
desarrollo_tipo desarrollo_tipo_enum,
cta_tipo cta_tipo_enum,
cta_texto text,
arco_emocional text, -- ej: "curiosidad → sorpresa → alivio"
conflicto_central text,
resolucion text,
pacing_ritmo pacing_enum,
numero_actos integer check (numero_actos between 1 and 4),
-- ── BLOQUE 4: Psicología / Cialdini ───────────────────────
cialdini_reciprocidad boolean default false,
cialdini_escasez boolean default false,
cialdini_autoridad boolean default false,
cialdini_consistencia boolean default false,
cialdini_prueba_social boolean default false,
cialdini_simpatia boolean default false,
cialdini_unidad boolean default false,
sesgo_cognitivo text, -- ej: "FOMO", "Efecto halo", "Anclaje"
trigger_emocional trigger_emocional_enum,
intensidad_emocional integer check (intensidad_emocional between 1 and 10),
-- ── BLOQUE 5: Neuropublicidad ──────────────────────────────
atencion_visual atencion_visual_enum,
lenguaje_sensorial boolean default false,
contraste_narrativo boolean default false, -- antes/después, ellos/nosotros
efecto_novedad boolean default false, -- algo inesperado en primeros 3s
dolor_placer dolor_placer_enum,
personalizacion boolean default false, -- habla directamente al "tú"
carga_cognitiva carga_cognitiva_enum,
velocidad_locucion velocidad_locucion_enum,
uso_musica boolean default false,
micro_compromisos boolean default false, -- pequeño compromiso antes del CTA
-- ── BLOQUE 6: Análisis de Contenido ───────────────────────
tema_principal text,
angulo_unico text,
palabras_clave text[],
transcript text,
tono tono_enum,
persona_narradora persona_narradora_enum,
promesa_explicita text,
nivel_especificidad nivel_especificidad_enum,
-- ── BLOQUE 7: Métricas Calculadas ─────────────────────────
score_engagement numeric(6,4), -- (likes + compartidos*3) / vistas * 100
score_virabilidad integer check (score_virabilidad between 1 and 100),
score_cialdini integer generated always as (
(cialdini_reciprocidad::int + cialdini_escasez::int +
cialdini_autoridad::int + cialdini_consistencia::int +
cialdini_prueba_social::int + cialdini_simpatia::int +
cialdini_unidad::int)
) stored,
resumen_patron text, -- párrafo generado por GPT-4o
embedding_vector vector(1536), -- text-embedding-3-small
-- ── BLOQUE 8: Auditoría del Sistema ───────────────────────
fecha_analisis timestamp with time zone default now(),
version_prompt text default 'v1.0', -- versión del prompt que generó el análisis
procesado_ok boolean default false, -- false si hubo error en el pipeline
error_detalle text -- log del error si procesado_ok = false
);
-- ============================================================
-- 5. ÍNDICES DE RENDIMIENTO
-- ============================================================
-- Búsqueda rápida por niche (el más frecuente en queries de agencia)
create index idx_guiones_niche on guiones(niche);
-- Búsqueda por cliente
create index idx_guiones_cliente on guiones(cliente_id);
-- Filtro por plataforma y engagement para rankings
create index idx_guiones_engagement on guiones(score_engagement desc nulls last);
-- Índice HNSW para búsqueda vectorial semántica (el más rápido para pgvector)
create index idx_guiones_vector on guiones
using hnsw (embedding_vector vector_cosine_ops)
with (m = 16, ef_construction = 64);
-- Índice compuesto niche + engagement para resumen_patrones()
create index idx_guiones_niche_engagement on guiones(niche, score_engagement desc nulls last);

View File

@ -0,0 +1,238 @@
-- ============================================================
-- FASE 1 — FUNCIONES DE INTELIGENCIA
-- Sistema de Ingeniería de Guiones V4.0
-- Ejecutar DESPUÉS de 01_schema.sql
-- ============================================================
-- ============================================================
-- FUNCIÓN 1: buscar_guiones_similares()
-- Encuentra los N guiones más parecidos semánticamente
-- filtrando por niche para que los referentes sean relevantes
-- ============================================================
create or replace function buscar_guiones_similares(
p_vector vector(1536), -- embedding del texto a comparar
p_niche text, -- niche para filtrar (obligatorio)
p_limite integer default 5,
p_cliente_id uuid default null, -- opcional: filtrar por cliente
p_solo_exitosos boolean default true -- si true, solo score_engagement > 0
)
returns table (
id uuid,
niche text,
sub_niche text,
plataforma plataforma_enum,
gancho_texto text,
estructura_narrativa estructura_narrativa_enum,
trigger_emocional trigger_emocional_enum,
score_engagement numeric(6,4),
score_virabilidad integer,
score_cialdini integer,
resumen_patron text,
similitud float
)
language sql stable
as $$
select
g.id,
g.niche,
g.sub_niche,
g.plataforma,
g.gancho_texto,
g.estructura_narrativa,
g.trigger_emocional,
g.score_engagement,
g.score_virabilidad,
g.score_cialdini,
g.resumen_patron,
1 - (g.embedding_vector <=> p_vector) as similitud
from guiones g
where
g.procesado_ok = true
and g.embedding_vector is not null
and g.niche = p_niche
and (p_cliente_id is null or g.cliente_id = p_cliente_id)
and (not p_solo_exitosos or (g.score_engagement is not null and g.score_engagement > 0))
order by
g.embedding_vector <=> p_vector -- menor distancia coseno = más similar
limit p_limite;
$$;
-- ============================================================
-- FUNCIÓN 2: resumen_patrones()
-- Agrega los patrones ganadores de un niche para el generador
-- Retorna un JSON estructurado que se enviará a GPT-4o
-- como contexto al generar guiones nuevos
-- ============================================================
create or replace function resumen_patrones(
p_niche text,
p_cliente_id uuid default null,
p_top_n integer default 20, -- cuántos guiones top analizar
p_min_engagement numeric default 0.1 -- filtro mínimo de engagement
)
returns json
language plpgsql stable
as $$
declare
v_resultado json;
begin
with top_guiones as (
-- Seleccionar los mejores guiones del niche
select *
from guiones
where
niche = p_niche
and procesado_ok = true
and score_engagement >= p_min_engagement
and (p_cliente_id is null or cliente_id = p_cliente_id)
order by score_engagement desc
limit p_top_n
),
conteos as (
select
-- Storytelling patterns
mode() within group (order by estructura_narrativa) as estructura_dominante,
mode() within group (order by gancho_tipo) as gancho_dominante,
mode() within group (order by cta_tipo) as cta_dominante,
mode() within group (order by pacing_ritmo) as pacing_dominante,
-- Psicología patterns
mode() within group (order by trigger_emocional) as trigger_dominante,
mode() within group (order by sesgo_cognitivo) as sesgo_dominante,
avg(intensidad_emocional) as intensidad_promedio,
-- Cialdini frecuencias
round(avg(cialdini_reciprocidad::int) * 100) as pct_reciprocidad,
round(avg(cialdini_escasez::int) * 100) as pct_escasez,
round(avg(cialdini_autoridad::int) * 100) as pct_autoridad,
round(avg(cialdini_consistencia::int) * 100) as pct_consistencia,
round(avg(cialdini_prueba_social::int) * 100) as pct_prueba_social,
round(avg(cialdini_simpatia::int) * 100) as pct_simpatia,
round(avg(cialdini_unidad::int) * 100) as pct_unidad,
-- Neuropublicidad patterns
mode() within group (order by atencion_visual) as atencion_dominante,
mode() within group (order by dolor_placer) as dolor_placer_dominante,
round(avg(lenguaje_sensorial::int) * 100) as pct_lenguaje_sensorial,
round(avg(contraste_narrativo::int) * 100) as pct_contraste,
round(avg(efecto_novedad::int) * 100) as pct_novedad,
-- Contenido patterns
mode() within group (order by tono) as tono_dominante,
mode() within group (order by persona_narradora) as persona_dominante,
mode() within group (order by nivel_especificidad) as especificidad_dominante,
-- Métricas generales
avg(score_engagement) as engagement_promedio,
avg(score_virabilidad) as virabilidad_promedio,
avg(score_cialdini) as cialdini_promedio,
avg(duracion_segundos) as duracion_promedio,
count(*) as total_analizados
from top_guiones
)
select json_build_object(
'niche', p_niche,
'total_analizados', c.total_analizados,
'fecha_generado', now(),
'metricas_promedio', json_build_object(
'engagement', round(c.engagement_promedio::numeric, 4),
'virabilidad', round(c.virabilidad_promedio::numeric, 1),
'cialdini_score', round(c.cialdini_promedio::numeric, 1),
'duracion_seg', round(c.duracion_promedio::numeric, 0)
),
'storytelling', json_build_object(
'estructura_dominante', c.estructura_dominante,
'gancho_dominante', c.gancho_dominante,
'cta_dominante', c.cta_dominante,
'pacing_dominante', c.pacing_dominante
),
'psicologia', json_build_object(
'trigger_dominante', c.trigger_dominante,
'sesgo_dominante', c.sesgo_dominante,
'intensidad_promedio', round(c.intensidad_promedio::numeric, 1),
'cialdini_frecuencias', json_build_object(
'reciprocidad', c.pct_reciprocidad,
'escasez', c.pct_escasez,
'autoridad', c.pct_autoridad,
'consistencia', c.pct_consistencia,
'prueba_social', c.pct_prueba_social,
'simpatia', c.pct_simpatia,
'unidad', c.pct_unidad
)
),
'neuropublicidad', json_build_object(
'atencion_dominante', c.atencion_dominante,
'dolor_placer_dominante', c.dolor_placer_dominante,
'pct_lenguaje_sensorial', c.pct_lenguaje_sensorial,
'pct_contraste_narrativo', c.pct_contraste,
'pct_efecto_novedad', c.pct_novedad
),
'contenido', json_build_object(
'tono_dominante', c.tono_dominante,
'persona_dominante', c.persona_dominante,
'especificidad_dominante', c.especificidad_dominante
)
) into v_resultado
from conteos c;
return v_resultado;
end;
$$;
-- ============================================================
-- FUNCIÓN 3: calcular_score_engagement()
-- Trigger para calcular automáticamente score_engagement
-- cuando se inserta o actualiza vistas/likes/compartidos
-- ============================================================
create or replace function calcular_score_engagement()
returns trigger
language plpgsql
as $$
begin
if new.vistas is not null and new.vistas > 0 then
new.score_engagement := round(
((coalesce(new.likes, 0) + coalesce(new.compartidos, 0) * 3)::numeric
/ new.vistas::numeric * 100)::numeric,
4
);
else
new.score_engagement := null;
end if;
return new;
end;
$$;
create trigger trg_calcular_engagement
before insert or update of vistas, likes, compartidos
on guiones
for each row
execute function calcular_score_engagement();
-- ============================================================
-- FUNCIÓN 4: guiones_por_niche()
-- Vista de resumen rápido para el dashboard
-- ============================================================
create or replace view vista_resumen_nichos as
select
g.niche,
g.cliente_id,
c.nombre as cliente_nombre,
count(*) as total_guiones,
round(avg(g.score_engagement)::numeric, 4) as engagement_promedio,
round(avg(g.score_virabilidad)::numeric, 1) as virabilidad_promedio,
round(avg(g.score_cialdini)::numeric, 1) as cialdini_promedio,
max(g.score_engagement) as mejor_engagement,
max(g.fecha_analisis) as ultimo_analisis
from guiones g
left join clientes c on c.id = g.cliente_id
where g.procesado_ok = true
group by g.niche, g.cliente_id, c.nombre
order by engagement_promedio desc nulls last;

View File

@ -0,0 +1,80 @@
-- ============================================================
-- FASE 1 — ROW LEVEL SECURITY (RLS)
-- Sistema de Ingeniería de Guiones V4.0
-- Ejecutar DESPUÉS de 01_schema.sql y 02_funciones.sql
-- ============================================================
-- IMPORTANTE: En Supabase, las políticas RLS controlan qué
-- filas puede ver/editar cada usuario autenticado.
-- El usuario autenticado se obtiene con auth.uid().
-- ============================================================
-- ── Habilitar RLS en ambas tablas ─────────────────────────
alter table clientes enable row level security;
alter table guiones enable row level security;
-- ============================================================
-- TABLA: clientes
-- ============================================================
-- Los usuarios autenticados pueden ver todos los clientes.
-- Si en el futuro cada usuario es de una agencia específica,
-- se puede agregar un campo agencia_id y filtrar aquí.
create policy "clientes_select"
on clientes for select
to authenticated
using (true);
create policy "clientes_insert"
on clientes for insert
to authenticated
with check (true);
create policy "clientes_update"
on clientes for update
to authenticated
using (true);
-- Solo usuarios con rol 'admin' pueden eliminar clientes
create policy "clientes_delete"
on clientes for delete
to authenticated
using (auth.jwt() ->> 'role' = 'admin');
-- ============================================================
-- TABLA: guiones
-- ============================================================
-- Cualquier usuario autenticado puede leer guiones
create policy "guiones_select"
on guiones for select
to authenticated
using (true);
-- Cualquier usuario autenticado puede insertar guiones
create policy "guiones_insert"
on guiones for insert
to authenticated
with check (true);
-- Actualización permitida para todos los autenticados
create policy "guiones_update"
on guiones for update
to authenticated
using (true);
-- Solo admin puede eliminar guiones
create policy "guiones_delete"
on guiones for delete
to authenticated
using (auth.jwt() ->> 'role' = 'admin');
-- ============================================================
-- ACCESO DE SERVICIO (Service Role)
-- El backend de Vercel usa la SERVICE_ROLE key de Supabase
-- que bypasea RLS automáticamente — no necesita políticas.
-- NUNCA exponer la SERVICE_ROLE key en el frontend.
-- ============================================================

View File

@ -0,0 +1,74 @@
-- ============================================================
-- FASE 1 — DATOS DE PRUEBA
-- Sistema de Ingeniería de Guiones V4.0
-- Ejecutar ÚLTIMO para validar que el schema funciona
-- ============================================================
-- 1. Insertar clientes de ejemplo
insert into clientes (nombre, industria, sub_industrias, mercados) values
('Agencia Demo', 'Marketing Digital', array['redes sociales', 'contenido'], array['MX', 'CO', 'AR']),
('Cliente Fitness', 'Salud y Bienestar', array['fitness', 'nutricion'], array['MX', 'US']),
('Cliente Finanzas', 'Finanzas Personales', array['inversion', 'ahorro', 'cripto'], array['MX', 'CO']);
-- 2. Insertar guion de ejemplo para validar el schema completo
-- (el embedding_vector se llenará cuando el backend procese videos reales)
insert into guiones (
niche, sub_niche, mercado_objetivo, idioma, proyecto_nombre,
plataforma, duracion_segundos,
vistas, likes, compartidos, fecha_publicacion,
estructura_narrativa, gancho_tipo, gancho_texto, gancho_duracion_seg,
desarrollo_tipo, cta_tipo, cta_texto,
arco_emocional, conflicto_central, resolucion,
pacing_ritmo, numero_actos,
cialdini_reciprocidad, cialdini_escasez, cialdini_autoridad,
cialdini_consistencia, cialdini_prueba_social, cialdini_simpatia,
cialdini_unidad,
sesgo_cognitivo, trigger_emocional, intensidad_emocional,
atencion_visual, lenguaje_sensorial, contraste_narrativo,
efecto_novedad, dolor_placer, personalizacion, carga_cognitiva,
velocidad_locucion, uso_musica, micro_compromisos,
tema_principal, angulo_unico, palabras_clave,
transcript, tono, persona_narradora, promesa_explicita,
nivel_especificidad, score_virabilidad, resumen_patron,
procesado_ok, version_prompt
) values (
'fitness', 'pérdida de peso', 'MX', 'es', 'Campaña Verano 2025',
'reels', 45,
850000, 62000, 18000, '2025-01-15',
'PAS', 'pregunta', '¿Por qué no bajas de peso aunque...', 4,
'problema_solucion', 'seguir', 'Sígueme para más tips',
'frustración → esperanza → motivación',
'La persona hace todo bien pero no ve resultados',
'El problema es el cortisol, no las calorías',
'rapido', 3,
true, false, true,
false, true, true,
false,
'Sesgo de confirmación', 'curiosidad', 8,
'cara_camara', true, true,
true, 'apela_dolor', true, 'baja',
'rapida', true, false,
'pérdida de peso', 'ataca el cortisol como causa real, no las calorías',
array['cortisol', 'bajar de peso', 'metabolismo', 'fitness'],
'¿Por qué no bajas de peso aunque comes bien y haces ejercicio? El problema no son las calorías. Es el cortisol...',
'educativo', 'segunda_persona', 'Aprenderás por qué tu cuerpo retiene grasa a pesar del ejercicio',
'ultra_especifico', 87,
'Video educativo que ataca creencia falsa común. Usa pregunta retórica como gancho, autoridad con dato científico y prueba social implícita.',
true, 'v1.0'
);
-- 3. Verificar que el trigger calculó el score de engagement
select
id,
niche,
vistas,
likes,
compartidos,
score_engagement,
score_cialdini
from guiones
where niche = 'fitness';
-- Resultado esperado:
-- score_engagement = (62000 + 18000*3) / 850000 * 100 = 13.647...
-- score_cialdini = 4 (reciprocidad + autoridad + prueba_social + simpatia)