Sync Dream QA latency polish

#46
by ADJCJH - opened
dream_customs/app_logic.py CHANGED
@@ -35,15 +35,15 @@ from dream_customs.schema import CustomsSession
35
 
36
  DEFAULT_TEXT_MODEL = "hf.co/openbmb/MiniCPM5-1B-GGUF:Q8_0"
37
  DEFAULT_VISION_MODEL = "openbmb/minicpm-v4.6"
38
- DEFAULT_HOSTED_TIMEOUT_SECONDS = 60.0
39
- DEFAULT_ASR_TIMEOUT_SECONDS = 45.0
40
  DEFAULT_TEXT_TEMPERATURE = 0.0
41
  DEFAULT_VISION_TEMPERATURE = 0.1
42
- DEFAULT_TEXT_MAX_TOKENS = 560
43
- DEFAULT_VISION_MAX_TOKENS = 320
44
- DEFAULT_TEXT_LATENCY_BUDGET_MS = 3500
45
- DEFAULT_VISION_LATENCY_BUDGET_MS = 6500
46
- DEFAULT_ASR_LATENCY_BUDGET_MS = 2500
47
 
48
 
49
  def _file_path(value: Any) -> str:
@@ -154,6 +154,7 @@ def _clients(text_backend: str, vision_backend: str, **settings):
154
  timeout=resolved["text_timeout_seconds"],
155
  temperature=resolved["text_temperature"],
156
  max_tokens=resolved["text_max_tokens"],
 
157
  )
158
  else:
159
  text_client = FakeTextClient()
@@ -171,6 +172,7 @@ def _clients(text_backend: str, vision_backend: str, **settings):
171
  timeout=resolved["vision_timeout_seconds"],
172
  temperature=resolved["vision_temperature"],
173
  max_tokens=resolved["vision_max_tokens"],
 
174
  )
175
  else:
176
  vision_client = FakeVisionClient()
@@ -180,6 +182,7 @@ def _clients(text_backend: str, vision_backend: str, **settings):
180
  endpoint=resolved["asr_endpoint"],
181
  token=resolved["hosted_token"],
182
  timeout=resolved["asr_timeout_seconds"],
 
183
  )
184
  else:
185
  asr_client = FakeASRClient()
 
35
 
36
  DEFAULT_TEXT_MODEL = "hf.co/openbmb/MiniCPM5-1B-GGUF:Q8_0"
37
  DEFAULT_VISION_MODEL = "openbmb/minicpm-v4.6"
38
+ DEFAULT_HOSTED_TIMEOUT_SECONDS = 9.0
39
+ DEFAULT_ASR_TIMEOUT_SECONDS = 9.0
40
  DEFAULT_TEXT_TEMPERATURE = 0.0
41
  DEFAULT_VISION_TEMPERATURE = 0.1
42
+ DEFAULT_TEXT_MAX_TOKENS = 480
43
+ DEFAULT_VISION_MAX_TOKENS = 220
44
+ DEFAULT_TEXT_LATENCY_BUDGET_MS = 8000
45
+ DEFAULT_VISION_LATENCY_BUDGET_MS = 9000
46
+ DEFAULT_ASR_LATENCY_BUDGET_MS = 8000
47
 
48
 
49
  def _file_path(value: Any) -> str:
 
154
  timeout=resolved["text_timeout_seconds"],
155
  temperature=resolved["text_temperature"],
156
  max_tokens=resolved["text_max_tokens"],
157
+ latency_budget_ms=resolved["text_latency_budget_ms"],
158
  )
159
  else:
160
  text_client = FakeTextClient()
 
172
  timeout=resolved["vision_timeout_seconds"],
173
  temperature=resolved["vision_temperature"],
174
  max_tokens=resolved["vision_max_tokens"],
175
+ latency_budget_ms=resolved["vision_latency_budget_ms"],
176
  )
177
  else:
178
  vision_client = FakeVisionClient()
 
182
  endpoint=resolved["asr_endpoint"],
183
  token=resolved["hosted_token"],
184
  timeout=resolved["asr_timeout_seconds"],
185
+ latency_budget_ms=resolved["asr_latency_budget_ms"],
186
  )
187
  else:
188
  asr_client = FakeASRClient()
modal_backend/contracts.py CHANGED
@@ -24,12 +24,12 @@ def normalize_text_payload(payload: Dict[str, Any]) -> Dict[str, Any]:
24
  if content:
25
  parts.append(content)
26
  prompt = "\n".join(parts).strip()
