WillHbx commited on
Commit
4a789ac
·
1 Parent(s): 8bf6f6f

fix(zerogpu): persist phase state, because zerogpu run each function in

Browse files

dedicated worker, so we need persistant data so workers can grab data
from worker to worker.

Files changed (2) hide show
  1. visualnovel/engine.py +36 -1
  2. visualnovel/llm.py +1 -2
visualnovel/engine.py CHANGED
@@ -19,6 +19,35 @@ import json
19
  import time
20
  from pathlib import Path
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  from . import config, memory, orchestrator, state
23
  from .llm import LLMBackend, get_llm
24
  from .metrics import collector
@@ -97,9 +126,10 @@ class Engine:
97
  state.save_memory(self.state)
98
  self.tracer.log(event="start", setup=setup.model_dump(), opening=opening.model_dump())
99
  intro = self.state.flags.get("situation_intro", "")
100
- # Stash for start_images()
101
  self._pending_out: DirectorOutput | None = opening
102
  self._pending_intro: str = intro
 
103
  # Build a text-only ViewState so the frontend can show dialogue immediately
104
  s = self.state
105
  speaker_ch = s.characters.get(opening.speaker)
@@ -138,6 +168,11 @@ class Engine:
138
  def start_images(self) -> ViewState:
139
  """Phase 2 — paint backdrop + sprite, TTS. Call after start_text().
140
  Returns the full ViewState."""
 
 
 
 
 
141
  assert self.state is not None and self._pending_out is not None, "call start_text() first"
142
  return self._view(self._pending_out, intro_text=self._pending_intro)
143
 
 
19
  import time
20
  from pathlib import Path
21
 
22
+ # ZeroGPU runs each @spaces.GPU call in a separate worker subprocess; in-memory
23
+ # state set by start_text() is invisible to start_images(). We bridge the gap
24
+ # by writing/reading a temp file between the two phases.
25
+ _PHASE_FILE = Path("/tmp/vn_phase_state.json")
26
+
27
+
28
+ def _persist_phase(state: GameState, out: DirectorOutput, intro: str) -> None:
29
+ _PHASE_FILE.write_text(
30
+ json.dumps({"state": state.model_dump(), "out": out.model_dump(), "intro": intro}),
31
+ encoding="utf-8",
32
+ )
33
+
34
+
35
+ def _restore_phase() -> tuple[GameState, DirectorOutput, str] | None:
36
+ if not _PHASE_FILE.exists():
37
+ return None
38
+ try:
39
+ d = json.loads(_PHASE_FILE.read_text(encoding="utf-8"))
40
+ from .schemas import DirectorOutput, GameState # noqa: PLC0415
41
+
42
+ return (
43
+ GameState.model_validate(d["state"]),
44
+ DirectorOutput.model_validate(d["out"]),
45
+ d.get("intro", ""),
46
+ )
47
+ except Exception:
48
+ return None
49
+
50
+
51
  from . import config, memory, orchestrator, state
52
  from .llm import LLMBackend, get_llm
53
  from .metrics import collector
 
126
  state.save_memory(self.state)
127
  self.tracer.log(event="start", setup=setup.model_dump(), opening=opening.model_dump())
128
  intro = self.state.flags.get("situation_intro", "")
129
+ # Stash for start_images() — also written to disk so ZeroGPU workers can pick it up
130
  self._pending_out: DirectorOutput | None = opening
131
  self._pending_intro: str = intro
132
+ _persist_phase(self.state, opening, intro)
133
  # Build a text-only ViewState so the frontend can show dialogue immediately
134
  s = self.state
135
  speaker_ch = s.characters.get(opening.speaker)
 
168
  def start_images(self) -> ViewState:
169
  """Phase 2 — paint backdrop + sprite, TTS. Call after start_text().
170
  Returns the full ViewState."""
171
+ if self.state is None or self._pending_out is None:
172
+ # Restore state persisted by start_text() (handles ZeroGPU worker isolation)
173
+ restored = _restore_phase()
174
+ if restored is not None:
175
+ self.state, self._pending_out, self._pending_intro = restored
176
  assert self.state is not None and self._pending_out is not None, "call start_text() first"
177
  return self._view(self._pending_out, intro_text=self._pending_intro)
178
 
visualnovel/llm.py CHANGED
@@ -158,8 +158,7 @@ class TransformersLLM:
158
  + "\nNo markdown fences. No explanation. Only the JSON object."
159
  )
160
  augmented = [
161
- {**m, "content": m["content"] + hint} if m["role"] == "system" else m
162
- for m in messages
163
  ]
164
  text = self.complete(augmented, **kw)
165
  return json.loads(text[text.find("{") : text.rfind("}") + 1])
 
158
  + "\nNo markdown fences. No explanation. Only the JSON object."
159
  )
160
  augmented = [
161
+ {**m, "content": m["content"] + hint} if m["role"] == "system" else m for m in messages
 
162
  ]
163
  text = self.complete(augmented, **kw)
164
  return json.loads(text[text.find("{") : text.rfind("}") + 1])