Spaces:
Sleeping
Sleeping
Pointf5ive commited on
Commit ·
7ae21b5
1
Parent(s): 9674914
stage 2: dashboard state contract
Browse filesScope: add dashboard state contract, gauge formula, escaping helper, and boundary adapter without changing existing scoring logic or callback return formats.
Validation: py_compile + contract smoke checks passed (0/35/weighted signal behavior verified); stage acceptance gate passed.
app.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
import hashlib
|
|
|
|
| 4 |
import json
|
| 5 |
import re
|
| 6 |
from html import escape
|
|
@@ -350,6 +351,173 @@ REVISION_ACTIONS = {
|
|
| 350 |
"Commercial Publishability": "Tighten hook, age fit, and list-readiness.",
|
| 351 |
}
|
| 352 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
|
| 354 |
def _clean_path(uploaded_file) -> Path:
|
| 355 |
return workbook_path(uploaded_file)
|
|
@@ -1439,4 +1607,9 @@ with gr.Blocks(title="TOTEM Studio") as demo:
|
|
| 1439 |
|
| 1440 |
|
| 1441 |
if __name__ == "__main__":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1442 |
demo.launch(ssr_mode=False, css=CSS + TOTEM_CSS + SS_CSS, head=HEAD)
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
import hashlib
|
| 4 |
+
import html
|
| 5 |
import json
|
| 6 |
import re
|
| 7 |
from html import escape
|
|
|
|
| 351 |
"Commercial Publishability": "Tighten hook, age fit, and list-readiness.",
|
| 352 |
}
|
| 353 |
|
| 354 |
+
DASHBOARD_STATE_KEYS = (
|
| 355 |
+
"project_name",
|
| 356 |
+
"workbook_loaded",
|
| 357 |
+
"analysis_status",
|
| 358 |
+
"last_analysis_at",
|
| 359 |
+
"totem_signal",
|
| 360 |
+
"metrics",
|
| 361 |
+
"metric_history",
|
| 362 |
+
"revision_queue",
|
| 363 |
+
"risk_clusters",
|
| 364 |
+
)
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
def esc(value) -> str:
|
| 368 |
+
"""HTML-escape UI text payloads safely."""
|
| 369 |
+
return html.escape(str(value or ""))
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
def _state_bool(value) -> bool:
|
| 373 |
+
if isinstance(value, bool):
|
| 374 |
+
return value
|
| 375 |
+
if isinstance(value, str):
|
| 376 |
+
low = value.strip().lower()
|
| 377 |
+
if low in {"1", "true", "yes", "y", "on"}:
|
| 378 |
+
return True
|
| 379 |
+
if low in {"0", "false", "no", "n", "off", "", "none", "null"}:
|
| 380 |
+
return False
|
| 381 |
+
return bool(value)
|
| 382 |
+
|
| 383 |
+
|
| 384 |
+
def _state_float(value, default: float = 0.0) -> float:
|
| 385 |
+
try:
|
| 386 |
+
return float(value)
|
| 387 |
+
except Exception:
|
| 388 |
+
return float(default)
|
| 389 |
+
|
| 390 |
+
|
| 391 |
+
def compute_totem_signal(
|
| 392 |
+
metrics: dict,
|
| 393 |
+
workbook_loaded: bool,
|
| 394 |
+
analysis_timestamp: str | None,
|
| 395 |
+
) -> int:
|
| 396 |
+
"""
|
| 397 |
+
Hero gauge contract from the design manual:
|
| 398 |
+
- 0 when no workbook is loaded.
|
| 399 |
+
- 35 when workbook is loaded but analysis has not run yet.
|
| 400 |
+
- Otherwise weighted metric blend, clamped 0..100.
|
| 401 |
+
"""
|
| 402 |
+
if not _state_bool(workbook_loaded):
|
| 403 |
+
return 0
|
| 404 |
+
if not analysis_timestamp:
|
| 405 |
+
return 35
|
| 406 |
+
|
| 407 |
+
weights = {
|
| 408 |
+
"overall_publishability": 0.30,
|
| 409 |
+
"read_aloud_flow": 0.15,
|
| 410 |
+
"emotional_truth": 0.20,
|
| 411 |
+
"visual_strength": 0.20,
|
| 412 |
+
"commercial_viability": 0.15,
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
total = 0.0
|
| 416 |
+
metric_map = metrics or {}
|
| 417 |
+
for key, weight in weights.items():
|
| 418 |
+
total += _state_float(metric_map.get(key, 0.0), 0.0) * weight
|
| 419 |
+
return max(0, min(100, int(round(total))))
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
def get_initial_dashboard_state() -> dict:
|
| 423 |
+
"""
|
| 424 |
+
Stage 2 state contract.
|
| 425 |
+
Placeholder values are deliberate before first analysis run.
|
| 426 |
+
"""
|
| 427 |
+
metrics = {
|
| 428 |
+
"overall_publishability": 0,
|
| 429 |
+
"read_aloud_flow": 0,
|
| 430 |
+
"emotional_truth": 0,
|
| 431 |
+
"visual_strength": 0,
|
| 432 |
+
"commercial_viability": 0,
|
| 433 |
+
}
|
| 434 |
+
return {
|
| 435 |
+
"project_name": "Editorial Workspace",
|
| 436 |
+
"workbook_loaded": False,
|
| 437 |
+
"analysis_status": "idle", # idle | ready | running | complete | error
|
| 438 |
+
"last_analysis_at": None,
|
| 439 |
+
"totem_signal": compute_totem_signal(metrics, workbook_loaded=False, analysis_timestamp=None),
|
| 440 |
+
"metrics": metrics,
|
| 441 |
+
"metric_history": {
|
| 442 |
+
"overall_publishability": [],
|
| 443 |
+
"read_aloud_flow": [],
|
| 444 |
+
"emotional_truth": [],
|
| 445 |
+
"visual_strength": [],
|
| 446 |
+
"commercial_viability": [],
|
| 447 |
+
},
|
| 448 |
+
"revision_queue": [],
|
| 449 |
+
"risk_clusters": [],
|
| 450 |
+
}
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
def normalize_dashboard_state(raw_existing_outputs) -> dict:
|
| 454 |
+
"""
|
| 455 |
+
Boundary adapter that normalizes scattered callback outputs into the Stage 2 state contract.
|
| 456 |
+
This adapter is UI-boundary only and does not alter extractor/scoring logic internals.
|
| 457 |
+
"""
|
| 458 |
+
state = get_initial_dashboard_state()
|
| 459 |
+
if raw_existing_outputs is None:
|
| 460 |
+
return state
|
| 461 |
+
if not isinstance(raw_existing_outputs, dict):
|
| 462 |
+
return state
|
| 463 |
+
|
| 464 |
+
state["project_name"] = str(raw_existing_outputs.get("project_name") or state["project_name"])
|
| 465 |
+
state["workbook_loaded"] = _state_bool(raw_existing_outputs.get("workbook_loaded", state["workbook_loaded"]))
|
| 466 |
+
|
| 467 |
+
status = str(raw_existing_outputs.get("analysis_status") or state["analysis_status"]).strip().lower()
|
| 468 |
+
if status not in {"idle", "ready", "running", "complete", "error"}:
|
| 469 |
+
status = state["analysis_status"]
|
| 470 |
+
state["analysis_status"] = status
|
| 471 |
+
|
| 472 |
+
ts_value = raw_existing_outputs.get("last_analysis_at")
|
| 473 |
+
state["last_analysis_at"] = str(ts_value).strip() if ts_value else None
|
| 474 |
+
|
| 475 |
+
incoming_metrics = raw_existing_outputs.get("metrics")
|
| 476 |
+
if isinstance(incoming_metrics, dict):
|
| 477 |
+
for key in state["metrics"]:
|
| 478 |
+
state["metrics"][key] = int(round(_state_float(incoming_metrics.get(key, state["metrics"][key]))))
|
| 479 |
+
state["metrics"][key] = max(0, min(100, state["metrics"][key]))
|
| 480 |
+
|
| 481 |
+
incoming_history = raw_existing_outputs.get("metric_history")
|
| 482 |
+
if isinstance(incoming_history, dict):
|
| 483 |
+
normalized_history = {}
|
| 484 |
+
for key in state["metric_history"].keys():
|
| 485 |
+
values = incoming_history.get(key, [])
|
| 486 |
+
if isinstance(values, list):
|
| 487 |
+
normalized_history[key] = [
|
| 488 |
+
max(0, min(100, int(round(_state_float(v)))))
|
| 489 |
+
for v in values
|
| 490 |
+
]
|
| 491 |
+
else:
|
| 492 |
+
normalized_history[key] = []
|
| 493 |
+
state["metric_history"] = normalized_history
|
| 494 |
+
|
| 495 |
+
revision_queue = raw_existing_outputs.get("revision_queue")
|
| 496 |
+
if isinstance(revision_queue, list):
|
| 497 |
+
state["revision_queue"] = revision_queue
|
| 498 |
+
|
| 499 |
+
risk_clusters = raw_existing_outputs.get("risk_clusters")
|
| 500 |
+
if isinstance(risk_clusters, list):
|
| 501 |
+
state["risk_clusters"] = risk_clusters
|
| 502 |
+
|
| 503 |
+
if "totem_signal" in raw_existing_outputs:
|
| 504 |
+
explicit = int(round(_state_float(raw_existing_outputs.get("totem_signal"), state["totem_signal"])))
|
| 505 |
+
state["totem_signal"] = max(0, min(100, explicit))
|
| 506 |
+
else:
|
| 507 |
+
state["totem_signal"] = compute_totem_signal(
|
| 508 |
+
state["metrics"],
|
| 509 |
+
workbook_loaded=state["workbook_loaded"],
|
| 510 |
+
analysis_timestamp=state["last_analysis_at"],
|
| 511 |
+
)
|
| 512 |
+
|
| 513 |
+
return state
|
| 514 |
+
|
| 515 |
+
|
| 516 |
+
def _dashboard_state_contract_smoke_test() -> tuple[bool, list[str]]:
|
| 517 |
+
state = get_initial_dashboard_state()
|
| 518 |
+
missing = [key for key in DASHBOARD_STATE_KEYS if key not in state]
|
| 519 |
+
return len(missing) == 0, missing
|
| 520 |
+
|
| 521 |
|
| 522 |
def _clean_path(uploaded_file) -> Path:
|
| 523 |
return workbook_path(uploaded_file)
|
|
|
|
| 1607 |
|
| 1608 |
|
| 1609 |
if __name__ == "__main__":
|
| 1610 |
+
state_ok, missing_keys = _dashboard_state_contract_smoke_test()
|
| 1611 |
+
if state_ok:
|
| 1612 |
+
print("[stage2] dashboard_state contract OK")
|
| 1613 |
+
else:
|
| 1614 |
+
print(f"[stage2] dashboard_state missing keys: {missing_keys}")
|
| 1615 |
demo.launch(ssr_mode=False, css=CSS + TOTEM_CSS + SS_CSS, head=HEAD)
|