155 lines
6.2 KiB
JavaScript
155 lines
6.2 KiB
JavaScript
|
|
#!/usr/bin/env node
|
||
|
|
/**
|
||
|
|
* Push a gnommo video project to GnommoEditor's ingest API.
|
||
|
|
*
|
||
|
|
* Usage:
|
||
|
|
* node scripts/push-gnommo-video.js <path-to-video-dir>
|
||
|
|
* 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 <video-dir>');
|
||
|
|
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}`);
|