Spaces:
Paused
Paused
File size: 5,214 Bytes
1da8cb0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 | import fs from "node:fs";
import path from "node:path";
interface RunLog {
topic: string;
startedAt: string;
stages: { name: string; duration: number; status: string; error?: string }[];
totalCost?: { estimated: number; actual?: number };
}
interface RunEntry {
dir: string;
topic: string;
date: string;
stages: { done: number; total: number };
totalDuration: number;
estimated: number | null;
actual: number | null;
}
/**
* Scan the output directory for log.json files and display aggregated cost usage.
*/
export function showUsageReport(outputDir: string): void {
const resolved = path.resolve(outputDir);
if (!fs.existsSync(resolved)) {
console.log(`\nNo output directory found at: ${resolved}`);
console.log("Run the pipeline first to generate output.\n");
return;
}
const entries: RunEntry[] = [];
let dirs: string[];
try {
dirs = fs.readdirSync(resolved).sort();
} catch {
console.log(`\nCannot read output directory: ${resolved}\n`);
return;
}
for (const dir of dirs) {
const logPath = path.join(resolved, dir, "log.json");
if (!fs.existsSync(logPath)) continue;
try {
const raw = fs.readFileSync(logPath, "utf-8");
const log: RunLog = JSON.parse(raw);
const doneStages = log.stages.filter((s) => s.status === "done").length;
const totalDuration = log.stages.reduce((sum, s) => sum + (s.duration ?? 0), 0);
entries.push({
dir,
topic: log.topic,
date: log.startedAt,
stages: { done: doneStages, total: log.stages.length },
totalDuration,
estimated: log.totalCost?.estimated ?? null,
actual: log.totalCost?.actual ?? null,
});
} catch {
// Skip malformed log files
}
}
if (entries.length === 0) {
console.log(`\nNo run logs found in: ${resolved}`);
console.log("Runs without log.json files (e.g. deleted or incomplete) are not included.\n");
return;
}
// Aggregate totals
const totalEstimated = entries.reduce((sum, e) => sum + (e.estimated ?? 0), 0);
const totalActual = entries.reduce((sum, e) => sum + (e.actual ?? 0), 0);
const runsWithActual = entries.filter((e) => e.actual !== null).length;
const runsWithEstimate = entries.filter((e) => e.estimated !== null).length;
const BOLD = "\x1b[1m";
const DIM = "\x1b[2m";
const RESET = "\x1b[0m";
const CYAN = "\x1b[36m";
const GREEN = "\x1b[32m";
const YELLOW = "\x1b[33m";
// Header
console.log();
console.log(`${BOLD}OpenReels Usage Report${RESET}`);
console.log(`${DIM}${"─".repeat(60)}${RESET}`);
console.log(`${DIM}Source: ${resolved}${RESET}`);
console.log(`${DIM}Runs found: ${entries.length} (from log.json files in output directories)${RESET}`);
console.log();
// Summary box
console.log(`${BOLD} Total Cost Summary${RESET}`);
console.log(`${DIM} ${"─".repeat(40)}${RESET}`);
if (runsWithActual > 0) {
console.log(` ${GREEN}Actual cost:${RESET} ${BOLD}$${totalActual.toFixed(4)}${RESET} ${DIM}(${runsWithActual} runs)${RESET}`);
}
if (runsWithEstimate > 0) {
console.log(` ${CYAN}Estimated cost:${RESET} ${BOLD}$${totalEstimated.toFixed(4)}${RESET} ${DIM}(${runsWithEstimate} runs)${RESET}`);
}
if (runsWithActual === 0 && runsWithEstimate === 0) {
console.log(` ${DIM}No cost data recorded in any run.${RESET}`);
}
console.log();
// Per-run table
console.log(`${BOLD} Run Details${RESET}`);
console.log(`${DIM} ${"─".repeat(40)}${RESET}`);
for (const entry of entries) {
const dateStr = formatDate(entry.date);
const topicDisplay = truncate(entry.topic, 50);
const stageStr = `${entry.stages.done}/${entry.stages.total} stages`;
const durationStr = formatDuration(entry.totalDuration);
console.log();
console.log(` ${BOLD}${topicDisplay}${RESET}`);
console.log(` ${DIM}${dateStr} | ${stageStr} | ${durationStr}${RESET}`);
if (entry.actual !== null) {
console.log(` ${GREEN}Actual: $${entry.actual.toFixed(4)}${RESET}`);
} else if (entry.estimated !== null) {
console.log(` ${CYAN}Estimated: $${entry.estimated.toFixed(4)}${RESET} ${DIM}(pipeline did not complete)${RESET}`);
} else {
console.log(` ${DIM}No cost data${RESET}`);
}
}
// Footer
console.log();
console.log(`${DIM}${"─".repeat(60)}${RESET}`);
console.log(`${YELLOW}Note:${RESET} This report is based on log.json files in the output directory.`);
console.log(`If you delete a run folder, its cost data will no longer appear here.`);
console.log();
}
function formatDate(iso: string): string {
try {
const d = new Date(iso);
return d.toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
} catch {
return iso;
}
}
function formatDuration(seconds: number): string {
if (seconds < 60) return `${seconds.toFixed(1)}s`;
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}m ${secs.toFixed(0)}s`;
}
function truncate(str: string, max: number): string {
if (str.length <= max) return str;
return str.slice(0, max - 1) + "…";
}
|