Siddharth63 commited on
Commit
e810f39
Β·
verified Β·
1 Parent(s): e37c394

Add build_report_excel.py

Browse files
Files changed (1) hide show
  1. build_report_excel.py +172 -0
build_report_excel.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Excel workbook for a cascade batch: machine analysis now, human analysis as soon as reviews are added.
3
+
4
+ Views one row per product x view: product info, every engine's verdict / flags, the final engine, time, estimated
5
+ cost, and Human verdict (Pass/Fail, drop-down) / comment / reviewer columns, filled live by review_server.py, from
6
+ review_*.json exports (--human), or typed in Excel. "Agreement" compares the machine verdict with the human one.
7
+ Summary formulas over Views (so they update when human verdicts are entered): overall, per view, per source, per
8
+ final engine β€” machine clean rate, human pass rate, agreement, engine shares, cost.
9
+ Products one row per product: views clean, engines used.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ from pathlib import Path
16
+
17
+ VIEWS = ("front", "hero45", "side", "back")
18
+ ENGINES = ("klein4b", "klein9b", "nb2")
19
+ ENAME = {"klein4b": "Klein-4B", "klein9b": "Klein-9B", "nb2": "Nano Banana 2"}
20
+ COST = {"klein4b": 0.040, "klein9b": 0.072, "nb2": 0.067, "judge": 0.002}
21
+
22
+
23
+ def rows_from_batch(batch: Path):
24
+ out, products = [], []
25
+ for f in sorted(batch.glob("*/*/product.json")):
26
+ r = json.loads(f.read_text())
27
+ if not r.get("attempts"):
28
+ continue
29
+ prow = {"source": r["source"], "product_id": r["product_id"], "title": r["title"], "views": 0, "clean": 0,
30
+ **{ENAME[e]: 0 for e in ENGINES}}
31
+ for v in VIEWS:
32
+ att = r["attempts"].get(v, [])
33
+ if not att or v not in r.get("final", {}):
34
+ continue
35
+ by = {a["engine"]: a for a in att}; last = att[-1]; final = r["final"][v]
36
+ cost = sum(COST[a["engine"]] + COST["judge"] for a in att)
37
+ out.append({"Source": "Goddiva" if r["source"] == "goddiva" else "Product feed", "Stratum": r.get("stratum", ""),
38
+ "Product id": r["product_id"], "Title": r["title"], "Brand": r.get("brand", ""), "Category": r.get("category", ""),
39
+ "Photos show": r.get("descriptors", {}).get("presentation", ""), "View": v,
40
+ "Tries": " β†’ ".join(ENAME[a["engine"]] + (" βœ“" if a["clean"] else " βœ—") for a in att),
41
+ "Final engine": ENAME[final], "Machine clean": bool(last["clean"]), "Final flags": ", ".join(last["flags"]),
42
+ "Final note": last["note"],
43
+ **{f"{ENAME[e]} verdict": ("clean" if by[e]["clean"] else "rejected") if e in by else "" for e in ENGINES},
44
+ **{f"{ENAME[e]} flags": (", ".join(by[e]["flags"]) + (f" β€” {by[e]['note']}" if by[e]["note"] else "")) if e in by else "" for e in ENGINES},
45
+ "Render seconds": round(sum(a["seconds"] for a in att), 1), "Est. cost USD": round(cost, 3),
46
+ "Human verdict": "", "Human comment": "", "Reviewer": ""})
47
+ prow["views"] += 1; prow["clean"] += bool(last["clean"]); prow[ENAME[final]] += 1
48
+ products.append(prow)
49
+ return out, products
50
+
51
+
52
+ def apply_human(rows, entries):
53
+ """entries: review rows ({product_id, view, human_verdict, human_comment, reviewer}), from review_*.json or reviews.json."""
54
+ idx = {(r["Product id"], r["View"]): r for r in rows}
55
+ n = 0
56
+ for h in entries:
57
+ r = idx.get((h["product_id"], h["view"]))
58
+ if r and (h.get("human_verdict") or h.get("human_comment")):
59
+ r["Human verdict"] = (h.get("human_verdict") or "").capitalize(); r["Human comment"] = h.get("human_comment", ""); r["Reviewer"] = h.get("reviewer", "")
60
+ n += bool(h.get("human_verdict"))
61
+ return n
62
+
63
+
64
+ def build(batch: Path, human: list, out: Path) -> str:
65
+ """Write the workbook for <batch> with the human review rows merged in; returns a one-line summary."""
66
+ from openpyxl import Workbook
67
+ from openpyxl.formatting.rule import CellIsRule
68
+ from openpyxl.styles import Alignment, Font, PatternFill
69
+ from openpyxl.utils import get_column_letter
70
+ from openpyxl.worksheet.datavalidation import DataValidation
71
+
72
+ rows, products = rows_from_batch(batch)
73
+ nh = apply_human(rows, human)
74
+ wb = Workbook(); head = Font(bold=True, color="FFFFFF"); hfill = PatternFill("solid", fgColor="2F5FD0")
75
+
76
+ # --- Views
77
+ ws = wb.active; ws.title = "Views"
78
+ cols = list(rows[0].keys()) + ["Agreement"]
79
+ ws.append(cols)
80
+ for c in ws[1]:
81
+ c.font, c.fill = head, hfill
82
+ L = {name: get_column_letter(i + 1) for i, name in enumerate(cols)}
83
+ for i, r in enumerate(rows, start=2):
84
+ ws.append([r[c] for c in cols[:-1]] + [f'=IF({L["Human verdict"]}{i}="","",IF(({L["Machine clean"]}{i}=TRUE)=({L["Human verdict"]}{i}="Pass"),"agree","disagree"))'])
85
+ n = len(rows) + 1
86
+ dv = DataValidation(type="list", formula1='"Pass,Fail"', allow_blank=True); ws.add_data_validation(dv); dv.add(f"{L['Human verdict']}2:{L['Human verdict']}{n}")
87
+ green, red = PatternFill("solid", fgColor="D9F2E3"), PatternFill("solid", fgColor="F8DAD7")
88
+ for col in ("Machine clean",):
89
+ ws.conditional_formatting.add(f"{L[col]}2:{L[col]}{n}", CellIsRule(operator="equal", formula=["TRUE"], fill=green))
90
+ ws.conditional_formatting.add(f"{L[col]}2:{L[col]}{n}", CellIsRule(operator="equal", formula=["FALSE"], fill=red))
91
+ ws.conditional_formatting.add(f"{L['Human verdict']}2:{L['Human verdict']}{n}", CellIsRule(operator="equal", formula=['"Pass"'], fill=green))
92
+ ws.conditional_formatting.add(f"{L['Human verdict']}2:{L['Human verdict']}{n}", CellIsRule(operator="equal", formula=['"Fail"'], fill=red))
93
+ widths = {"Title": 44, "Tries": 40, "Final note": 40, "Human comment": 40, "Category": 26, "Product id": 30}
94
+ for name, letter in L.items():
95
+ ws.column_dimensions[letter].width = widths.get(name, 16 if "flags" not in name else 34)
96
+ ws.freeze_panes = "E2"; ws.auto_filter.ref = f"A1:{get_column_letter(len(cols))}{n}"
97
+
98
+ # --- Summary (formulas)
99
+ sm = wb.create_sheet("Summary", 0)
100
+ V = lambda col: f"Views!${L[col]}$2:${L[col]}${n}" # noqa: E731
101
+ sm["A1"] = "Production cascade β€” machine and human analysis"; sm["A1"].font = Font(bold=True, size=14)
102
+ sm["A2"] = ("Machine = one gemini-3.8-flash audit of the final image (mannequin/person, angle, construction, print/material, crop). "
103
+ "Human = Pass/Fail in the Views sheet (from review.html / review_server.py). Rates update automatically as verdicts are added.")
104
+ sm["A2"].alignment = Alignment(wrap_text=True); sm.merge_cells("A2:I2"); sm.row_dimensions[2].height = 45
105
+ hdr = ["Group", "Views", "Machine clean %", "Human reviewed", "Human pass %", "Agreement %", "Klein-4B share", "Klein-9B share", "NB2 share", "Avg cost USD"]
106
+ r0 = 4
107
+ sm.append([]); sm.append(hdr)
108
+ for c in sm[r0]:
109
+ c.font, c.fill = head, hfill
110
+
111
+ def line(label, cond_col=None, cond_val=None):
112
+ crit = f',{V(cond_col)},"{cond_val}"' if cond_col else ""
113
+ cnt = f"COUNTIFS({V('View')},\"<>\"{crit})"
114
+ return [label, f"={cnt}",
115
+ f'=IFERROR(COUNTIFS({V("Machine clean")},TRUE{crit})/{cnt},"")',
116
+ f'=COUNTIFS({V("Human verdict")},"<>"{crit})',
117
+ f'=IFERROR(COUNTIFS({V("Human verdict")},"Pass"{crit})/COUNTIFS({V("Human verdict")},"<>"{crit}),"")',
118
+ f'=IFERROR(COUNTIFS({V("Agreement")},"agree"{crit})/COUNTIFS({V("Human verdict")},"<>"{crit}),"")',
119
+ *[f'=IFERROR(COUNTIFS({V("Final engine")},"{ENAME[e]}"{crit})/{cnt},"")' for e in ENGINES],
120
+ f'=IFERROR(SUMIFS({V("Est. cost USD")},{V("View")},"<>"{crit})/{cnt},"")']
121
+ sm.append(line("All views"))
122
+ for v, name in zip(VIEWS, ("Front", "45Β° (hero)", "Side", "Back")):
123
+ sm.append(line(f"View: {name}", "View", v))
124
+ for s in ("Goddiva", "Product feed"):
125
+ sm.append(line(f"Source: {s}", "Source", s))
126
+ for e in ENGINES:
127
+ sm.append(line(f"Final engine: {ENAME[e]}", "Final engine", ENAME[e]))
128
+ strata = sorted({r["Stratum"] for r in rows})
129
+ for st in strata:
130
+ sm.append(line(f"Stratum: {st}", "Stratum", st))
131
+ last = sm.max_row
132
+ for row in sm.iter_rows(min_row=r0 + 1, max_row=last):
133
+ for c in row[2:3] + row[4:9]:
134
+ c.number_format = "0.0%"
135
+ row[9].number_format = "$0.000"
136
+ sm.column_dimensions["A"].width = 34
137
+ for col in "BCDEFGHIJ":
138
+ sm.column_dimensions[col].width = 15
139
+ sm.cell(last + 2, 1, "Engine tries (all attempts)").font = Font(bold=True)
140
+ for k, e in enumerate(ENGINES):
141
+ sm.cell(last + 3 + k, 1, ENAME[e]); sm.cell(last + 3 + k, 2, f'=COUNTIF({V(ENAME[e] + " verdict")},"*")')
142
+ sm.cell(last + 3 + k, 3, f'=IFERROR(COUNTIF({V(ENAME[e] + " verdict")},"clean")/COUNTIF({V(ENAME[e] + " verdict")},"*"),"")').number_format = "0.0%"
143
+ sm.cell(last + 2, 2, "tries").font = Font(bold=True); sm.cell(last + 2, 3, "machine clean % of its tries").font = Font(bold=True)
144
+ sm.cell(last + 7, 1, "Total estimated cost (USD)"); sm.cell(last + 7, 2, f"=SUM({V('Est. cost USD')})").number_format = "$0.00"
145
+ sm.cell(last + 8, 1, "Total render time (hours)"); sm.cell(last + 8, 2, f"=SUM({V('Render seconds')})/3600").number_format = "0.0"
146
+
147
+ # --- Products
148
+ pw = wb.create_sheet("Products")
149
+ pcols = list(products[0].keys())
150
+ pw.append(pcols)
151
+ for c in pw[1]:
152
+ c.font, c.fill = head, hfill
153
+ for p in products:
154
+ pw.append([p[c] for c in pcols])
155
+ pw.column_dimensions["C"].width = 44; pw.freeze_panes = "B2"; pw.auto_filter.ref = f"A1:{get_column_letter(len(pcols))}{len(products) + 1}"
156
+
157
+ wb.save(out)
158
+ return f"{out}: {len(rows)} views, {len(products)} products, {nh} human verdicts merged"
159
+
160
+
161
+ def main() -> None:
162
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
163
+ ap.add_argument("--batch", type=Path, default=Path("/home/admin/cascade-run/batch"))
164
+ ap.add_argument("--human", nargs="*", default=[], help="review_*.json exports or the server's reviews.json")
165
+ ap.add_argument("--out", type=Path)
166
+ a = ap.parse_args()
167
+ human = [h for f in a.human for h in json.loads(Path(f).read_text())]
168
+ print(build(a.batch, human, a.out or a.batch / "cascade_report.xlsx"))
169
+
170
+
171
+ if __name__ == "__main__":
172
+ main()