27
- max_tokens = payload.get("max_tokens", 560)
28
  try:
29
  max_tokens = int(max_tokens)
30
  except (TypeError, ValueError):
31
- max_tokens = 560
32
- max_tokens = max(64, min(max_tokens, 1200))
33
  temperature = payload.get("temperature", 0.0)
34
  try:
35
  temperature = float(temperature)
 
24
  if content:
25
  parts.append(content)
26
  prompt = "\n".join(parts).strip()
27
+ max_tokens = payload.get("max_tokens", 480)
28
  try:
29
  max_tokens = int(max_tokens)
30
  except (TypeError, ValueError):
31
+ max_tokens = 480
32
+ max_tokens = max(64, min(max_tokens, 700))
33
  temperature = payload.get("temperature", 0.0)
34
  try:
35
  temperature = float(temperature)
modal_backend/dream_customs_modal.py CHANGED
@@ -538,7 +538,8 @@ async def vision(
538
  ],
539
  }
540
  ]
541
- result = pipe(text=messages, max_new_tokens=int(payload.get("max_tokens", 320)))
 
542
  return response_payload(_stringify_pipeline_result(result))
543
 
544
 
 
538
  ],
539
  }
540
  ]
541
+ max_tokens = max(64, min(int(payload.get("max_tokens", 220)), 400))
542
+ result = pipe(text=messages, max_new_tokens=max_tokens)
543
  return response_payload(_stringify_pipeline_result(result))
544
 
545
 
