import { readdirSync, readFileSync, statSync, existsSync } from "fs"; import path from "path"; import { prisma } from "./prisma"; interface ScannedFile { filePath: string; fileName: string; type: string; platform: string | null; format: string; dimensions: string | null; metadata: string | null; } /** Truncate a JSON string to maxLen while keeping it valid JSON. */ function safeJsonTruncate(json: string, maxLen: number): string { if (json.length <= maxLen) return json; try { const parsed = JSON.parse(json); // Try to produce a shorter version by re-stringifying with key limits const trimmed = JSON.stringify(parsed, (_key, value) => { if (typeof value === "string" && value.length > 200) { return value.slice(0, 200) + "…"; } if (Array.isArray(value) && value.length > 5) { return value.slice(0, 5); } return value; }); if (trimmed.length <= maxLen) return trimmed; // Still too long — return a minimal summary return JSON.stringify({ _truncated: true, _originalLength: json.length }); } catch { return JSON.stringify({ _truncated: true, _originalLength: json.length }); } } const FORMAT_TO_TYPE: Record = { png: "image", jpg: "image", jpeg: "image", webp: "image", gif: "image", mp4: "video", webm: "video", json: "copy", txt: "copy", md: "research", html: "research", }; function inferPlatform(fileName: string, filePath: string): string | null { const lower = (fileName + filePath).toLowerCase(); if (lower.includes("instagram") || lower.includes("ig_") || lower.includes("ig ")) return "instagram"; if (lower.includes("tiktok") || lower.includes("tt_") || lower.includes("tik_tok")) return "tiktok"; if (lower.includes("nextdoor") || lower.includes("nd_")) return "nextdoor"; return null; } function inferDimensions(fileName: string): string | null { const match = fileName.match(/(\d{3,4})x(\d{3,4})/); return match ? `${match[1]}x${match[2]}` : null; } function inferTypeFromPath(filePath: string, format: string): string { const lower = filePath.toLowerCase(); if (lower.includes("/ads/")) return "image"; if (lower.includes("/video/")) return "video"; if (lower.includes("/copy/")) return "copy"; if (lower.includes("/scripts/")) return "script"; return FORMAT_TO_TYPE[format] || "research"; } /** * Try to read metadata from adjacent JSON files or manifest. * For an image "instagram_feed_hook_a_1080x1080.png", look for: * - "instagram_feed_hook_a_1080x1080.json" (same name, .json ext) * - "ad_manifest.json" in same directory * For copy JSON files, read the file itself as metadata. */ function loadMetadata(fullPath: string, format: string): string | null { try { // For JSON files, read the content as metadata if (format === "json") { const content = readFileSync(fullPath, "utf-8"); const parsed = JSON.parse(content); // Extract caption/summary if it's an array of captions if (Array.isArray(parsed)) { return JSON.stringify({ captions: parsed.slice(0, 3), totalVariations: parsed.length }); } return safeJsonTruncate(content, 2000); } // For media files, look for adjacent JSON with same name const jsonPath = fullPath.replace(/\.[^.]+$/, ".json"); if (existsSync(jsonPath)) { return safeJsonTruncate(readFileSync(jsonPath, "utf-8"), 2000); } // Look for manifest in same directory const dir = path.dirname(fullPath); const manifestPath = path.join(dir, "ad_manifest.json"); if (existsSync(manifestPath)) { const manifest = JSON.parse(readFileSync(manifestPath, "utf-8")); const fileName = path.basename(fullPath); // Find this file's entry in the manifest if (Array.isArray(manifest)) { const entry = manifest.find((e: { fileName?: string; file?: string }) => e.fileName === fileName || e.file === fileName ); if (entry) { // Ensure style field is preserved in metadata return JSON.stringify(entry); } } } // Look for scene_plans.json for video files if (format === "mp4" || format === "webm") { const scenePlansPath = path.join(dir, "scene_plans.json"); if (existsSync(scenePlansPath)) { const plans = JSON.parse(readFileSync(scenePlansPath, "utf-8")); const fileName = path.basename(fullPath); if (plans[fileName]) return JSON.stringify(plans[fileName]); } } } catch { // Metadata loading is best-effort } return null; } function scanDirectory(dir: string, baseDir: string): ScannedFile[] { const files: ScannedFile[] = []; try { const entries = readdirSync(dir); for (const entry of entries) { if (entry.startsWith(".")) continue; const fullPath = path.join(dir, entry); const stat = statSync(fullPath); if (stat.isDirectory()) { files.push(...scanDirectory(fullPath, baseDir)); } else { const ext = path.extname(entry).toLowerCase().slice(1); if (!ext || ext === "gitkeep") continue; // Only ingest deliverable files — skip source/build artifacts const ASSET_EXTENSIONS = new Set([ "png", "jpg", "jpeg", "webp", "gif", // images "mp4", "webm", // videos ]); const CONTENT_EXTENSIONS = new Set([ "json", "md", "txt", // copy/scripts/research ]); // Skip HTML source files, render scripts, and build tools if (!ASSET_EXTENSIONS.has(ext) && !CONTENT_EXTENSIONS.has(ext)) continue; // Skip known build/tool artifacts const SKIP_FILES = new Set([ "tavily_search.mjs", "render_posters.mjs", "design_philosophy.md", ]); if (SKIP_FILES.has(entry)) continue; // Skip HTML source files in ads/ (they're build artifacts, not deliverables) const relativePath0 = path.relative(baseDir, fullPath); if (ext === "html" && relativePath0.includes("/ads/")) continue; const relativePath = path.relative(baseDir, fullPath); const type = inferTypeFromPath(relativePath, ext); const metadata = loadMetadata(fullPath, ext); files.push({ filePath: relativePath, fileName: entry, type, platform: inferPlatform(entry, relativePath), format: ext, dimensions: inferDimensions(entry), metadata, }); } } } catch { // Directory doesn't exist or can't be read } return files; } export async function scanOutputDirectory( campaignId: string, outputPath: string, pipelineRoot: string ) { const fullOutputPath = path.join(pipelineRoot, outputPath); const files = scanDirectory(fullOutputPath, pipelineRoot); let created = 0; for (const file of files) { const existing = await prisma.asset.findFirst({ where: { campaignId, filePath: file.filePath, }, }); if (!existing) { await prisma.asset.create({ data: { campaignId, type: file.type, platform: file.platform, format: file.format, filePath: file.filePath, fileName: file.fileName, dimensions: file.dimensions, metadata: file.metadata, status: "draft", }, }); created++; } } return { scanned: files.length, created }; }