feat: script de carga para las 33 creatividades de Blue Parrot (oct26-ene27)
scripts/seed_blue_parrot_pipeline.ts sube portada + slides de carrusel al bucket tenant-assets y crea/actualiza las filas en creative_pipeline (upsert por tenant_id+external_ref, así se puede re-ejecutar sin duplicar). Metadata en scripts/blue-parrot-pipeline-seed.json: títulos reales extraídos de las 33 piezas ya diseñadas (oct-01..oct-12, nov-01..nov-07, dic-01..dic-07, ene27-01..ene27-07). Estado inicial por defecto: revision_interna (parametrizable con --status). Requiere ejecutarse en el propio VPS (con las credenciales reales de Supabase ya configuradas ahí) contra la carpeta descomprimida del ZIP de imágenes — no incluye las imágenes ni credenciales en el repo. Añade dotenv y tsx como devDependencies (ya usados por otros scripts del repo pero no declarados en package.json). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WqGTGPznxTp8mwMg6Yv4Ew
This commit is contained in:
38
scripts/blue-parrot-pipeline-seed.json
Normal file
38
scripts/blue-parrot-pipeline-seed.json
Normal file
@@ -0,0 +1,38 @@
|
||||
[
|
||||
{ "id": "oct-01", "format": "carrusel", "month": "2026-10", "category": "aprende inglés", "title": "3 false friends que te delatan" },
|
||||
{ "id": "oct-02", "format": "story", "month": "2026-10", "category": "aprende inglés", "title": "Palabra del día: deadline" },
|
||||
{ "id": "oct-03", "format": "reel", "month": "2026-10", "category": "speaking", "title": "Preséntate en inglés en 30 segundos" },
|
||||
{ "id": "oct-04", "format": "post", "month": "2026-10", "category": "cursos y certificaciones", "title": "Cursos intensivos: certifica tu nivel" },
|
||||
{ "id": "oct-05", "format": "story", "month": "2026-10", "category": "test de nivel", "title": "¿No sabes por dónde empezar?" },
|
||||
{ "id": "oct-06", "format": "carrusel", "month": "2026-10", "category": "cursos y certificaciones", "title": "Aptis, Cambridge o IELTS: ¿cuál te conviene?" },
|
||||
{ "id": "oct-07", "format": "reel", "month": "2026-10", "category": "aprende inglés", "title": "Make o do: el truco para no fallar" },
|
||||
{ "id": "oct-08", "format": "story", "month": "2026-10", "category": "comunidad", "title": "¿Qué idioma quieres aprender?" },
|
||||
{ "id": "oct-09", "format": "carrusel", "month": "2026-10", "category": "inglés para el trabajo", "title": "3 frases para sonar más natural en una reunión" },
|
||||
{ "id": "oct-10", "format": "reel", "month": "2026-10", "category": "comunidad", "title": "Un día en la academia" },
|
||||
{ "id": "oct-11", "format": "post", "month": "2026-10", "category": "cursos y certificaciones", "title": "Grupos de máximo 8 alumnos" },
|
||||
{ "id": "oct-12", "format": "carrusel", "month": "2026-10", "category": "aprende inglés", "title": "Phrasal verbs con «get» que usarás cada día" },
|
||||
|
||||
{ "id": "nov-01", "format": "carrusel", "month": "2026-11", "category": "inglés con series", "title": "Friendsgiving: el inglés de Acción de Gracias con Friends" },
|
||||
{ "id": "nov-02", "format": "reel", "month": "2026-11", "category": "inglés para el día a día", "title": "Black Friday: 5 palabras que verás en todas las tiendas" },
|
||||
{ "id": "nov-03", "format": "story", "month": "2026-11", "category": "comunidad", "title": "¿Deal o bargain? Tú decides" },
|
||||
{ "id": "nov-04", "format": "carrusel", "month": "2026-11", "category": "inglés con series", "title": "El inglés de Stranger Things" },
|
||||
{ "id": "nov-05", "format": "post", "month": "2026-11", "category": "cursos y certificaciones", "title": "Certifica tu nivel antes de que acabe el año" },
|
||||
{ "id": "nov-06", "format": "reel", "month": "2026-11", "category": "speaking", "title": "Thanks, cheers, much appreciated: cómo dar las gracias en inglés" },
|
||||
{ "id": "nov-07", "format": "story", "month": "2026-11", "category": "aprende inglés", "title": "Palabra del día: leftovers" },
|
||||
|
||||
{ "id": "dic-01", "format": "carrusel", "month": "2026-12", "category": "inglés con películas", "title": "El inglés de Solo en casa" },
|
||||
{ "id": "dic-02", "format": "story", "month": "2026-12", "category": "aprende inglés", "title": "Palabra del día: cosy" },
|
||||
{ "id": "dic-03", "format": "reel", "month": "2026-12", "category": "speaking", "title": "Felicita la Navidad en inglés sin decir solo Merry Christmas" },
|
||||
{ "id": "dic-04", "format": "carrusel", "month": "2026-12", "category": "inglés con películas", "title": "Love Actually: frases para practicar tu inglés" },
|
||||
{ "id": "dic-05", "format": "post", "month": "2026-12", "category": "cursos y certificaciones", "title": "Estas Navidades, regala un idioma" },
|
||||
{ "id": "dic-06", "format": "reel", "month": "2026-12", "category": "aprende inglés", "title": "Christmas vocabulary: mistletoe, stocking, Boxing Day" },
|
||||
{ "id": "dic-07", "format": "story", "month": "2026-12", "category": "comunidad", "title": "¿Tu propósito de 2027 es aprender un idioma?" },
|
||||
|
||||
{ "id": "ene27-01", "format": "carrusel", "month": "2027-01", "category": "aprende inglés", "title": "New Year's resolutions: di tus propósitos en inglés" },
|
||||
{ "id": "ene27-02", "format": "story", "month": "2027-01", "category": "test de nivel", "title": "Empieza el año sabiendo tu nivel" },
|
||||
{ "id": "ene27-03", "format": "reel", "month": "2027-01", "category": "aprende inglés", "title": "Take up, give up, keep up: 3 phrasal verbs para tus propósitos" },
|
||||
{ "id": "ene27-04", "format": "post", "month": "2027-01", "category": "cursos y certificaciones", "title": "Nuevo año, nuevo idioma" },
|
||||
{ "id": "ene27-05", "format": "carrusel", "month": "2027-01", "category": "aprende inglés", "title": "Blue Monday: vocabulario para los días grises" },
|
||||
{ "id": "ene27-06", "format": "reel", "month": "2027-01", "category": "aprende inglés", "title": "Winter vocabulary: freezing, chilly, to bundle up" },
|
||||
{ "id": "ene27-07", "format": "story", "month": "2027-01", "category": "aprende inglés", "title": "Palabra del día: chilly" }
|
||||
]
|
||||
164
scripts/seed_blue_parrot_pipeline.ts
Normal file
164
scripts/seed_blue_parrot_pipeline.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Carga las 33 piezas de Blue Parrot (oct 2026 - ene 2027) en creative_pipeline.
|
||||
*
|
||||
* Requiere:
|
||||
* 1. Haber aplicado sql/creative_pipeline.sql en Supabase.
|
||||
* 2. Haber descomprimido Blue-Parrot-creatividades-con-imagen.zip en una carpeta.
|
||||
* 3. .env.local con NEXT_PUBLIC_SUPABASE_URL y SUPABASE_SERVICE_ROLE_KEY reales
|
||||
* (los mismos que usa la app en producción — ejecutar esto en el propio VPS,
|
||||
* por ejemplo desde "Open Terminal" en Dokploy, nunca pegando la service
|
||||
* role key en un chat).
|
||||
*
|
||||
* Uso:
|
||||
* npx tsx scripts/seed_blue_parrot_pipeline.ts --dir /ruta/a/Blue-Parrot-creatividades-con-imagen \
|
||||
* [--status revision_interna] [--dry-run]
|
||||
*/
|
||||
import { config } from 'dotenv';
|
||||
import { resolve, join } from 'path';
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
config({ path: resolve(process.cwd(), '.env.local') });
|
||||
|
||||
// Mismo tenant_id que scripts/provision_victor_bps.ts
|
||||
const BLUE_PARROT_TENANT_ID = '8bec977a-6032-4759-aa7f-b5042081f450';
|
||||
const BUCKET = 'tenant-assets';
|
||||
|
||||
type SeedItem = {
|
||||
id: string;
|
||||
format: 'post' | 'story' | 'reel' | 'carrusel';
|
||||
month: string;
|
||||
category: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
function arg(name: string, fallback?: string): string | undefined {
|
||||
const i = process.argv.indexOf(`--${name}`);
|
||||
if (i === -1) return fallback;
|
||||
return process.argv[i + 1];
|
||||
}
|
||||
const DRY_RUN = process.argv.includes('--dry-run');
|
||||
|
||||
async function main() {
|
||||
const url = process.env.NEXT_PUBLIC_SUPABASE_URL?.trim();
|
||||
const key = process.env.SUPABASE_SERVICE_ROLE_KEY?.trim();
|
||||
if (!url || !key) throw new Error('Faltan NEXT_PUBLIC_SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY en .env.local');
|
||||
|
||||
const dir = arg('dir');
|
||||
if (!dir || !existsSync(dir)) {
|
||||
throw new Error('Pasa --dir /ruta/a/la/carpeta/descomprimida (el ZIP de las 33 piezas)');
|
||||
}
|
||||
const status = arg('status', 'revision_interna')!;
|
||||
|
||||
const metaPath = resolve(__dirname, 'blue-parrot-pipeline-seed.json');
|
||||
const items: SeedItem[] = JSON.parse(readFileSync(metaPath, 'utf-8'));
|
||||
|
||||
const admin = createClient(url, key, { auth: { persistSession: false, autoRefreshToken: false } });
|
||||
|
||||
// Asegura el bucket (no falla si ya existe).
|
||||
const { data: buckets } = await admin.storage.listBuckets();
|
||||
if (!buckets?.some((b) => b.id === BUCKET)) {
|
||||
await admin.storage.createBucket(BUCKET, { public: false });
|
||||
}
|
||||
|
||||
let ok = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const it of items) {
|
||||
const isCarrusel = it.format === 'carrusel';
|
||||
const coverLocal = isCarrusel
|
||||
? join(dir, `${it.id}-carrusel`, `${it.id}-1.jpg`)
|
||||
: join(dir, `${it.id}-${it.format}.jpg`);
|
||||
|
||||
if (!existsSync(coverLocal)) {
|
||||
console.error(`[SKIP] ${it.id}: no existe ${coverLocal}`);
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`[${it.id}] ${it.title}`);
|
||||
if (DRY_RUN) {
|
||||
ok++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 1) upsert de la fila (sin imágenes todavía) para obtener el id.
|
||||
const { data: row, error: upErr } = await admin
|
||||
.from('creative_pipeline')
|
||||
.upsert(
|
||||
{
|
||||
tenant_id: BLUE_PARROT_TENANT_ID,
|
||||
external_ref: it.id,
|
||||
title: it.title,
|
||||
format: it.format,
|
||||
month: it.month,
|
||||
status,
|
||||
notes: it.category,
|
||||
},
|
||||
{ onConflict: 'tenant_id,external_ref' }
|
||||
)
|
||||
.select('id')
|
||||
.single();
|
||||
|
||||
if (upErr || !row) {
|
||||
console.error(`[FAIL] ${it.id}: ${upErr?.message}`);
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
const itemId = row.id as string;
|
||||
|
||||
// 2) sube portada
|
||||
const coverPath = `${BLUE_PARROT_TENANT_ID}/pipeline/${itemId}/cover.jpg`;
|
||||
const coverBuf = readFileSync(coverLocal);
|
||||
const coverUp = await admin.storage.from(BUCKET).upload(coverPath, coverBuf, {
|
||||
contentType: 'image/jpeg',
|
||||
upsert: true,
|
||||
});
|
||||
if (coverUp.error) {
|
||||
console.error(`[FAIL cover] ${it.id}: ${coverUp.error.message}`);
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3) sube slides (carrusel: 2,3,4,5 → 4 slides interiores + CTA)
|
||||
const slidePaths: string[] = [];
|
||||
if (isCarrusel) {
|
||||
for (let n = 2; n <= 5; n++) {
|
||||
const local = join(dir, `${it.id}-carrusel`, `${it.id}-${n}.jpg`);
|
||||
if (!existsSync(local)) continue;
|
||||
const path = `${BLUE_PARROT_TENANT_ID}/pipeline/${itemId}/slide-${n - 1}.jpg`;
|
||||
const buf = readFileSync(local);
|
||||
const up = await admin.storage.from(BUCKET).upload(path, buf, {
|
||||
contentType: 'image/jpeg',
|
||||
upsert: true,
|
||||
});
|
||||
if (up.error) {
|
||||
console.error(`[FAIL slide ${n}] ${it.id}: ${up.error.message}`);
|
||||
continue;
|
||||
}
|
||||
slidePaths.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
// 4) cierra la fila con las rutas de storage
|
||||
const { error: patchErr } = await admin
|
||||
.from('creative_pipeline')
|
||||
.update({ cover_path: coverPath, cover_mime: 'image/jpeg', slide_paths: slidePaths })
|
||||
.eq('id', itemId);
|
||||
|
||||
if (patchErr) {
|
||||
console.error(`[FAIL patch] ${it.id}: ${patchErr.message}`);
|
||||
failed++;
|
||||
continue;
|
||||
}
|
||||
ok++;
|
||||
}
|
||||
|
||||
console.log(`\nHecho: ${ok} ok, ${failed} con error, de ${items.length} piezas.`);
|
||||
if (failed > 0) process.exitCode = 1;
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Reference in New Issue
Block a user