scripts/benchmark_hosted_latency.py ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import sys
4
+ import tempfile
5
+ import time
6
+ import wave
7
+ from pathlib import Path
8
+ from typing import Any, Callable
9
+
10
+ ROOT = Path(__file__).resolve().parents[1]
11
+ if str(ROOT) not in sys.path:
12
+ sys.path.insert(0, str(ROOT))
13
+
14
+ from dream_customs.models import HostedASRClient, HostedMiniCPMTextClient, HostedMiniCPMVisionClient
15
+ from dream_customs.prompts import negotiation_prompt, today_tip_prompt
16
+ from dream_customs.pipeline import build_intake, build_qa_state
17
+ from dream_customs.schema import TodayTipCard
18
+
19
+
20
+ class TextFallback:
21
+ def generate_negotiation(self, _prompt: str) -> dict[str, Any]:
22
+ return {"visitor_name": "fallback", "questions": ["fallback"], "tone_note": "fallback"}
23
+
24
+ def generate_today_tip(self, _prompt: str) -> TodayTipCard:
25
+ return TodayTipCard(
26
+ dream_summary="fallback",
27
+ main_question="fallback",
28
+ dream_anchors=["fallback"],
29
+ followup_questions=[],
30
+ user_answers=[],
31
+ interpretation="fallback",
32
+ today_tip="fallback",
33
+ tiny_action="fallback",
34
+ caring_note="fallback",
35
+ safety_note="",
36
+ )
37
+
38
+
39
+ class VisionFallback:
40
+ def extract_clues(self, _image_path: str) -> list[str]:
41
+ return ["fallback"]
42
+
43
+ def extract_witness(self, _image_path: str):
44
+ raise RuntimeError("fallback witness should not be used in this benchmark")
45
+
46
+
47
+ class ASRFallback:
48
+ def transcribe(self, _audio_path: str) -> str:
49
+ return "fallback"
50
+
51
+
52
+ def _measure(name: str, fn: Callable[[], Any]) -> dict[str, Any]:
53
+ start = time.perf_counter()
54
+ try:
55
+ value = fn()
56
+ ok = True
57
+ error = ""
58
+ except Exception as exc: # pragma: no cover - benchmark safety net
59
+ value = None
60
+ ok = False
61
+ error = exc.__class__.__name__
62
+ elapsed = time.perf_counter() - start
63
+ return {
64
+ "name": name,
65
+ "ok": ok,
66
+ "elapsed_seconds": round(elapsed, 3),
67
+ "fallback": _looks_like_fallback(value),
68
+ "error": error,
69
+ }
70
+
71
+
72
+ def _looks_like_fallback(value: Any) -> bool:
73
+ if isinstance(value, dict):
74
+ return value.get("visitor_name") == "fallback"
75
+ if isinstance(value, TodayTipCard):
76
+ return value.dream_summary == "fallback"
77
+ if isinstance(value, list):
78
+ return value == ["fallback"]
79
+ return value == "fallback"
80
+
81
+
82
+ def _write_probe_wav() -> str:
83
+ temp = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
84
+ temp.close()
85
+ with wave.open(temp.name, "wb") as wav:
86
+ wav.setnchannels(1)
87
+ wav.setsampwidth(2)
88
+ wav.setframerate(16000)
89
+ wav.writeframes(b"\x00\x00" * 1600)
90
+ return temp.name
91
+
92
+
93
+ def main() -> int:
94
+ token = os.getenv("DREAM_CUSTOMS_HOSTED_TOKEN", "")
95
+ text_endpoint = os.getenv("DREAM_CUSTOMS_TEXT_ENDPOINT", "").strip()
96
+ vision_endpoint = os.getenv("DREAM_CUSTOMS_VISION_ENDPOINT", "").strip()
97
+ asr_endpoint = os.getenv("DREAM_CUSTOMS_ASR_ENDPOINT", "").strip()
98
+ image_path = os.getenv("DREAM_CUSTOMS_SMOKE_IMAGE", "").strip()
99
+ audio_path = os.getenv("DREAM_CUSTOMS_SMOKE_AUDIO", "").strip()
100
+
101
+ text_timeout = float(os.getenv("DREAM_CUSTOMS_BENCH_TEXT_TIMEOUT", "9"))
102
+ vision_timeout = float(os.getenv("DREAM_CUSTOMS_BENCH_VISION_TIMEOUT", "9"))
103
+ asr_timeout = float(os.getenv("DREAM_CUSTOMS_BENCH_ASR_TIMEOUT", "9"))
104
+ text_budget = int(float(os.getenv("DREAM_CUSTOMS_BENCH_TEXT_BUDGET_MS", "8000")))
105
+ vision_budget = int(float(os.getenv("DREAM_CUSTOMS_BENCH_VISION_BUDGET_MS", "9000")))
106
+ asr_budget = int(float(os.getenv("DREAM_CUSTOMS_BENCH_ASR_BUDGET_MS", "8000")))
107
+
108
+ intake = build_intake(
109
+ dream_text="I dreamed my phone died while I waited for an elevator.",
110
+ mood="Anxious",
111
+ )
112
+ answers = "The dead phone felt closest to being behind before I even start."
113
+ state = build_qa_state(
114
+ intake,
115
+ questions=["Which detail feels closest to your waking life right now?"],
116
+ answers=[answers],
117
+ language="en",
118
+ )
119
+
120
+ report: dict[str, Any] = {
121
+ "configured": {
122
+ "text_endpoint": bool(text_endpoint),
123
+ "vision_endpoint": bool(vision_endpoint),
124
+ "asr_endpoint": bool(asr_endpoint),
125
+ "token": bool(token),
126
+ "image_path": bool(image_path),
127
+ "audio_path": bool(audio_path),
128
+ },
129
+ "budgets_ms": {"text": text_budget, "vision": vision_budget, "asr": asr_budget},
130
+ "results": [],
131
+ }
132
+
133
+ if text_endpoint:
134
+ text_client = HostedMiniCPMTextClient(
135
+ endpoint=text_endpoint,
136
+ token=token,
137
+ timeout=text_timeout,
138
+ max_tokens=560,
139
+ latency_budget_ms=text_budget,
140
+ fallback=TextFallback(),
141
+ )
142
+ report["results"].append(
143
+ _measure(
144
+ "text_negotiation",
145
+ lambda: text_client.generate_negotiation(negotiation_prompt(intake, "en")),
146
+ )
147
+ )
148
+ report["results"].append(
149
+ _measure(
150
+ "text_today_tip",
151
+ lambda: text_client.generate_today_tip(today_tip_prompt(state, "en")),
152
+ )
153
+ )
154
+ if vision_endpoint and image_path and Path(image_path).exists():
155
+ vision_client = HostedMiniCPMVisionClient(
156
+ endpoint=vision_endpoint,
157
+ token=token,
158
+ timeout=vision_timeout,
159
+ max_tokens=220,
160
+ latency_budget_ms=vision_budget,
161
+ fallback=VisionFallback(),
162
+ )
163
+ report["results"].append(_measure("vision_clues", lambda: vision_client.extract_clues(image_path)))
164
+ if asr_endpoint:
165
+ probe_audio = ""
166
+ try:
167
+ probe_audio = audio_path if audio_path and Path(audio_path).exists() else _write_probe_wav()
168
+ asr_client = HostedASRClient(
169
+ endpoint=asr_endpoint,
170
+ token=token,
171
+ timeout=asr_timeout,
172
+ latency_budget_ms=asr_budget,
173
+ fallback=ASRFallback(),
174
+ )
175
+ report["results"].append(_measure("asr_transcribe", lambda: asr_client.transcribe(probe_audio)))
176
+ finally:
177
+ if probe_audio and probe_audio != audio_path:
178
+ try:
179
+ os.unlink(probe_audio)
180
+ except OSError:
181
+ pass
182
+
183
+ print(json.dumps(report, ensure_ascii=False, indent=2))
184
+ return 0
185
+
186
+
187
+ if __name__ == "__main__":
188
+ raise SystemExit(main())
tests/test_app_logic.py CHANGED
@@ -125,6 +125,9 @@ def test_developer_settings_configure_hosted_clients():
125
  vision_temperature=0.22,
