JG1310 commited on
Commit
5be77ee
·
verified ·
1 Parent(s): 717604e

fix: re-run scribe on cards a failed chunk left blank; never paint an empty (subjectless) prompt

Browse files
Files changed (1) hide show
  1. app.py +185 -0
app.py CHANGED
@@ -448,6 +448,29 @@ def do_conjure(theme, style, custom):
448
  yield from _run_parallel(chunks, fn=lambda i, ch: _gpu_scribe(deck, ch),
449
  on_done=lambda res, ch: merge_fields(res, ch, _SCRIBE_OUT),
450
  tick=scribe_tick)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
451
  t_scribe = time.time() - t_scribe0
452
 
453
  # Lore is done → OPEN THE REAL DECK VIEW now (names + full meanings); the art then
@@ -1060,6 +1083,153 @@ footer { display:none !important; }
1060
  """
1061
 
1062
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1063
  # ----------------------------------------------------------------- ui
1064
  def build_demo() -> gr.Blocks:
1065
  with gr.Blocks(title="Arcana") as demo:
@@ -1211,6 +1381,21 @@ def build_demo() -> gr.Blocks:
1211
  # on load, collapse to just the landing page.
1212
  demo.load(lambda: show("landing"), None, pages)
1213
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1214
  return demo
1215
 
1216
 
 
448
  yield from _run_parallel(chunks, fn=lambda i, ch: _gpu_scribe(deck, ch),
449
  on_done=lambda res, ch: merge_fields(res, ch, _SCRIBE_OUT),
450
  tick=scribe_tick)
451
+
452
+ # RECONCILE: if a scribe chunk failed, its cards still have blank meaning/art.
453
+ # Re-run the scribe on JUST those (one more GPU pass) so we never paint an empty,
454
+ # subjectless prompt — which FLUX fills with a generic medieval/religious figure —
455
+ # and so those cards aren't left meaningless in the reading.
456
+ def _blank(c):
457
+ return not (c.get("art_prompt") or "").strip()
458
+ missing = [c["arcana_number"] for c in deck["cards"] if _blank(c)]
459
+ for _try in range(2):
460
+ if not missing:
461
+ break
462
+ yield stay("<div class='gen-load'><div class='gen-spin'></div>"
463
+ "<h2>✦ Writing the lore</h2><p>finishing the last cards…</p></div>")
464
+ try:
465
+ merge_fields(_gpu_scribe(deck, missing), missing, _SCRIBE_OUT)
466
+ except Exception as e:
467
+ print(f"[SCRIBE_RECONCILE_ERR] {missing}: {type(e).__name__}: {e}", flush=True)
468
+ missing = [c["arcana_number"] for c in deck["cards"] if _blank(c)]
469
+ if missing: # still blank after retries → at least give FLUX the concept as subject
470
+ print(f"[SCRIBE_FALLBACK] concept-only art for {missing}", flush=True)
471
+ for c in deck["cards"]:
472
+ if _blank(c):
473
+ c["art_prompt"] = c["concept"]
474
  t_scribe = time.time() - t_scribe0
475
 
476
  # Lore is done → OPEN THE REAL DECK VIEW now (names + full meanings); the art then
 
1083
  """
1084
 
1085
 
