Pointf5ive commited on
Commit
1292735
Β·
verified Β·
1 Parent(s): fa8670d

Deploy Codex Extractor Gradio app

Browse files
Files changed (1) hide show
  1. app.py +1101 -0
app.py ADDED
@@ -0,0 +1,1101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from html import escape
5
+ from pathlib import Path
6
+
7
+ import gradio as gr
8
+ import pandas as pd
9
+
10
+ from src.totem_workbook import (
11
+ DEFAULT_WORKBOOK,
12
+ LOG_COLUMNS,
13
+ METRICS,
14
+ export_updated_workbook,
15
+ manuscript_tracker_table,
16
+ recalculate_log,
17
+ score_log,
18
+ score_single_row,
19
+ viability_table,
20
+ workbook_overview,
21
+ workbook_path,
22
+ workstack_table,
23
+ )
24
+
25
+ from src.codex_extractor import process_upload, format_fingerprint_report
26
+
27
+ ORIGINAL_WORKBOOK_PATH = "data/order69_macmillan_totem_rebuilt.xlsx"
28
+
29
+ CSS = """
30
+ :root {
31
+ --studio-green: #0e4a1d;
32
+ --studio-green-2: #17642a;
33
+ --studio-gold: #e4aa1a;
34
+ --studio-cream: #fffaf0;
35
+ --studio-ink: #15351d;
36
+ --studio-muted: #6d725f;
37
+ --studio-line: #eadfbd;
38
+ --studio-red: #cf4b3f;
39
+ }
40
+
41
+ .gradio-container {
42
+ max-width: none !important;
43
+ padding: 0 !important;
44
+ background: #fffaf0 !important;
45
+ color: var(--studio-ink) !important;
46
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
47
+ }
48
+
49
+ footer { display: none !important; }
50
+
51
+ #hidden-export, #hidden-status {
52
+ max-width: 1180px;
53
+ margin: 0 auto 18px auto;
54
+ }
55
+
56
+ #studio-actions {
57
+ max-width: 1180px;
58
+ margin: -705px auto 16px auto;
59
+ padding-left: 18px;
60
+ position: relative;
61
+ z-index: 5;
62
+ }
63
+
64
+ #studio-actions .wrap {
65
+ max-width: 430px;
66
+ display: grid;
67
+ grid-template-columns: 1fr 1fr;
68
+ gap: 12px;
69
+ }
70
+
71
+ #studio-actions button {
72
+ border-radius: 8px !important;
73
+ min-height: 48px !important;
74
+ font-weight: 800 !important;
75
+ }
76
+
77
+ #path-panel {
78
+ max-width: 1180px;
79
+ margin: 18px auto;
80
+ padding: 0 18px;
81
+ }
82
+
83
+ #path-panel .wrap {
84
+ display: grid;
85
+ grid-template-columns: minmax(360px, 1fr) 210px;
86
+ gap: 12px;
87
+ max-width: 760px;
88
+ }
89
+
90
+ #path-panel textarea,
91
+ #path-panel input {
92
+ border-radius: 8px !important;
93
+ border: 1px solid var(--studio-line) !important;
94
+ background: white !important;
95
+ }
96
+
97
+ #path-panel button {
98
+ border-radius: 8px !important;
99
+ min-height: 52px !important;
100
+ font-weight: 800 !important;
101
+ }
102
+
103
+ #score-panel {
104
+ max-width: 1180px;
105
+ margin: 18px auto 44px auto;
106
+ padding: 0 18px;
107
+ }
108
+
109
+ #score-panel .score-card {
110
+ border: 1px solid var(--studio-line);
111
+ background: #fffef8;
112
+ border-radius: 8px;
113
+ padding: 18px;
114
+ }
115
+
116
+ #score-panel h3 {
117
+ margin: 0 0 12px 0;
118
+ font-size: 18px;
119
+ color: var(--studio-green);
120
+ }
121
+
122
+ #score-panel button {
123
+ border-radius: 8px !important;
124
+ font-weight: 800 !important;
125
+ }
126
+
127
+ #score-panel .wrap {
128
+ gap: 12px;
129
+ }
130
+
131
+ #codex-panel {
132
+ max-width: 1180px;
133
+ margin: 18px auto 44px auto;
134
+ padding: 0 18px;
135
+ }
136
+
137
+ #codex-panel .codex-header {
138
+ background: linear-gradient(135deg, #0e4a1d, #17642a);
139
+ border-radius: 8px 8px 0 0;
140
+ padding: 20px 24px;
141
+ color: white;
142
+ }
143
+
144
+ #codex-panel .codex-header h3 {
145
+ margin: 0;
146
+ color: #f8e838;
147
+ font-size: 20px;
148
+ font-family: Georgia, serif;
149
+ }
150
+
151
+ #codex-panel .codex-header p {
152
+ margin: 6px 0 0;
153
+ color: #d9ead0;
154
+ font-size: 13px;
155
+ }
156
+
157
+ #codex-panel .codex-body {
158
+ border: 1px solid var(--studio-line);
159
+ border-top: none;
160
+ border-radius: 0 0 8px 8px;
161
+ padding: 24px;
162
+ background: #fffef8;
163
+ }
164
+
165
+ #codex-panel .confidence-high {
166
+ background: #e8f5e9;
167
+ border: 1px solid #a5d6a7;
168
+ border-radius: 6px;
169
+ padding: 10px 14px;
170
+ color: #1b5e20;
171
+ font-weight: 700;
172
+ }
173
+
174
+ #codex-panel .confidence-medium {
175
+ background: #fff8e1;
176
+ border: 1px solid #ffe082;
177
+ border-radius: 6px;
178
+ padding: 10px 14px;
179
+ color: #e65100;
180
+ font-weight: 700;
181
+ }
182
+
183
+ #codex-panel .confidence-low {
184
+ background: #ffebee;
185
+ border: 1px solid #ef9a9a;
186
+ border-radius: 6px;
187
+ padding: 10px 14px;
188
+ color: #b71c1c;
189
+ font-weight: 700;
190
+ }
191
+
192
+ .dataframe, .table-wrap, .sheet, .tabs, .tabitem {
193
+ border-radius: 8px !important;
194
+ }
195
+
196
+ @media (max-width: 900px) {
197
+ #studio-actions {
198
+ margin-top: 0;
199
+ padding: 14px;
200
+ }
201
+ #studio-actions .wrap,
202
+ #path-panel .wrap {
203
+ grid-template-columns: 1fr;
204
+ }
205
+ }
206
+ """
207
+
208
+
209
+ REVISION_ACTIONS = {
210
+ "Clarity": "Simplify the line and sharpen the subject/action.",
211
+ "Rhythm": "Rework beat pattern and remove drag.",
212
+ "Read-aloud Flow": "Run a speak-test pass and cut mouth knots.",
213
+ "Emotional Truth": "Anchor the feeling in the child-facing moment.",
214
+ "Visual Strength": "Sharpen the drawable page beat.",
215
+ "Commercial Publishability": "Tighten hook, age fit, and list-readiness.",
216
+ }
217
+
218
+
219
+ def _clean_path(uploaded_file) -> Path:
220
+ return workbook_path(uploaded_file)
221
+
222
+
223
+ def _validate_workbook_path(path: Path) -> Path:
224
+ if not path.exists():
225
+ raise gr.Error(f"Workbook path does not exist: {path}")
226
+ if path.suffix.lower() not in {".xlsx", ".xlsm"}:
227
+ raise gr.Error("Upload or load an Excel workbook: .xlsx or .xlsm.")
228
+ return path
229
+
230
+
231
+ def _score_summary(log_df: pd.DataFrame | None) -> str:
232
+ if log_df is None or log_df.empty:
233
+ return "No scored rows yet."
234
+ scored = log_df[log_df["Weighted Score"].astype(str) != ""].copy()
235
+ if scored.empty:
236
+ return "No scored rows yet."
237
+ scored["Weighted Score"] = pd.to_numeric(scored["Weighted Score"], errors="coerce")
238
+ average = round(float(scored["Weighted Score"].mean()), 1)
239
+ revisions = int((scored["Revision Flag"] == "Yes").sum())
240
+ return f"{len(scored)} scored rows. Average weighted score {average}/10. Revision flags {revisions}."
241
+
242
+
243
+ def _metric_value(log_df: pd.DataFrame, metric: str, fallback: float) -> int:
244
+ if log_df is not None and not log_df.empty and metric in log_df:
245
+ values = pd.to_numeric(log_df[metric], errors="coerce").dropna()
246
+ if not values.empty:
247
+ return int(round(float(values.mean()) * 10))
248
+ return int(round(fallback * 10))
249
+
250
+
251
+ def _small_spark(value: int, tone: str) -> str:
252
+ heights = [19, 23, 16, 18, 17, 20, 22, 30, 25, 29, 34, 27]
253
+ color = "#3f8f2f" if tone == "green" else "#dda10c" if tone == "gold" else "#d84f45"
254
+ bars = "".join(f"<i style='height:{h}px;background:{color}'></i>" for h in heights)
255
+ return f"<div class='spark' aria-label='score trend {value}'>{bars}</div>"
256
+
257
+
258
+ def _kpi_card(title: str, value: int, icon: str, tone: str, delta: str) -> str:
259
+ return f"""
260
+ <article class="kpi-card">
261
+ <div class="kpi-top">
262
+ <span class="kpi-icon {tone}">{icon}</span>
263
+ <div><b>{escape(title)}</b><strong>{value}<em>/100</em></strong></div>
264
+ </div>
265
+ {_small_spark(value, tone)}
266
+ <p class="{tone}">{escape(delta)}</p>
267
+ </article>
268
+ """
269
+
270
+
271
+ def _priority_badge(gate: str) -> tuple[str, str]:
272
+ if gate in {"HARD FAIL", "SOFT FAIL", "READ-ALOUD BLOCK"}:
273
+ return "High", "high"
274
+ if gate in {"COMMERCIAL CHECK", "REVISE"}:
275
+ return "Medium", "medium"
276
+ return "Low", "low"
277
+
278
+
279
+ def _revision_rows(log_df: pd.DataFrame, tracker_df: pd.DataFrame) -> str:
280
+ rows = []
281
+ if log_df is not None and not log_df.empty:
282
+ working = log_df.copy()
283
+ working["Weighted Score"] = pd.to_numeric(working["Weighted Score"], errors="coerce")
284
+ working = working.sort_values(["Revision Flag", "Weighted Score"], ascending=[False, True])
285
+ for _, row in working.head(5).iterrows():
286
+ metric = str(row.get("Priority Fix") or "Read-aloud Flow")
287
+ gate = str(row.get("Gate") or "REVISE")
288
+ priority, cls = _priority_badge(gate)
289
+ block = str(row.get("Stanza ID") or row.get("Sequence") or "Live block")
290
+ action = REVISION_ACTIONS.get(metric, "Revise the weakest pressure point first.")
291
+ rows.append(
292
+ f"""
293
+ <div class="queue-row">
294
+ <span>{escape(block)}</span>
295
+ <span>{escape(metric)}</span>
296
+ <span>{escape(gate.title())}</span>
297
+ <b class="{cls}">{priority}</b>
298
+ <span>{escape(action)}</span>
299
+ </div>
300
+ """
301
+ )
302
+
303
+ if len(rows) < 5 and tracker_df is not None and not tracker_df.empty:
304
+ for _, row in tracker_df.head(5 - len(rows)).iterrows():
305
+ block = str(row.get("Block", "Block"))
306
+ metric = str(row.get("TOTEM priority", "Read-aloud Flow"))
307
+ action = str(row.get("Next action", REVISION_ACTIONS.get(metric, "Continue next pass.")))
308
+ rows.append(
309
+ f"""
310
+ <div class="queue-row">
311
+ <span>{escape(block)}</span>
312
+ <span>{escape(metric)}</span>
313
+ <span>Development Gate</span>
314
+ <b class="medium">Medium</b>
315
+ <span>{escape(action)}</span>
316
+ </div>
317
+ """
318
+ )
319
+
320
+ return "\n".join(rows) or "<p class='empty'>No revision rows found.</p>"
321
+
322
+
323
+ def _risk_cards(log_df: pd.DataFrame) -> str:
324
+ if log_df is None or log_df.empty:
325
+ risks = [("Workbook Intake", "No scored rows detected yet.", "Medium Risk", "medium", "▁▂▃▂▁▂")]
326
+ else:
327
+ counts: dict[str, int] = {}
328
+ for metric in METRICS:
329
+ values = pd.to_numeric(log_df[metric], errors="coerce").dropna()
330
+ weak = int((values < 7).sum())
331
+ if weak:
332
+ counts[metric] = weak
333
+ if not counts:
334
+ counts = {"Commercial Publishability": 1}
335
+ ordered = sorted(counts.items(), key=lambda item: item[1], reverse=True)[:3]
336
+ risks = []
337
+ for metric, count in ordered:
338
+ tone = "high" if count >= 2 else "medium"
339
+ label = "High Risk" if tone == "high" else "Medium Risk"
340
+ risks.append((metric, f"Detected under target in {count} scored block(s).", label, tone, "▂▅▃▇▁▆▂▅"))
341
+
342
+ cards = []
343
+ icons = {
344
+ "Read-aloud Flow": "≋", "Rhythm": "≋", "Visual Strength": "β—‰",
345
+ "Emotional Truth": "β™‘", "Commercial Publishability": "β†—",
346
+ }
347
+ for metric, detail, label, tone, bars in risks:
348
+ cards.append(
349
+ f"""
350
+ <div class="risk-row">
351
+ <span class="risk-icon {tone}">{icons.get(metric, "!")}</span>
352
+ <div><b>{escape(metric)}</b><small>{escape(detail)}</small></div>
353
+ <em class="{tone}">{escape(label)}</em>
354
+ <code>{escape(bars)}</code>
355
+ </div>
356
+ """
357
+ )
358
+ return "\n".join(cards)
359
+
360
+
361
+ def _recent_workbooks(path: Path, overall: int) -> str:
362
+ name = path.stem.replace("_", " ")
363
+ return f"""
364
+ <div class="book-card">
365
+ <div class="book-cover">TOTEM</div>
366
+ <div>
367
+ <b>{escape(name[:38])}</b>
368
+ <small>Current workbook Β· Loaded now</small>
369
+ <div class="progress"><i style="width:{max(8, min(overall, 100))}%"></i></div>
370
+ </div>
371
+ <strong>{overall}%</strong>
372
+ </div>
373
+ """
374
+
375
+
376
+ def dashboard_html(path: Path, notice: str = "") -> str:
377
+ path = _validate_workbook_path(path)
378
+ overview = workbook_overview(path)
379
+ log_df = score_log(path)
380
+ viability_df, viability_summary = viability_table(path)
381
+ tracker_df = manuscript_tracker_table(path)
382
+ workstack_df = workstack_table(path)
383
+
384
+ avg_viability = float(viability_df["Score"].mean()) if viability_df is not None and not viability_df.empty else 0
385
+ overall = int(round(avg_viability * 10)) if avg_viability else 0
386
+ read_flow = _metric_value(log_df, "Read-aloud Flow", 6.4)
387
+ emotional = _metric_value(log_df, "Emotional Truth", 7.8)
388
+ visual = _metric_value(log_df, "Visual Strength", 6.9)
389
+ commercial = _metric_value(log_df, "Commercial Publishability", avg_viability or 7.1)
390
+ weakest = "Read-aloud Flow"
391
+ strongest = "Emotional Truth"
392
+ if log_df is not None and not log_df.empty:
393
+ metric_means = {
394
+ metric: pd.to_numeric(log_df[metric], errors="coerce").dropna().mean()
395
+ for metric in METRICS
396
+ }
397
+ metric_means = {metric: value for metric, value in metric_means.items() if pd.notna(value)}
398
+ if metric_means:
399
+ weakest = min(metric_means, key=metric_means.get)
400
+ strongest = max(metric_means, key=metric_means.get)
401
+
402
+ next_item = ""
403
+ if workstack_df is not None and not workstack_df.empty and "Status" in workstack_df:
404
+ active = workstack_df[workstack_df["Status"].isin(["Active", "Queued"])]
405
+ if not active.empty:
406
+ next_item = str(active.iloc[0].get("Next item", "Run next pass"))
407
+ next_item = next_item or "Run the next TOTEM pass"
408
+
409
+ notice_block = f"<div class='notice'>{escape(notice)}</div>" if notice else ""
410
+
411
+ return f"""
412
+ <style>
413
+ .studio-shell {{
414
+ min-height: 900px;
415
+ display: grid;
416
+ grid-template-columns: 250px 1fr;
417
+ background: var(--studio-cream);
418
+ }}
419
+ .studio-sidebar {{
420
+ background: linear-gradient(180deg, #145b23 0%, #063f18 100%);
421
+ color: white;
422
+ padding: 22px 13px;
423
+ }}
424
+ .brand {{
425
+ padding: 4px 10px 22px 10px;
426
+ }}
427
+ .brand h1 {{
428
+ margin: 0;
429
+ color: #f8e838;
430
+ font-size: 28px;
431
+ line-height: .82;
432
+ text-shadow: 0 2px 0 #2b8c23;
433
+ letter-spacing: 0;
434
+ }}
435
+ .brand small {{
436
+ display: block;
437
+ margin-top: 13px;
438
+ letter-spacing: 4px;
439
+ font-size: 11px;
440
+ }}
441
+ .nav-item {{
442
+ display: flex;
443
+ gap: 12px;
444
+ align-items: center;
445
+ padding: 14px 13px;
446
+ margin: 6px 0;
447
+ border-radius: 8px;
448
+ color: #eef6e8;
449
+ font-weight: 700;
450
+ }}
451
+ .nav-item.active {{
452
+ background: linear-gradient(90deg, #f5c93c, #e5a721);
453
+ color: white;
454
+ }}
455
+ .sidebar-foot {{
456
+ margin-top: 170px;
457
+ border-top: 1px solid rgba(255,255,255,.12);
458
+ padding: 24px 10px;
459
+ color: #d9ead0;
460
+ font-size: 13px;
461
+ }}
462
+ .studio-main {{
463
+ background: linear-gradient(180deg, #fffdf7 0%, #fff7de 42%, #fffaf0 100%);
464
+ }}
465
+ .topbar {{
466
+ height: 72px;
467
+ display: flex;
468
+ align-items: center;
469
+ justify-content: space-between;
470
+ gap: 24px;
471
+ padding: 0 28px;
472
+ border-bottom: 1px solid var(--studio-line);
473
+ }}
474
+ .search {{
475
+ flex: 1;
476
+ max-width: 640px;
477
+ border: 1px solid var(--studio-line);
478
+ border-radius: 999px;
479
+ padding: 13px 18px;
480
+ background: rgba(255,255,255,.78);
481
+ color: #8d8d78;
482
+ }}
483
+ .profile {{
484
+ display: flex;
485
+ align-items: center;
486
+ gap: 14px;
487
+ font-weight: 800;
488
+ }}
489
+ .avatar {{
490
+ width: 46px;
491
+ height: 46px;
492
+ border-radius: 50%;
493
+ background: linear-gradient(135deg, #c8915e, #f3d3ad);
494
+ display: grid;
495
+ place-items: center;
496
+ color: #5b2b13;
497
+ }}
498
+ .hero {{
499
+ position: relative;
500
+ padding: 34px 36px 28px;
501
+ min-height: 205px;
502
+ overflow: hidden;
503
+ }}
504
+ .hero::before {{
505
+ content: "";
506
+ position: absolute;
507
+ inset: 44px 0 0 0;
508
+ background: radial-gradient(circle at 72% 32%, rgba(246,207,80,.28), transparent 18%),
509
+ radial-gradient(circle at 86% 65%, rgba(89,145,49,.12), transparent 20%),
510
+ linear-gradient(160deg, transparent 0 35%, rgba(238,192,66,.18) 36% 52%, transparent 53%);
511
+ }}
512
+ .hero h2 {{
513
+ position: relative;
514
+ margin: 0;
515
+ font-family: Georgia, serif;
516
+ font-size: 44px;
517
+ color: #0e4a1d;
518
+ letter-spacing: 0;
519
+ }}
520
+ .hero p {{
521
+ position: relative;
522
+ margin: 8px 0 22px;
523
+ font-size: 20px;
524
+ color: #355b35;
525
+ }}
526
+ .mascot {{
527
+ position: absolute;
528
+ right: 88px;
529
+ top: 10px;
530
+ width: 156px;
531
+ height: 156px;
532
+ border-radius: 50%;
533
+ background: radial-gradient(circle at 48% 43%, #fff7db 0 18%, #e7b96c 19% 48%, #3f8f2f 49% 61%, transparent 62%),
534
+ radial-gradient(circle at 32% 11%, #f6d08f 0 13%, transparent 14%),
535
+ radial-gradient(circle at 70% 8%, #f6d08f 0 13%, transparent 14%);
536
+ box-shadow: 0 15px 35px rgba(120,88,24,.18);
537
+ }}
538
+ .content {{
539
+ padding: 0 28px 34px;
540
+ }}
541
+ .kpi-grid {{
542
+ display: grid;
543
+ grid-template-columns: repeat(5, minmax(150px, 1fr));
544
+ gap: 14px;
545
+ }}
546
+ .kpi-card, .panel {{
547
+ background: rgba(255,255,255,.88);
548
+ border: 1px solid var(--studio-line);
549
+ border-radius: 8px;
550
+ box-shadow: 0 10px 28px rgba(58,44,14,.06);
551
+ }}
552
+ .kpi-card {{
553
+ padding: 18px;
554
+ }}
555
+ .kpi-top {{
556
+ display: flex;
557
+ align-items: center;
558
+ gap: 14px;
559
+ }}
560
+ .kpi-icon {{
561
+ width: 48px;
562
+ height: 48px;
563
+ border-radius: 50%;
564
+ display: grid;
565
+ place-items: center;
566
+ color: white;
567
+ font-weight: 900;
568
+ font-size: 24px;
569
+ }}
570
+ .green {{ color: #348426; }}
571
+ .gold {{ color: #d69200; }}
572
+ .red {{ color: #d84f45; }}
573
+ .kpi-icon.green {{ background: #4e982f; color: white; }}
574
+ .kpi-icon.gold {{ background: #e6a500; color: white; }}
575
+ .kpi-icon.red {{ background: #d84f45; color: white; }}
576
+ .kpi-card b {{
577
+ display: block;
578
+ font-size: 13px;
579
+ color: var(--studio-ink);
580
+ }}
581
+ .kpi-card strong {{
582
+ display: block;
583
+ color: var(--studio-ink);
584
+ font-size: 34px;
585
+ line-height: 1;
586
+ }}
587
+ .kpi-card em {{
588
+ font-size: 13px;
589
+ font-style: normal;
590
+ color: var(--studio-muted);
591
+ }}
592
+ .spark {{
593
+ height: 42px;
594
+ display: flex;
595
+ align-items: end;
596
+ gap: 8px;
597
+ margin: 12px 0 5px;
598
+ }}
599
+ .spark i {{
600
+ width: 4px;
601
+ border-radius: 4px;
602
+ }}
603
+ .kpi-card p {{
604
+ margin: 0;
605
+ font-size: 12px;
606
+ background: transparent !important;
607
+ }}
608
+ .dashboard-grid {{
609
+ display: grid;
610
+ grid-template-columns: 1.55fr 1fr;
611
+ gap: 14px;
612
+ margin-top: 16px;
613
+ }}
614
+ .panel h3 {{
615
+ margin: 0;
616
+ padding: 16px 18px;
617
+ border-bottom: 1px solid var(--studio-line);
618
+ color: var(--studio-ink);
619
+ }}
620
+ .queue-row {{
621
+ display: grid;
622
+ grid-template-columns: 1.15fr .9fr .85fr .55fr 1.35fr;
623
+ gap: 10px;
624
+ align-items: center;
625
+ padding: 12px 18px;
626
+ border-bottom: 1px solid #f0e7cf;
627
+ font-size: 13px;
628
+ }}
629
+ .queue-row b, .risk-row em {{
630
+ text-align: center;
631
+ border-radius: 999px;
632
+ padding: 5px 9px;
633
+ font-style: normal;
634
+ }}
635
+ .high {{ background: #ffe3da; color: var(--studio-red); }}
636
+ .medium {{ background: #fff0c5; color: #c28600; }}
637
+ .low {{ background: #e9f5dc; color: #4b8e2e; }}
638
+ .risk-row {{
639
+ display: grid;
640
+ grid-template-columns: 54px 1fr 96px 86px;
641
+ gap: 12px;
642
+ align-items: center;
643
+ padding: 14px 18px;
644
+ border-bottom: 1px solid #f0e7cf;
645
+ }}
646
+ .risk-icon {{
647
+ width: 46px;
648
+ height: 46px;
649
+ display: grid;
650
+ place-items: center;
651
+ border-radius: 50%;
652
+ color: white;
653
+ font-weight: 900;
654
+ background: #e6a500;
655
+ }}
656
+ .risk-icon.high {{ background: #d84f45; color: white; }}
657
+ .risk-icon.medium {{ background: #e6a500; color: white; }}
658
+ .risk-row small {{
659
+ display: block;
660
+ color: var(--studio-muted);
661
+ margin-top: 4px;
662
+ }}
663
+ .risk-row code {{
664
+ color: #d84f45;
665
+ font-size: 24px;
666
+ letter-spacing: 1px;
667
+ }}
668
+ .bottom-grid {{
669
+ display: grid;
670
+ grid-template-columns: 1fr 1fr;
671
+ gap: 14px;
672
+ margin-top: 16px;
673
+ }}
674
+ .book-card {{
675
+ display: grid;
676
+ grid-template-columns: 58px 1fr 48px;
677
+ align-items: center;
678
+ gap: 14px;
679
+ padding: 14px 18px;
680
+ }}
681
+ .book-cover {{
682
+ width: 54px;
683
+ height: 76px;
684
+ display: grid;
685
+ place-items: center;
686
+ border-radius: 6px;
687
+ background: linear-gradient(160deg, #072d18, #135b25);
688
+ color: #f2d444;
689
+ font-weight: 900;
690
+ font-size: 11px;
691
+ }}
692
+ .book-card small {{
693
+ display: block;
694
+ color: var(--studio-muted);
695
+ margin: 5px 0 12px;
696
+ }}
697
+ .progress {{
698
+ height: 7px;
699
+ border-radius: 999px;
700
+ background: #eadfbd;
701
+ }}
702
+ .progress i {{
703
+ display: block;
704
+ height: 7px;
705
+ border-radius: 999px;
706
+ background: #4e982f;
707
+ }}
708
+ .snapshot {{
709
+ display: grid;
710
+ grid-template-columns: repeat(3, 1fr);
711
+ gap: 8px;
712
+ padding: 18px;
713
+ }}
714
+ .snapshot div {{
715
+ border-right: 1px solid var(--studio-line);
716
+ min-height: 82px;
717
+ }}
718
+ .snapshot div:last-child {{
719
+ border-right: none;
720
+ }}
721
+ .snapshot small {{
722
+ color: var(--studio-muted);
723
+ display: block;
724
+ }}
725
+ .snapshot b {{
726
+ display: block;
727
+ margin-top: 9px;
728
+ }}
729
+ .snapshot strong {{
730
+ font-size: 28px;
731
+ }}
732
+ .notice {{
733
+ margin: 0 28px 16px;
734
+ padding: 12px 14px;
735
+ background: #e9f5dc;
736
+ border: 1px solid #c5ddb5;
737
+ border-radius: 8px;
738
+ font-weight: 700;
739
+ }}
740
+ @media (max-width: 1100px) {{
741
+ .studio-shell {{ grid-template-columns: 1fr; }}
742
+ .studio-sidebar {{ display: none; }}
743
+ .kpi-grid, .dashboard-grid, .bottom-grid {{ grid-template-columns: 1fr; }}
744
+ .mascot {{ display: none; }}
745
+ .queue-row {{ grid-template-columns: 1fr; }}
746
+ }}
747
+ </style>
748
+ <div class="studio-shell">
749
+ <aside class="studio-sidebar">
750
+ <div class="brand">
751
+ <h1>TOTEM<br>Studio</h1>
752
+ <small>PUBLISHING INTERFACE</small>
753
+ </div>
754
+ <div class="nav-item active">βŒ‚ Home</div>
755
+ <div class="nav-item">β–£ Projects</div>
756
+ <div class="nav-item">β–€ Workbook Upload</div>
757
+ <div class="nav-item">β–₯ TOTEM Analytics</div>
758
+ <div class="nav-item">☷ Revision Queue</div>
759
+ <div class="nav-item">⬑ Risk Clusters</div>
760
+ <div class="nav-item">β—ˆ Codex Extractor</div>
761
+ <div class="nav-item">⇩ Export</div>
762
+ <div class="sidebar-foot">Knowledge. Structure.<br>Story. Performance.</div>
763
+ </aside>
764
+ <main class="studio-main">
765
+ <div class="topbar">
766
+ <div class="search">Search projects, workbooks, blocks...</div>
767
+ <div class="profile"><span>Editorial Workspace</span><span>πŸ””</span><span class="avatar">T</span><span>TOTEM<br><small>Studio</small></span></div>
768
+ </div>
769
+ <section class="hero">
770
+ <h2>TOTEM Studio.</h2>
771
+ <p>Data-driven insight for stronger stories.</p>
772
+ <div class="mascot" aria-label="TOTEM Studio mascot"></div>
773
+ </section>
774
+ {notice_block}
775
+ <section class="content">
776
+ <div class="kpi-grid">
777
+ {_kpi_card("Overall Publishability", overall, "✦", "green", "Source: viability lens")}
778
+ {_kpi_card("Read-Aloud Flow", read_flow, "≋", "red" if read_flow < 65 else "gold", "Weakest live pressure")}
779
+ {_kpi_card("Emotional Truth", emotional, "β™‘", "green", "Strongest story signal")}
780
+ {_kpi_card("Visual Strength", visual, "β—‰", "gold" if visual < 75 else "green", "Drawable page value")}
781
+ {_kpi_card("Commercial Viability", commercial, "β†—", "green", "Publisher-facing lens")}
782
+ </div>
783
+ <div class="dashboard-grid">
784
+ <section class="panel">
785
+ <h3>Revision Priority Queue <small>{len(log_df) if log_df is not None else 0} live item(s)</small></h3>
786
+ <div class="queue-row header"><b>Block</b><b>Weakest Dimension</b><b>Gate</b><b>Priority</b><b>Recommended Action</b></div>
787
+ {_revision_rows(log_df, tracker_df)}
788
+ </section>
789
+ <section class="panel">
790
+ <h3>Risk Clusters</h3>
791
+ {_risk_cards(log_df)}
792
+ </section>
793
+ </div>
794
+ <div class="bottom-grid">
795
+ <section class="panel">
796
+ <h3>Recent Workbook</h3>
797
+ {_recent_workbooks(path, overall)}
798
+ </section>
799
+ <section class="panel">
800
+ <h3>TOTEM Snapshot <small>Based on latest run</small></h3>
801
+ <div class="snapshot">
802
+ <div><small>Weakest Dimension</small><b>{escape(weakest)}</b><strong>{read_flow}</strong><small>/100</small></div>
803
+ <div><small>Strongest Dimension</small><b>{escape(strongest)}</b><strong>{max(emotional, visual)}</strong><small>/100</small></div>
804
+ <div><small>Next Work</small><b>{escape(next_item[:42])}</b><small>{escape(viability_summary)}</small></div>
805
+ </div>
806
+ </section>
807
+ </div>
808
+ <p style="color:#6d725f;font-size:12px;margin:18px 0 0;">Loaded {escape(path.name)} Β· {overview['sheet_count']} sheets read privately Β· matrix hidden from the product surface.</p>
809
+ </section>
810
+ </main>
811
+ </div>
812
+ """
813
+
814
+
815
+ # ── CODEX EXTRACTOR FUNCTIONS ─────────────────────────────────────────────────
816
+
817
+ def run_codex_extraction(
818
+ file_obj,
819
+ author_name: str,
820
+ author_id: str,
821
+ works_sampled: str,
822
+ ) -> tuple[str, str]:
823
+ """
824
+ Gradio handler for the Codex Extraction tab.
825
+ Returns (report_text, json_output) tuple.
826
+ """
827
+ if file_obj is None:
828
+ return "No file uploaded. Please upload a .txt or .pdf file.", ""
829
+
830
+ if not author_name.strip():
831
+ return "Please enter the author's full name before extracting.", ""
832
+
833
+ try:
834
+ report, fp_dict = process_upload(
835
+ file_path=file_obj,
836
+ author_name=author_name.strip(),
837
+ author_id=author_id.strip() or "CA-XXX",
838
+ works_sampled=works_sampled.strip(),
839
+ )
840
+
841
+ if not fp_dict:
842
+ return report, ""
843
+
844
+ # Format JSON output for workbook entry
845
+ json_out = json.dumps(fp_dict, indent=2)
846
+ return report, json_out
847
+
848
+ except Exception as e:
849
+ return f"Extraction error: {type(e).__name__}: {str(e)}", ""
850
+
851
+
852
+ def clear_codex_form() -> tuple[None, str, str, str, str, str]:
853
+ """Reset the Codex extraction form."""
854
+ return None, "", "CA-XXX", "", "", ""
855
+
856
+
857
+ # ── WORKBOOK FUNCTIONS ────────────────────────────────────────────────────────
858
+
859
+ def load_workbook(uploaded_file=None, notice: str = ""):
860
+ path = _validate_workbook_path(_clean_path(uploaded_file))
861
+ log_df = score_log(path)
862
+ return str(path), dashboard_html(path, notice), log_df, _score_summary(log_df)
863
+
864
+
865
+ def load_default():
866
+ return load_workbook(None, "Bundled workbook reloaded.")
867
+
868
+
869
+ def load_uploaded(uploaded_file):
870
+ if uploaded_file is None:
871
+ raise gr.Error("Choose an .xlsx or .xlsm workbook first.")
872
+ return load_workbook(uploaded_file, "Workbook uploaded and analysed.")
873
+
874
+
875
+ def load_local_path(path_text: str):
876
+ path = _validate_workbook_path(Path(path_text or "").expanduser())
877
+ log_df = score_log(path)
878
+ return str(path), dashboard_html(path, "Local workbook loaded."), log_df, _score_summary(log_df)
879
+
880
+
881
+ def run_analysis(active_path: str):
882
+ path = _validate_workbook_path(Path(active_path) if active_path else DEFAULT_WORKBOOK)
883
+ log_df = score_log(path)
884
+ return dashboard_html(path, "TOTEM analysis refreshed."), log_df, _score_summary(log_df)
885
+
886
+
887
+ def recalc_log(log_df, active_path: str):
888
+ path = _validate_workbook_path(Path(active_path) if active_path else DEFAULT_WORKBOOK)
889
+ recalculated = recalculate_log(log_df, path)
890
+ return dashboard_html(path, "Gates recalculated."), recalculated, _score_summary(recalculated)
891
+
892
+
893
+ def export_log(log_df, active_path: str):
894
+ path = _validate_workbook_path(Path(active_path) if active_path else DEFAULT_WORKBOOK)
895
+ return export_updated_workbook(log_df, path)
896
+
897
+
898
+ def single_score(active_path, sequence, stanza_id, draft_pass, clarity, rhythm, flow,
899
+ emotional_truth, visual_strength, commercial, notes):
900
+ path = _validate_workbook_path(Path(active_path) if active_path else DEFAULT_WORKBOOK)
901
+ df = score_single_row(path, sequence, stanza_id, draft_pass, clarity, rhythm,
902
+ flow, emotional_truth, visual_strength, commercial, notes)
903
+ return df, _score_summary(df)
904
+
905
+
906
+ # ── GRADIO INTERFACE ──────────────────────────────────────────────────────────
907
+
908
+ with gr.Blocks(title="TOTEM Studio", css=CSS) as demo:
909
+ active_path = gr.State(str(DEFAULT_WORKBOOK))
910
+ log_state = gr.State(pd.DataFrame(columns=LOG_COLUMNS))
911
+
912
+ with gr.Tabs():
913
+
914
+ # ── TAB 1: DASHBOARD ─────────────────────────────────────────────────
915
+ with gr.TabItem("Dashboard"):
916
+ dashboard = gr.HTML()
917
+
918
+ with gr.Row(elem_id="studio-actions"):
919
+ with gr.Column(elem_classes=["wrap"]):
920
+ workbook_upload = gr.UploadButton(
921
+ "Upload Workbook",
922
+ file_types=[".xlsx", ".xlsm"],
923
+ type="filepath",
924
+ variant="primary",
925
+ scale=1,
926
+ )
927
+ run_button = gr.Button("Run TOTEM Analysis", variant="secondary", scale=1)
928
+
929
+ with gr.Row(elem_id="path-panel"):
930
+ with gr.Column(elem_classes=["wrap"]):
931
+ path_input = gr.Textbox(label="Local workbook path", value=ORIGINAL_WORKBOOK_PATH)
932
+ path_button = gr.Button("Load Local Path", variant="primary")
933
+
934
+ with gr.Accordion("Private scoring controls", open=False, elem_id="score-panel"):
935
+ gr.HTML("<div class='score-card'><h3>Live Score A Block</h3></div>")
936
+ with gr.Row():
937
+ sequence = gr.Textbox(label="Sequence", value="Live pass")
938
+ stanza_id = gr.Textbox(label="Stanza ID", value="New block")
939
+ draft_pass = gr.Textbox(label="Draft / Pass", value="First score")
940
+ with gr.Row():
941
+ clarity = gr.Slider(1, 10, value=7, step=0.5, label="Clarity")
942
+ rhythm = gr.Slider(1, 10, value=7, step=0.5, label="Rhythm")
943
+ flow = gr.Slider(1, 10, value=7, step=0.5, label="Read-aloud Flow")
944
+ with gr.Row():
945
+ emotional_truth = gr.Slider(1, 10, value=7, step=0.5, label="Emotional Truth")
946
+ visual_strength = gr.Slider(1, 10, value=7, step=0.5, label="Visual Strength")
947
+ commercial = gr.Slider(1, 10, value=7, step=0.5, label="Commercial Publishability")
948
+ notes = gr.Textbox(label="Notes", lines=2)
949
+ with gr.Row():
950
+ single_button = gr.Button("Score Block", variant="primary")
951
+ recalc_button = gr.Button("Recalculate Gates")
952
+ export_button = gr.Button("Download Updated Workbook")
953
+ single_df = gr.Dataframe(label="Latest scorecard", interactive=False, visible=False)
954
+ score_status = gr.Markdown(elem_id="hidden-status")
955
+ exported_file = gr.File(label="Export appears here", elem_id="hidden-export")
956
+
957
+ # ── TAB 2: CODEX EXTRACTOR ───────────────────────────────────────────
958
+ with gr.TabItem("β—ˆ Codex Extractor"):
959
+ gr.HTML("""
960
+ <div style="
961
+ background: linear-gradient(135deg, #0e4a1d, #17642a);
962
+ border-radius: 8px;
963
+ padding: 20px 24px;
964
+ margin-bottom: 16px;
965
+ ">
966
+ <h3 style="margin:0; color:#f8e838; font-family:Georgia,serif; font-size:20px;">
967
+ Codex Fingerprint Extractor
968
+ </h3>
969
+ <p style="margin:8px 0 0; color:#d9ead0; font-size:13px;">
970
+ Upload an author's text or PDF. The extractor computes 17 Tier 1 voice metrics
971
+ (VM-001 to VM-013, VM-024 to VM-028) mathematically from the text.
972
+ Copy the output into CODEX_03_FINGERPRINTS in the workbook.
973
+ Use Codex Build Prompt 2 (ChatGPT/Gemini) for the 10 Tier 2 qualitative metrics.
974
+ </p>
975
+ </div>
976
+ """)
977
+
978
+ with gr.Row():
979
+ with gr.Column(scale=1):
980
+ gr.Markdown("### Author Details")
981
+ codex_author_name = gr.Textbox(
982
+ label="Author Full Name",
983
+ placeholder="e.g. Julia Donaldson",
984
+ )
985
+ codex_author_id = gr.Textbox(
986
+ label="Codex Author ID",
987
+ placeholder="e.g. CA-001",
988
+ value="CA-XXX",
989
+ )
990
+ codex_works = gr.Textbox(
991
+ label="Works Sampled (comma-separated)",
992
+ placeholder="e.g. The Gruffalo, Room on the Broom, Zog",
993
+ lines=2,
994
+ )
995
+ codex_file = gr.File(
996
+ label="Upload Text or PDF",
997
+ file_types=[".txt", ".pdf"],
998
+ type="filepath",
999
+ )
1000
+ with gr.Row():
1001
+ codex_extract_btn = gr.Button(
1002
+ "Extract Fingerprint",
1003
+ variant="primary",
1004
+ scale=2,
1005
+ )
1006
+ codex_clear_btn = gr.Button(
1007
+ "Clear",
1008
+ variant="secondary",
1009
+ scale=1,
1010
+ )
1011
+
1012
+ gr.HTML("""
1013
+ <div style="
1014
+ background: #fff8e1;
1015
+ border: 1px solid #ffe082;
1016
+ border-radius: 6px;
1017
+ padding: 12px 14px;
1018
+ margin-top: 12px;
1019
+ font-size: 13px;
1020
+ color: #5d4037;
1021
+ ">
1022
+ <b>File requirements:</b><br>
1023
+ β€’ PDF must contain selectable text (not scanned images)<br>
1024
+ β€’ Minimum 1,000 words for HIGH confidence fingerprint<br>
1025
+ β€’ Combine multiple works in one file to increase sample size<br>
1026
+ β€’ Visual-primary books (Van Allsburg, Jeffers) will flag LOW confidence
1027
+ </div>
1028
+ """)
1029
+
1030
+ with gr.Column(scale=2):
1031
+ gr.Markdown("### Extraction Report β€” Tier 1 Metrics")
1032
+ codex_report = gr.Textbox(
1033
+ label="",
1034
+ lines=32,
1035
+ interactive=False,
1036
+ placeholder="Upload a file and click Extract Fingerprint to see results here...",
1037
+ elem_id="codex-report",
1038
+ )
1039
+
1040
+ gr.Markdown("### Raw Output β€” Copy into CODEX_03_FINGERPRINTS")
1041
+ codex_json = gr.Textbox(
1042
+ label="",
1043
+ lines=20,
1044
+ interactive=False,
1045
+ placeholder="JSON values appear here after extraction. Copy individual metric values into the workbook row.",
1046
+ elem_id="codex-json",
1047
+ )
1048
+
1049
+ gr.HTML("""
1050
+ <div style="
1051
+ background: #e8f5e9;
1052
+ border: 1px solid #a5d6a7;
1053
+ border-radius: 6px;
1054
+ padding: 14px 18px;
1055
+ margin-top: 8px;
1056
+ font-size: 13px;
1057
+ ">
1058
+ <b style="color:#1b5e20;">Tier 2 reminder:</b>
1059
+ <span style="color:#2e7d32;">
1060
+ VM-014 (Narrative person) through VM-023 (Animal/nature imagery ratio) require
1061
+ qualitative judgment. Use Codex Build Prompt 2 from the Codex Build Prompts document
1062
+ with the same text in ChatGPT or Gemini to complete the remaining 10 metrics.
1063
+ </span>
1064
+ </div>
1065
+ """)
1066
+
1067
+ # ── EVENT WIRING ─────────────────────────────────────────────────────────
1068
+
1069
+ # Dashboard tab
1070
+ demo.load(load_workbook, outputs=[active_path, dashboard, log_state, score_status])
1071
+ workbook_upload.upload(load_uploaded, inputs=[workbook_upload],
1072
+ outputs=[active_path, dashboard, log_state, score_status])
1073
+ run_button.click(run_analysis, inputs=[active_path],
1074
+ outputs=[dashboard, log_state, score_status])
1075
+ path_button.click(load_local_path, inputs=[path_input],
1076
+ outputs=[active_path, dashboard, log_state, score_status])
1077
+ recalc_button.click(recalc_log, inputs=[log_state, active_path],
1078
+ outputs=[dashboard, log_state, score_status])
1079
+ export_button.click(export_log, inputs=[log_state, active_path], outputs=[exported_file])
1080
+ single_button.click(
1081
+ single_score,
1082
+ inputs=[active_path, sequence, stanza_id, draft_pass, clarity, rhythm,
1083
+ flow, emotional_truth, visual_strength, commercial, notes],
1084
+ outputs=[single_df, score_status],
1085
+ )
1086
+
1087
+ # Codex Extractor tab
1088
+ codex_extract_btn.click(
1089
+ run_codex_extraction,
1090
+ inputs=[codex_file, codex_author_name, codex_author_id, codex_works],
1091
+ outputs=[codex_report, codex_json],
1092
+ )
1093
+ codex_clear_btn.click(
1094
+ clear_codex_form,
1095
+ outputs=[codex_file, codex_author_name, codex_author_id, codex_works,
1096
+ codex_report, codex_json],
1097
+ )
1098
+
1099
+
1100
+ if __name__ == "__main__":
1101
+ demo.launch()