126
  text_max_tokens=256,
127
  vision_max_tokens=128,
 
 
 
128
  )
129
 
130
  assert isinstance(text_client, HostedMiniCPMTextClient)
@@ -140,6 +143,25 @@ def test_developer_settings_configure_hosted_clients():
140
  assert vision_client.temperature == 0.22
141
  assert text_client.max_tokens == 256
142
  assert vision_client.max_tokens == 128
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
144
 
145
  def test_asr_endpoint_derives_from_modal_text_endpoint_when_secret_is_missing():
 
125
  vision_temperature=0.22,
126
  text_max_tokens=256,
127
  vision_max_tokens=128,
128
+ text_latency_budget_ms=5000,
129
+ vision_latency_budget_ms=6000,
130
+ asr_latency_budget_ms=4000,
131
  )
132
 
133
  assert isinstance(text_client, HostedMiniCPMTextClient)
 
143
  assert vision_client.temperature == 0.22
144
  assert text_client.max_tokens == 256
145
  assert vision_client.max_tokens == 128
146
+ assert text_client.latency_budget_ms == 5000
147
+ assert vision_client.latency_budget_ms == 6000
148
+ assert asr_client.latency_budget_ms == 4000
149
+ assert text_client.request_timeout == 5.0
150
+ assert vision_client.request_timeout == 6.0
151
+ assert asr_client.request_timeout == 4.0
152
+
153
+
154
+ def test_default_modal_latency_settings_target_fast_feedback():
155
+ settings = _client_settings()
156
+
157
+ assert settings["text_timeout_seconds"] <= 10
158
+ assert settings["vision_timeout_seconds"] <= 10
159
+ assert settings["asr_timeout_seconds"] <= 10
160
+ assert settings["text_latency_budget_ms"] <= 10_000
161
+ assert settings["vision_latency_budget_ms"] <= 10_000
162
+ assert settings["asr_latency_budget_ms"] <= 10_000
163
+ assert settings["text_max_tokens"] <= 560
164
+ assert settings["vision_max_tokens"] <= 220
165
 
166
 
167
  def test_asr_endpoint_derives_from_modal_text_endpoint_when_secret_is_missing():
tests/test_modal_contract.py CHANGED
@@ -22,12 +22,12 @@ def test_normalize_text_payload_accepts_openai_style_messages():
22
  {"messages": [{"role": "user", "content": "Dream of an elevator."}]}
23
  )
24
  assert payload["prompt"] == "Dream of an elevator."
25
- assert payload["max_tokens"] == 560
26
 
27
 
28
  def test_normalize_text_payload_clamps_max_tokens():
29
  payload = normalize_text_payload({"prompt": "x", "max_tokens": 9000})
30
- assert payload["max_tokens"] == 1200
31
 
32
 
33
  def test_decode_image_payload_accepts_image_key():
 
22
  {"messages": [{"role": "user", "content": "Dream of an elevator."}]}
23
  )
24
  assert payload["prompt"] == "Dream of an elevator."
25
+ assert payload["max_tokens"] == 480
26
 
27
 
28
  def test_normalize_text_payload_clamps_max_tokens():
29
  payload = normalize_text_payload({"prompt": "x", "max_tokens": 9000})
30
+ assert payload["max_tokens"] == 700
31
 
32
 
33
  def test_decode_image_payload_accepts_image_key():
tests/test_ui_actions.py CHANGED
@@ -332,7 +332,7 @@ def test_mobile_reset_restores_calm_mood():
332
  60,
333
  0.2,
334
  0.1,
335
- 560,
336
  320,
337
  "demo",
338
  "",
 
332
  60,
333
  0.2,
334
  0.1,
335
+ 480,
336
  320,
337
  "demo",
338
  "",