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; } 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 content.slice(0, 2000); } // For media files, look for adjacent JSON with same name const jsonPath = fullPath.replace(/\.[^.]+$/, ".json"); if (existsSync(jsonPath)) { return readFileSync(jsonPath, "utf-8").slice(0, 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) 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; 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 }; }