1086
+ TRACE_ON = os.environ.get("ARCANA_TRACE", "") == "1" # the JG1310 trace-logging copy sets this
1087
+
1088
+
1089
+ def _assemble_trace(steps, task, theme, visual_style, deck_id, models, pipeline, extra=None):
1090
+ import hashlib
1091
+ from datetime import datetime, timezone
1092
+ trace_id = hashlib.sha1(f"{deck_id}:{task}".encode("utf-8")).hexdigest()[:8]
1093
+ obj = {
1094
+ "trace_id": trace_id, "app": "Arcana", "task": task,
1095
+ "theme": theme, "visual_style": visual_style, "deck_id": deck_id,
1096
+ "ts": datetime.now(timezone.utc).isoformat(),
1097
+ "models": models, "pipeline": pipeline, "n_steps": len(steps),
1098
+ "provenance": ("Live capture — every step's prompts, raw model output, and "
1099
+ "latency were recorded as the pipeline ran (sequential trace "
1100
+ "harness on the JG1310/arcana-gpu-dev trace-logging copy)."),
1101
+ "steps": steps,
1102
+ }
1103
+ if extra:
1104
+ obj.update(extra)
1105
+ return obj
1106
+
1107
+
1108
+ _TR_TEXT = {"name": "Qwen3-14B", "backend": "llama.cpp"}
1109
+ _TR_IMG = {"name": "FLUX.2-klein-9B", "backend": "diffusers"}
1110
+
1111
+
1112
+ def _heartbeat(target, args):
1113
+ """Run `target(*args, state)` in a worker thread, yielding a heartbeat every ~3s so
1114
+ the SSE stream stays alive through long GPU phases; final yield is the result/ERROR.
1115
+ Splitting the work into SHORT GPU calls (this is used per-phase) also avoids the
1116
+ 'GPU task aborted' that a single multi-minute ZeroGPU hold triggers."""
1117
+ import threading, time as _t
1118
+ state = {"done": False, "result": None, "err": None, "msg": "starting…"}
1119
+ th = threading.Thread(target=target, args=(*args, state), daemon=True)
1120
+ th.start()
1121
+ while not state["done"]:
1122
+ _t.sleep(3)
1123
+ yield f"⏳ {state['msg']}"
1124
+ yield ("ERROR\n" + state["err"]) if state["err"] else state["result"]
1125
+
1126
+
1127
+ def _run_llm_phase(theme, style, custom, question, state):
1128
+ """designer → scribe (4 chunks) → reader, all through tracing wrappers. Returns the
1129
+ deck + the LLM trace steps (no images here, so it stays a short ~150s GPU call)."""
1130
+ import json as _json, traceback
1131
+ from arcana import tracing, gpu_models as gm
1132
+ from arcana.styles import resolve_style
1133
+ from arcana.designer import design_deck
1134
+ from arcana.loremaster import scribe_deck
1135
+ from arcana.build import deck_dir, slugify, _seed_base
1136
+ from arcana.reader import draw_spread, card_partial, final_synthesis
1137
+ from arcana.llm import get_llm
1138
+ try:
1139
+ theme = (theme or "Greek mythology").strip()
1140
+ question = (question or "").strip()
1141
+ TEXT = {**_TR_TEXT, "repo": gm.MAP_REPO, "file": gm.MAP_FILE}
1142
+ gtr = tracing.Tracer()
1143
+ gllm = tracing.TracingLLM(get_llm(), gtr)
1144
+ state["msg"] = "designer: mapping the 22 arcana…"
1145
+ gtr.ctx("designer", "mapping")
1146
+ deck = design_deck(theme, llm=gllm)
1147
+ style_id, style_suffix = resolve_style(style, custom)
1148
+ deck_id = f"{slugify(theme)}-{style_id}-{int(time.time())}"
1149
+ os.makedirs(deck_dir(deck_id), exist_ok=True)
1150
+ deck.update({"deck_id": deck_id, "theme": theme, "visual_style": style_id,
1151
+ "style_suffix": style_suffix, "seed_base": _seed_base(theme, style_id)})
1152
+ nums = [c["arcana_number"] for c in deck["cards"]]
1153
+ chunks = [sorted(c) for c in (nums[i::4] for i in range(4)) if c]
1154
+ for wi, ch in enumerate(chunks):
1155
+ state["msg"] = f"scribe: writing lore + art ({wi + 1}/{len(chunks)} batches)…"
1156
+ gtr.ctx("scribe", "meaning+art", worker=wi, cards=ch)
1157
+ scribe_deck(deck, subset=ch, llm=gllm)
1158
+ gen_llm_steps = gtr.steps
1159
+
1160
+ rtr = tracing.Tracer()
1161
+ rllm = tracing.TracingLLM(get_llm(), rtr)
1162
+ drawn = draw_spread(deck, spread="three", reversals=True, seed=7)
1163
+ partials = []
1164
+ for i in range(len(drawn)):
1165
+ state["msg"] = f"reader: interpreting card {i + 1}/{len(drawn)}…"
1166
+ rtr.ctx("reader", "card_reading", position=drawn[i]["position"],
1167
+ concept=drawn[i]["concept"], orientation=drawn[i]["orientation"])
1168
+ partials.append(card_partial(deck, question, drawn, i, llm=rllm))
1169
+ state["msg"] = "reader: final synthesis…"
1170
+ rtr.ctx("reader", "synthesis")
1171
+ final_synthesis(deck, question, drawn, partials, llm=rllm)
1172
+
1173
+ meta = {"theme": theme, "visual_style": style_id, "deck_id": deck_id,
1174
+ "question": question or "(open reading)", "spread": "three",
1175
+ "text_model": TEXT, "image_model": _TR_IMG,
1176
+ "drawn": [{"position": d["position"], "concept": d["concept"],
1177
+ "orientation": d["orientation"]} for d in drawn]}
1178
+ state["result"] = _json.dumps({"deck": deck, "gen_llm_steps": gen_llm_steps,
1179
+ "read_steps": rtr.steps, "meta": meta},
1180
+ ensure_ascii=False)
1181
+ except Exception:
1182
+ state["err"] = traceback.format_exc()
1183
+ finally:
1184
+ state["done"] = True
1185
+
1186
+
1187
+ def _run_paint_phase(deck_json, subset_csv, state):
1188
+ """Trace the FLUX image calls for a subset of cards (or 'back'). Image is discarded —
1189
+ the trace records the real call (styled prompt the model receives, seed, latency).
1190
+ Kept to a short subset per call so the GPU hold stays well under the abort threshold."""
1191
+ import json as _json, traceback
1192
+ from arcana import tracing, gpu_models as gm
1193
+ from arcana.imagegen import get_imagegen
1194
+ from arcana.build import _back_prompt
1195
+ try:
1196
+ deck = _json.loads(deck_json)
1197
+ by_n = {c["arcana_number"]: c for c in deck["cards"]}
1198
+ style_suffix = deck.get("style_suffix")
1199
+ seed_base = deck["seed_base"]
1200
+ IMG = {**_TR_IMG, "repo": gm.IMAGE_MODEL}
1201
+ tr = tracing.Tracer()
1202
+ tig = tracing.TracingImageGen(get_imagegen(deck_style=style_suffix), tr, style_suffix, IMG["name"])
1203
+ if subset_csv.strip().lower() == "back":
1204
+ state["msg"] = "painter: deck-back…"
1205
+ tig.generate(_back_prompt(style_suffix), seed=seed_base - 1)
1206
+ tr.steps[-1]["card"] = {"deck_back": True}
1207
+ else:
1208
+ nums = [int(x) for x in subset_csv.split(",") if x.strip() != ""]
1209
+ for i, m in enumerate(nums):
1210
+ state["msg"] = f"painter: FLUX rendering {i + 1}/{len(nums)} (card {m})…"
1211
+ tig.generate(by_n[m].get("art_prompt", ""), seed=seed_base + m)
1212
+ tr.steps[-1]["card"] = {"arcana_number": m, "arcana_name": by_n[m].get("arcana_name"),
1213
+ "concept": by_n[m].get("concept")}
1214
+ state["result"] = _json.dumps({"paint_steps": tr.steps}, ensure_ascii=False)
1215
+ except Exception:
1216
+ state["err"] = traceback.format_exc()
1217
+ finally:
1218
+ state["done"] = True
1219
+
1220
+
1221
+ @GPU(duration=300)
1222
+ def trace_llm(theme, style, custom, question):
1223
+ """LIVE trace (trace-logging copy only): designer + scribe + reader in one short call."""
1224
+ yield from _heartbeat(_run_llm_phase, (theme, style, custom, question))
1225
+
1226
+
1227
+ @GPU(duration=300)
1228
+ def trace_paint(deck_json, subset_csv):
1229
+ """LIVE trace (trace-logging copy only): FLUX image calls for a subset of cards."""
1230
+ yield from _heartbeat(_run_paint_phase, (deck_json, subset_csv))
1231
+
1232
+
1233
  # ----------------------------------------------------------------- ui
