#!/usr/bin/env node /** * Push a gnommo video project to GnommoEditor's ingest API. * * Usage: * node scripts/push-gnommo-video.js * node scripts/push-gnommo-video.js ../gnommo/video1 * * Reads: project.json, slides.json, manuscript.txt, narration.json, * audio.json, videos.json, citations.json, .gnommo_sync.json * * Environment: * INGEST_API_KEY — Bearer token (default: dev-ingest-key-change-me) * INGEST_URL — API base URL (default: http://localhost:3001) */ import { readFileSync, existsSync } from 'fs'; import { resolve, join, dirname } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); // Load .env from project root const envPath = resolve(__dirname, '../.env'); if (existsSync(envPath)) { for (const line of readFileSync(envPath, 'utf8').split('\n')) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const eqIdx = trimmed.indexOf('='); if (eqIdx < 0) continue; const key = trimmed.slice(0, eqIdx).trim(); const val = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, ''); if (!process.env[key]) process.env[key] = val; } } const API_KEY = process.env.INGEST_API_KEY || 'dev-ingest-key-change-me'; const BASE_URL = process.env.INGEST_URL || 'http://localhost:3001'; const videoDir = process.argv[2]; if (!videoDir) { console.error('Usage: node scripts/push-gnommo-video.js '); process.exit(1); } const absDir = resolve(process.cwd(), videoDir); function readJson(absPath, label) { if (!existsSync(absPath)) return null; try { return JSON.parse(readFileSync(absPath, 'utf8')); } catch (e) { console.warn(`Warning: could not parse ${label}: ${e.message}`); return null; } } function readText(absPath) { return existsSync(absPath) ? readFileSync(absPath, 'utf8') : null; } // Resolve a project-relative path against the video directory. // Falls back to a conventional path if the field is missing. function resolveAsset(fieldValue, fallbackRelative) { if (fieldValue) { const candidate = resolve(absDir, fieldValue); if (existsSync(candidate)) return candidate; } if (fallbackRelative) { const videoName = absDir.split('/').pop(); const candidate = resolve(absDir, fallbackRelative.replace('{name}', videoName)); if (existsSync(candidate)) return candidate; } return null; } // ── Read project.json ────────────────────────────────────────────────────── const projectPath = join(absDir, 'project.json'); if (!existsSync(projectPath)) { console.error(`project.json not found at: ${projectPath}`); process.exit(1); } const project = JSON.parse(readFileSync(projectPath, 'utf8')); console.log(`Pushing: ${project.id}`); // ── Read slides.json ─────────────────────────────────────────────────────── const slidesPath = resolveAsset( project.slides, `media/slides/{name}/slides.json` ); if (!slidesPath) { console.error('Could not find slides.json'); process.exit(1); } const slides = JSON.parse(readFileSync(slidesPath, 'utf8')); console.log(` slides: ${Object.keys(slides).length}`); // ── Read manuscript.txt (optional) ──────────────────────────────────────── const manuscript = readText(join(absDir, 'manuscript.txt')) ?? readText(join(absDir, 'script.md')); if (manuscript) console.log(' manuscript: found'); // ── Read narration.json (optional) ──────────────────────────────────────── const narrationPath = resolveAsset(project.narration); const narration = narrationPath ? readJson(narrationPath, 'narration.json') : null; if (narration) console.log(` narration: ${Object.keys(narration).length} segments`); // ── Read audio.json (optional) ──────────────────────────────────────────── const audioPath = resolveAsset(project.audio); const audio = audioPath ? readJson(audioPath, 'audio.json') : null; if (audio) console.log(` audio: ${Object.keys(audio).length} tracks`); // ── Read videos.json (optional) ─────────────────────────────────────────── const videosPath = resolveAsset(project.videos); const videos = videosPath ? readJson(videosPath, 'videos.json') : null; if (videos) console.log(` video assets: ${Object.keys(videos).length}`); // ── Read citations.json (optional) ──────────────────────────────────────── const citations = readJson(join(absDir, 'citations.json'), 'citations.json'); if (citations) console.log(` citations: ${citations.length}`); // ── Read .gnommo_sync.json (optional) ───────────────────────────────────── const sync = readJson(join(absDir, '.gnommo_sync.json'), '.gnommo_sync.json'); if (sync) console.log(` sync: version ${sync.video_version}`); // ── POST to ingest API ───────────────────────────────────────────────────── const payload = { project, slides, ...(manuscript ? { manuscript } : {}), ...(narration ? { narration } : {}), ...(audio ? { audio } : {}), ...(videos ? { videos } : {}), ...(citations ? { citations } : {}), ...(sync ? { sync } : {}), }; const response = await fetch(`${BASE_URL}/api/ingest`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${API_KEY}`, }, body: JSON.stringify(payload), }); const body = await response.json(); if (!response.ok) { console.error(`Ingest failed (${response.status}):`, body); process.exit(1); } console.log(`Done — video_id=${body.video_id}, slides_upserted=${body.slides_upserted}`);