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:
189
database/migrations/01_schema.sql
Normal file
189
database/migrations/01_schema.sql
Normal 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);
|
||||
238
database/migrations/02_funciones.sql
Normal file
238
database/migrations/02_funciones.sql
Normal 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;
|
||||
80
database/migrations/03_rls.sql
Normal file
80
database/migrations/03_rls.sql
Normal 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.
|
||||
-- ============================================================
|
||||
Reference in New Issue
Block a user