1234
  def build_demo() -> gr.Blocks:
1235
  with gr.Blocks(title="Arcana") as demo:
 
1381
  # on load, collapse to just the landing page.
1382
  demo.load(lambda: show("landing"), None, pages)
1383
 
1384
+ # TRACE-LOGGING COPY ONLY (ARCANA_TRACE=1): a hidden endpoint that runs the full
1385
+ # pipeline through tracing wrappers and returns the live traces. Never registered
1386
+ # on the production Space, so its behaviour is byte-identical there.
1387
+ if TRACE_ON:
1388
+ _llm_in = [gr.Textbox("Greek mythology", visible=False),
1389
+ gr.Textbox("rider-waite-smith", visible=False),
1390
+ gr.Textbox("", visible=False), gr.Textbox("", visible=False)]
1391
+ _llm_out = gr.Textbox(visible=False)
1392
+ gr.Button(visible=False).click(trace_llm, _llm_in, _llm_out, api_name="trace_llm")
1393
+ _pt_deck = gr.Textbox("", visible=False)
1394
+ _pt_sub = gr.Textbox("", visible=False)
1395
+ _pt_out = gr.Textbox(visible=False)
1396
+ gr.Button(visible=False).click(trace_paint, [_pt_deck, _pt_sub], _pt_out,
1397
+ api_name="trace_paint")
1398
+
1399
  return demo
1400
 
1401