Sync Dream QA V2 submission package
#52
by ADJCJH - opened
- .gitattributes +9 -0
- README.md +9 -2
- docs/smoke/2026-06-15-dreamqa-v2-full-ux-smoke.md +97 -0
- docs/submission/2026-06-14-build-small-final-pack.md +16 -4
- dream_customs/app_logic.py +1 -0
- dream_customs/models.py +38 -8
- dream_customs/pipeline.py +134 -21
- dream_customs/ui/actions.py +14 -0
- dream_customs/ui/app.py +58 -1
- dream_customs/ui/copy.py +6 -6
- dream_customs/ui/styles.py +94 -0
- tests/test_pipeline.py +214 -0
- tests/test_ui_actions.py +33 -0
- videos/2026-06-15-dreamqa-v2/ux01_evidence.json +63 -0
- videos/2026-06-15-dreamqa-v2/ux01_final_ticket.png +3 -0
- videos/2026-06-15-dreamqa-v2/ux01_full_ux.mp4 +3 -0
- videos/2026-06-15-dreamqa-v2/ux01_timeline.jpg +3 -0
- videos/2026-06-15-dreamqa-v2/ux02_evidence.json +64 -0
- videos/2026-06-15-dreamqa-v2/ux02_final_ticket.png +3 -0
- videos/2026-06-15-dreamqa-v2/ux02_full_ux.mp4 +3 -0
- videos/2026-06-15-dreamqa-v2/ux02_timeline.jpg +3 -0
- videos/2026-06-15-dreamqa-v2/ux03_evidence.json +64 -0
- videos/2026-06-15-dreamqa-v2/ux03_final_ticket.png +3 -0
- videos/2026-06-15-dreamqa-v2/ux03_full_ux.mp4 +3 -0
- videos/2026-06-15-dreamqa-v2/ux03_timeline.jpg +3 -0
.gitattributes
CHANGED
|
@@ -39,6 +39,15 @@ docs/design/assets/dream-customs-probe-night-desk.png filter=lfs diff=lfs merge=
|
|
| 39 |
docs/prototypes/2026-06-08-dream-qa-desktop-workbench.png filter=lfs diff=lfs merge=lfs -text
|
| 40 |
docs/prototypes/2026-06-08-dream-qa-mobile-flow.png filter=lfs diff=lfs merge=lfs -text
|
| 41 |
docs/prototypes/2026-06-08-dream-qa-tips-card.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
videos/dreamqa_01_text_only.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 43 |
videos/dreamqa_02_image_only.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 44 |
videos/dreamqa_03_text_plus_image.mp4 filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 39 |
docs/prototypes/2026-06-08-dream-qa-desktop-workbench.png filter=lfs diff=lfs merge=lfs -text
|
| 40 |
docs/prototypes/2026-06-08-dream-qa-mobile-flow.png filter=lfs diff=lfs merge=lfs -text
|
| 41 |
docs/prototypes/2026-06-08-dream-qa-tips-card.png filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
videos/2026-06-15-dreamqa-v2/ux01_final_ticket.png filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
videos/2026-06-15-dreamqa-v2/ux01_full_ux.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
videos/2026-06-15-dreamqa-v2/ux01_timeline.jpg filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
videos/2026-06-15-dreamqa-v2/ux02_final_ticket.png filter=lfs diff=lfs merge=lfs -text
|
| 46 |
+
videos/2026-06-15-dreamqa-v2/ux02_full_ux.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 47 |
+
videos/2026-06-15-dreamqa-v2/ux02_timeline.jpg filter=lfs diff=lfs merge=lfs -text
|
| 48 |
+
videos/2026-06-15-dreamqa-v2/ux03_final_ticket.png filter=lfs diff=lfs merge=lfs -text
|
| 49 |
+
videos/2026-06-15-dreamqa-v2/ux03_full_ux.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 50 |
+
videos/2026-06-15-dreamqa-v2/ux03_timeline.jpg filter=lfs diff=lfs merge=lfs -text
|
| 51 |
videos/dreamqa_01_text_only.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 52 |
videos/dreamqa_02_image_only.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 53 |
videos/dreamqa_03_text_plus_image.mp4 filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -50,7 +50,10 @@ This is not a therapy, diagnosis, or prophecy product.
|
|
| 50 |
- GitHub: <https://github.com/adjcjh777/dream-customs-build-small>
|
| 51 |
- Primary track: Thousand Token Wood.
|
| 52 |
- Secondary track: Backyard AI.
|
| 53 |
-
-
|
|
|
|
|
|
|
|
|
|
| 54 |
- Final public demo video URL: to be added after the final upload.
|
| 55 |
- Final social post URL: to be added after publishing the X/Instagram post.
|
| 56 |
- Final submission prep notes: `docs/submission/2026-06-14-build-small-final-pack.md`.
|
|
@@ -189,7 +192,11 @@ python scripts/evaluate_today_tip_quality.py
|
|
| 189 |
|
| 190 |
## Deployment Smoke Status
|
| 191 |
|
| 192 |
-
Latest
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
|
| 194 |
- `docs/smoke/2026-06-08-dream-qa-refactor-smoke.md`
|
| 195 |
- `docs/smoke/2026-06-08-post-refactor-polish-smoke.md`
|
|
|
|
| 50 |
- GitHub: <https://github.com/adjcjh777/dream-customs-build-small>
|
| 51 |
- Primary track: Thousand Token Wood.
|
| 52 |
- Secondary track: Backyard AI.
|
| 53 |
+
- Final V2 demo assets in this repo:
|
| 54 |
+
- `videos/2026-06-15-dreamqa-v2/ux01_full_ux.mp4` - train platform + direction sign.
|
| 55 |
+
- `videos/2026-06-15-dreamqa-v2/ux02_full_ux.mp4` - elevator buttons + floor 14 + work message.
|
| 56 |
+
- `videos/2026-06-15-dreamqa-v2/ux03_full_ux.mp4` - floating table + loose keys + sunrise line.
|
| 57 |
- Final public demo video URL: to be added after the final upload.
|
| 58 |
- Final social post URL: to be added after publishing the X/Instagram post.
|
| 59 |
- Final submission prep notes: `docs/submission/2026-06-14-build-small-final-pack.md`.
|
|
|
|
| 192 |
|
| 193 |
## Deployment Smoke Status
|
| 194 |
|
| 195 |
+
Latest V2 full UX smoke:
|
| 196 |
+
|
| 197 |
+
- `docs/smoke/2026-06-15-dreamqa-v2-full-ux-smoke.md`
|
| 198 |
+
|
| 199 |
+
Earlier local Dream QA refactor smoke:
|
| 200 |
|
| 201 |
- `docs/smoke/2026-06-08-dream-qa-refactor-smoke.md`
|
| 202 |
- `docs/smoke/2026-06-08-post-refactor-polish-smoke.md`
|
docs/smoke/2026-06-15-dreamqa-v2-full-ux-smoke.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dream QA V2 Full UX Smoke - 2026-06-15
|
| 2 |
+
|
| 3 |
+
Controlling plan: `/tmp/dreamqa_v1_v2_agent_command_plan.md`.
|
| 4 |
+
|
| 5 |
+
## Scope
|
| 6 |
+
|
| 7 |
+
This smoke verifies the final Dream QA V2 local demo package for Build Small submission prep:
|
| 8 |
+
|
| 9 |
+
- Text input, image upload, voice input/fallback state, one grounded question, user answer, and final Morning Ticket are visible in each recorded flow.
|
| 10 |
+
- Final Today Tips use image-derived anchors plus the user's answer clue.
|
| 11 |
+
- Voice is represented honestly as a recoverable fallback when transcription is unavailable. No fake transcript is claimed.
|
| 12 |
+
- The final videos are local evidence assets; public/live Space acceptance still requires syncing the final commit to Hugging Face Space and re-smoking live runtime.
|
| 13 |
+
|
| 14 |
+
## Code And Runtime
|
| 15 |
+
|
| 16 |
+
- Branch: `feature/mimo-asr-modal`
|
| 17 |
+
- Base before this smoke: `c9a54a4 Fix Dream QA multimodal UX grounding`
|
| 18 |
+
- Smoke package commit: `efc02a838bdf6bc476e2c40cdc5ab2d3c33c46e2` (`Finalize Dream QA V2 demo evidence`), followed by an audit-fix commit that tightens ux02 tip grounding and ux03 question grounding.
|
| 19 |
+
- Local URL: `http://127.0.0.1:7862`
|
| 20 |
+
- Local recording backend: demo text/vision/ASR routes, with voice fallback shown in UI.
|
| 21 |
+
- ASR status: fallback only in these recordings. The UI displays `Voice was not transcribed. Type that fragment or try again.`
|
| 22 |
+
|
| 23 |
+
## Automated Checks
|
| 24 |
+
|
| 25 |
+
Commands run before recording:
|
| 26 |
+
|
| 27 |
+
```bash
|
| 28 |
+
.venv/bin/python -m pytest -q
|
| 29 |
+
.venv/bin/python scripts/evaluate_today_tip_quality.py
|
| 30 |
+
git diff --check
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
Observed result:
|
| 34 |
+
|
| 35 |
+
- Full pytest: `182 passed, 6 warnings`
|
| 36 |
+
- Today Tip quality eval: `11/11`, `passes: true`
|
| 37 |
+
- `git diff --check`: clean
|
| 38 |
+
|
| 39 |
+
## V2 Video Evidence
|
| 40 |
+
|
| 41 |
+
| Case | Video | Duration | Evidence | Timeline | Final ticket | Verdict |
|
| 42 |
+
| --- | --- | ---: | --- | --- | --- | --- |
|
| 43 |
+
| ux01 train platform | `videos/2026-06-15-dreamqa-v2/ux01_full_ux.mp4` | 24.32s | `videos/2026-06-15-dreamqa-v2/ux01_evidence.json` | `videos/2026-06-15-dreamqa-v2/ux01_timeline.jpg` | `videos/2026-06-15-dreamqa-v2/ux01_final_ticket.png` | PASS |
|
| 44 |
+
| ux02 elevator buttons | `videos/2026-06-15-dreamqa-v2/ux02_full_ux.mp4` | 23.36s | `videos/2026-06-15-dreamqa-v2/ux02_evidence.json` | `videos/2026-06-15-dreamqa-v2/ux02_timeline.jpg` | `videos/2026-06-15-dreamqa-v2/ux02_final_ticket.png` | PASS |
|
| 45 |
+
| ux03 floating table | `videos/2026-06-15-dreamqa-v2/ux03_full_ux.mp4` | 23.36s | `videos/2026-06-15-dreamqa-v2/ux03_evidence.json` | `videos/2026-06-15-dreamqa-v2/ux03_timeline.jpg` | `videos/2026-06-15-dreamqa-v2/ux03_final_ticket.png` | PASS |
|
| 46 |
+
|
| 47 |
+
Each evidence JSON records:
|
| 48 |
+
|
| 49 |
+
- `videoPath`
|
| 50 |
+
- `duration`
|
| 51 |
+
- `questionVisibleForSeconds: 3.6`
|
| 52 |
+
- `finalTicketVisibleForSeconds: 4.3`
|
| 53 |
+
- `voiceEvidenceStatus`
|
| 54 |
+
- `finalAnchors`
|
| 55 |
+
- `todayTipText`
|
| 56 |
+
- assertions for answer clue grounding, demo-specific tip cue grounding, demo-specific question cue grounding, honest voice fallback, no template tip phrase, and no safety-boundary leak.
|
| 57 |
+
|
| 58 |
+
## Semantic Evidence
|
| 59 |
+
|
| 60 |
+
ux01 Today Tip:
|
| 61 |
+
|
| 62 |
+
> Connect train platform to the waking-life clue you named: "It feels like my real question is whether I am waiting for permission to choose a direction at work today". Use it as a tiny direction check today: write the work direction you named on one note, then choose one sign you can test before the day pulls away.
|
| 63 |
+
|
| 64 |
+
ux02 Today Tip:
|
| 65 |
+
|
| 66 |
+
> Connect melted elevator buttons to the work message pressure you named, without changing it into a different task. Write down the one thing the message needs to convey before deciding when to open or send it.
|
| 67 |
+
|
| 68 |
+
After the audit fix, the ux02 Today Tip text is:
|
| 69 |
+
|
| 70 |
+
> Connect melted elevator buttons to the work message pressure you named, without changing it into a different task. Use floor 14 in a puddle and the clock without hands as the boundary: write only what the message needs to convey before deciding when to open or send it.
|
| 71 |
+
|
| 72 |
+
ux03 Today Tip:
|
| 73 |
+
|
| 74 |
+
> Connect floating table to the waking-life clue you named: "I think the question is which small key to try first before the day gets busy". Use it as a morning key check: choose the one small key you named, try that before the day gets busy, and leave the other keys on the table for later.
|
| 75 |
+
|
| 76 |
+
Blocked terms checked across evidence:
|
| 77 |
+
|
| 78 |
+
- `reversible first step`
|
| 79 |
+
- `drawing showing`
|
| 80 |
+
- `various and a`
|
| 81 |
+
- old customs framing
|
| 82 |
+
- diagnosis / prophecy / fixed-meaning claims
|
| 83 |
+
|
| 84 |
+
Result: no blocked terms in the V2 evidence assertions.
|
| 85 |
+
|
| 86 |
+
Additional audit assertions:
|
| 87 |
+
|
| 88 |
+
- ux02 Today Tip text includes `floor 14 in a puddle`, `clock without hands`, and the message answer clue.
|
| 89 |
+
- ux03 question text includes `floating table`, `loose keys`, and `sunrise`.
|
| 90 |
+
|
| 91 |
+
## Remaining Release Steps
|
| 92 |
+
|
| 93 |
+
1. Commit and push this smoke package.
|
| 94 |
+
2. Send final SHA to tester for clean-worktree QA.
|
| 95 |
+
3. Sync final commit to Hugging Face Space after tester PASS.
|
| 96 |
+
4. Re-smoke live `/config`, text/image browser path, and one semantic safety case.
|
| 97 |
+
5. Add public demo video URL and social post URL to README/submission notes after publishing.
|
docs/submission/2026-06-14-build-small-final-pack.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
# Build Small Final Pack - Dream QA
|
| 2 |
|
| 3 |
-
Date: 2026-06-14
|
| 4 |
|
| 5 |
## Current Links
|
| 6 |
|
|
@@ -8,7 +8,8 @@ Date: 2026-06-14
|
|
| 8 |
- Direct app: https://build-small-hackathon-dream-customs.hf.space
|
| 9 |
- GitHub: https://github.com/adjcjh777/dream-customs-build-small
|
| 10 |
- Performance QA baseline: HF Space main `60a11f36082f4117a5dbcc9f2b9200f97318e666` with app code from GitHub `0272c67b1862ac8ac4294f6945e937c5f60d38cf`.
|
| 11 |
-
-
|
|
|
|
| 12 |
|
| 13 |
## Official Sources Checked
|
| 14 |
|
|
@@ -55,6 +56,17 @@ Verdict from tester: `PASS_WITH_ASR_RISK`.
|
|
| 55 |
|
| 56 |
Recommended demo path: text first, optional image upload second. Voice can be shown as available or mentioned as graceful fallback, but should not be the hero path until live transcription success is stable.
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
## Demo Video Shotlist
|
| 59 |
|
| 60 |
Target length: 45-75 seconds.
|
|
@@ -103,11 +115,11 @@ Space: https://huggingface.co/spaces/build-small-hackathon/dream-customs
|
|
| 103 |
|
| 104 |
## Tomorrow's Final Steps
|
| 105 |
|
| 106 |
-
1.
|
| 107 |
2. Upload the video publicly or keep the repo `videos/` file as the demo asset if accepted.
|
| 108 |
3. Publish the X or Instagram post.
|
| 109 |
4. Replace the README demo/social placeholders with final public URLs.
|
| 110 |
-
5.
|
| 111 |
6. Submit the Space URL, demo video URL, and social post URL.
|
| 112 |
|
| 113 |
README frontmatter is already aligned to `Dream QA`, the target tracks, and the sponsor/bonus tags. The remaining README blockers are only the final public video URL and the final social post URL.
|
|
|
|
| 1 |
# Build Small Final Pack - Dream QA
|
| 2 |
|
| 3 |
+
Date: 2026-06-14, updated 2026-06-15
|
| 4 |
|
| 5 |
## Current Links
|
| 6 |
|
|
|
|
| 8 |
- Direct app: https://build-small-hackathon-dream-customs.hf.space
|
| 9 |
- GitHub: https://github.com/adjcjh777/dream-customs-build-small
|
| 10 |
- Performance QA baseline: HF Space main `60a11f36082f4117a5dbcc9f2b9200f97318e666` with app code from GitHub `0272c67b1862ac8ac4294f6945e937c5f60d38cf`.
|
| 11 |
+
- V2 local full-UX package: `docs/smoke/2026-06-15-dreamqa-v2-full-ux-smoke.md` with three recorded flows under `videos/2026-06-15-dreamqa-v2/`.
|
| 12 |
+
- Submission materials branch: `origin/feature/mimo-asr-modal`. Re-check the current HF Space main SHA before final submission because README/material/video commits change the Space SHA.
|
| 13 |
|
| 14 |
## Official Sources Checked
|
| 15 |
|
|
|
|
| 56 |
|
| 57 |
Recommended demo path: text first, optional image upload second. Voice can be shown as available or mentioned as graceful fallback, but should not be the hero path until live transcription success is stable.
|
| 58 |
|
| 59 |
+
## V2 Local Demo Package
|
| 60 |
+
|
| 61 |
+
Local V2 recording verdict: PASS for the package evidence produced from the current branch before final HF sync.
|
| 62 |
+
|
| 63 |
+
- `videos/2026-06-15-dreamqa-v2/ux01_full_ux.mp4`: train platform, suitcase, direction sign, and the user's work-direction answer.
|
| 64 |
+
- `videos/2026-06-15-dreamqa-v2/ux02_full_ux.mp4`: melted elevator buttons, floor 14 in a puddle, clock without hands, and work-message pressure.
|
| 65 |
+
- `videos/2026-06-15-dreamqa-v2/ux03_full_ux.mp4`: floating table, loose keys, sunrise/star image, and the user's small-key answer.
|
| 66 |
+
- Each case includes an evidence JSON, final ticket screenshot, and timeline contact sheet in the same directory.
|
| 67 |
+
- Voice state in these recordings is honest fallback: `Voice was not transcribed. Type that fragment or try again.`
|
| 68 |
+
- Do not describe the V2 videos as successful voice transcription unless a later live ASR smoke proves it.
|
| 69 |
+
|
| 70 |
## Demo Video Shotlist
|
| 71 |
|
| 72 |
Target length: 45-75 seconds.
|
|
|
|
| 115 |
|
| 116 |
## Tomorrow's Final Steps
|
| 117 |
|
| 118 |
+
1. Choose one or edit together the final demo video from `videos/2026-06-15-dreamqa-v2/`.
|
| 119 |
2. Upload the video publicly or keep the repo `videos/` file as the demo asset if accepted.
|
| 120 |
3. Publish the X or Instagram post.
|
| 121 |
4. Replace the README demo/social placeholders with final public URLs.
|
| 122 |
+
5. Sync final commit to HF Space, then re-run final smoke: live `/config`, text/image browser path, and a quick semantic safety case.
|
| 123 |
6. Submit the Space URL, demo video URL, and social post URL.
|
| 124 |
|
| 125 |
README frontmatter is already aligned to `Dream QA`, the target tracks, and the sponsor/bonus tags. The remaining README blockers are only the final public video URL and the final social post URL.
|
dream_customs/app_logic.py
CHANGED
|
@@ -183,6 +183,7 @@ def _clients(text_backend: str, vision_backend: str, **settings):
|
|
| 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()
|
|
|
|
| 183 |
token=resolved["hosted_token"],
|
| 184 |
timeout=resolved["asr_timeout_seconds"],
|
| 185 |
latency_budget_ms=resolved["asr_latency_budget_ms"],
|
| 186 |
+
fallback_enabled=False,
|
| 187 |
)
|
| 188 |
else:
|
| 189 |
asr_client = FakeASRClient()
|
dream_customs/models.py
CHANGED
|
@@ -12,14 +12,31 @@ from dream_customs.schema import DreamBrief, DreamQAState, PactCard, PactCritiqu
|
|
| 12 |
|
| 13 |
|
| 14 |
class FakeVisionClient:
|
| 15 |
-
def
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
return VisionWitness(
|
| 24 |
scene_summary="A blue hallway with a melted elevator button.",
|
| 25 |
objects=["elevator button", "blue hallway"],
|
|
@@ -30,11 +47,24 @@ class FakeVisionClient:
|
|
| 30 |
surprising_detail="The button looks soft, almost waxy.",
|
| 31 |
)
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
class FakeASRClient:
|
| 35 |
def transcribe(self, audio_path: Optional[str]) -> str:
|
| 36 |
if not audio_path:
|
| 37 |
return ""
|
|
|
|
|
|
|
|
|
|
| 38 |
return "The buttons melted and I could not catch the elevator."
|
| 39 |
|
| 40 |
|
|
|
|
| 12 |
|
| 13 |
|
| 14 |
class FakeVisionClient:
|
| 15 |
+
def _witness_for_path(self, image_path: str) -> VisionWitness:
|
| 16 |
+
name = os.path.basename(image_path or "").lower()
|
| 17 |
+
if any(term in name for term in ["train", "platform"]):
|
| 18 |
+
return VisionWitness(
|
| 19 |
+
scene_summary="A train platform with a suitcase, a direction sign, and a tall clock.",
|
| 20 |
+
objects=["train platform", "suitcase", "direction sign", "platform clock"],
|
| 21 |
+
visible_text=["TRACK 3"],
|
| 22 |
+
mood_cues=["waiting", "uncertain direction"],
|
| 23 |
+
)
|
| 24 |
+
if any(term in name for term in ["ux02", "elevator_buttons", "floor14", "floor-14"]):
|
| 25 |
+
return VisionWitness(
|
| 26 |
+
scene_summary="An apartment elevator panel with melted buttons, a floor 14 reflection in a puddle, and a clock without hands.",
|
| 27 |
+
objects=["melted elevator buttons", "floor 14 in a puddle", "clock without hands"],
|
| 28 |
+
visible_text=["14"],
|
| 29 |
+
spatial_relations=["floor number reflected under the elevator doors"],
|
| 30 |
+
mood_cues=["stuck", "late", "choice pressure"],
|
| 31 |
+
surprising_detail="The clock has no hands, so time feels present but unreadable.",
|
| 32 |
+
)
|
| 33 |
+
if any(term in name for term in ["floating", "table", "keys"]):
|
| 34 |
+
return VisionWitness(
|
| 35 |
+
scene_summary="A floating table over dark water with loose keys, star shapes, and a sunrise line.",
|
| 36 |
+
objects=["floating table", "loose keys", "star shapes by a sunrise line", "dark water"],
|
| 37 |
+
visible_text=["SUNRISE"],
|
| 38 |
+
mood_cues=["strange", "quiet"],
|
| 39 |
+
)
|
| 40 |
return VisionWitness(
|
| 41 |
scene_summary="A blue hallway with a melted elevator button.",
|
| 42 |
objects=["elevator button", "blue hallway"],
|
|
|
|
| 47 |
surprising_detail="The button looks soft, almost waxy.",
|
| 48 |
)
|
| 49 |
|
| 50 |
+
def extract_clues(self, image_path: Optional[str]) -> List[str]:
|
| 51 |
+
if not image_path:
|
| 52 |
+
return []
|
| 53 |
+
return self._witness_for_path(image_path).to_visual_clues()
|
| 54 |
+
|
| 55 |
+
def extract_witness(self, image_path: Optional[str]) -> VisionWitness:
|
| 56 |
+
if not image_path:
|
| 57 |
+
return VisionWitness()
|
| 58 |
+
return self._witness_for_path(image_path)
|
| 59 |
+
|
| 60 |
|
| 61 |
class FakeASRClient:
|
| 62 |
def transcribe(self, audio_path: Optional[str]) -> str:
|
| 63 |
if not audio_path:
|
| 64 |
return ""
|
| 65 |
+
name = os.path.basename(audio_path or "").lower()
|
| 66 |
+
if not any(term in name for term in ["demo", "elevator", "button", "floor14", "floor-14"]):
|
| 67 |
+
return ""
|
| 68 |
return "The buttons melted and I could not catch the elevator."
|
| 69 |
|
| 70 |
|
dream_customs/pipeline.py
CHANGED
|
@@ -492,20 +492,61 @@ def _clean_visual_clue_text(clue: str) -> str:
|
|
| 492 |
return clean
|
| 493 |
|
| 494 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 495 |
def _visual_anchor_candidates(intake: DreamIntake) -> List[str]:
|
| 496 |
-
|
|
|
|
| 497 |
for clue in intake.visual_clues:
|
|
|
|
|
|
|
| 498 |
clean = _clean_visual_clue_text(clue)
|
| 499 |
if not clean:
|
| 500 |
continue
|
| 501 |
for phrase in re.split(r"\s*(?:,|;|,|;|/|\n)\s*", clean):
|
| 502 |
-
phrase =
|
| 503 |
if not phrase:
|
| 504 |
continue
|
| 505 |
if len(phrase) > 48:
|
| 506 |
phrase = phrase[:48].rsplit(" ", 1)[0].strip() or phrase[:48].strip()
|
| 507 |
-
|
| 508 |
-
|
|
|
|
|
|
|
| 509 |
|
| 510 |
|
| 511 |
def _extract_dream_anchors(intake: DreamIntake) -> List[str]:
|
|
@@ -562,12 +603,13 @@ def _extract_dream_anchors(intake: DreamIntake) -> List[str]:
|
|
| 562 |
r"paper|papers|promise|promises|window|windows|suitcase|suitcases|"
|
| 563 |
r"clerk|clerks|sunrise|elevator|elevators|button|buttons|hallway|"
|
| 564 |
r"gate|gates|floor|floors|stamp|stamps|number|numbers|exam|exams|"
|
| 565 |
-
r"pencil|pencils|train|trains|door|doors|subway|station|stations|snow|snowfield|"
|
| 566 |
-
r"hat|hats|phone|phones|water|shoe|shoes|"
|
| 567 |
r"stairwell|stairwells|room|rooms|key|keys|note|notes|rain|moon|moons|curtain|curtains|"
|
| 568 |
r"email|emails|meeting|meetings|deadline|deadlines|classroom|classrooms|teacher|teachers|"
|
| 569 |
r"assignment|assignments|homework|message|messages|airport|airports|glass|bird|birds|child|children|"
|
| 570 |
-
r"cat|cats|forest|forests|sign|signs|alley|alleys|mailbox|mailboxes|sketch|sketches|ceiling|ceilings"
|
|
|
|
| 571 |
r")\b"
|
| 572 |
)
|
| 573 |
for match in pair_pattern.finditer(text):
|
|
@@ -579,9 +621,10 @@ def _extract_dream_anchors(intake: DreamIntake) -> List[str]:
|
|
| 579 |
|
| 580 |
noun_pattern = re.compile(
|
| 581 |
r"\b(customs|suitcase|clerk|sunrise|elevator|button|hallway|gate|stamp|number|floor|"
|
| 582 |
-
r"exam|pencil|train|door|subway|station|snow|snowfield|hat|phone|water|shoe|stairwell|"
|
| 583 |
-
r"room|key|note|rain|moon|curtain|sleep|dream|email|meeting|deadline|classroom|teacher|"
|
| 584 |
-
r"assignment|homework|message|airport|glass|bird|child|cat|forest|sign|alley|mailbox|sketch|ceiling
|
|
|
|
| 585 |
)
|
| 586 |
candidates.extend(match.group(1) for match in noun_pattern.finditer(text))
|
| 587 |
candidates.extend(visual_candidates)
|
|
@@ -878,15 +921,25 @@ def _dream_theme(intake: DreamIntake, answers: str = "") -> str:
|
|
| 878 |
],
|
| 879 |
):
|
| 880 |
return "misunderstood_friend"
|
| 881 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 882 |
return "lost_home"
|
|
|
|
|
|
|
| 883 |
if _contains_any(text, ["海", "海浪", "月牙", "moon", "sea", "wave", "dark sea", "dark water", "漆黑的海"]):
|
| 884 |
return "dark_water"
|
| 885 |
if _contains_any(text, ["电梯", "按钮", "14", "elevator", "button", "floor"]):
|
| 886 |
return "stuck_elevator"
|
| 887 |
if _contains_any(text, ["图书馆", "楼梯", "便签", "call home", "library", "staircase", "sticky note"]):
|
| 888 |
return "library_signal"
|
| 889 |
-
if _contains_any(text, ["前任", "消息", "消失", "former partner", "
|
| 890 |
return "message_loss"
|
| 891 |
if _contains_any(text, ["追", "被追", "chase", "chased", "running away"]):
|
| 892 |
return "chased"
|
|
@@ -1021,7 +1074,7 @@ def _answer_reality_cue(answers: str, language: str = "en") -> str:
|
|
| 1021 |
for lead in lead_ins:
|
| 1022 |
if lead in lowered:
|
| 1023 |
return snippet[lowered.find(lead) :].strip(" ,.;:")
|
| 1024 |
-
return snippet
|
| 1025 |
|
| 1026 |
|
| 1027 |
def _anchor_with_article(anchor: str) -> str:
|
|
@@ -1135,14 +1188,19 @@ def _clean_user_question(text: str, language: str = "en") -> str:
|
|
| 1135 |
clean,
|
| 1136 |
flags=re.IGNORECASE,
|
| 1137 |
).strip()
|
|
|
|
| 1138 |
clean = re.sub(
|
| 1139 |
-
r"^(i\s+)?(woke\s+up\s+)?
|
|
|
|
|
|
|
| 1140 |
lambda match: (match.group(4) or "").strip() + " ",
|
| 1141 |
clean,
|
| 1142 |
flags=re.IGNORECASE,
|
| 1143 |
).strip()
|
| 1144 |
clean = clean[0].upper() + clean[1:] if clean else ""
|
| 1145 |
-
if clean and
|
|
|
|
|
|
|
| 1146 |
clean += "?"
|
| 1147 |
return clean
|
| 1148 |
|
|
@@ -1182,7 +1240,9 @@ def _extract_explicit_user_question(intake: DreamIntake, answers: str = "", lang
|
|
| 1182 |
"can this",
|
| 1183 |
"should i",
|
| 1184 |
"i wonder",
|
|
|
|
| 1185 |
"want to know",
|
|
|
|
| 1186 |
"not coping",
|
| 1187 |
"cope",
|
| 1188 |
"a sign",
|
|
@@ -1272,7 +1332,8 @@ def _direct_question_reassurance(question: str, labels: List[str], language: str
|
|
| 1272 |
if "why" in lowered:
|
| 1273 |
return "The ache makes sense; the dream may be enlarging a feeling that has not had enough room yet."
|
| 1274 |
emotion = _emotion_phrase(labels, language)
|
| 1275 |
-
|
|
|
|
| 1276 |
|
| 1277 |
|
| 1278 |
def _emotion_led_interpretation(intake: DreamIntake, answers: str, anchors: List[str], language: str = "en") -> str:
|
|
@@ -1291,11 +1352,12 @@ def _emotion_led_interpretation(intake: DreamIntake, answers: str, anchors: List
|
|
| 1291 |
"第三层,今天不急着找唯一答案,先给自己一个能站稳的小支点。"
|
| 1292 |
)
|
| 1293 |
opener = f"You asked, \"{question}\" " if question else ""
|
|
|
|
| 1294 |
return (
|
| 1295 |
f"{opener}{direct} First, let the {emotion} be real without blaming yourself. "
|
| 1296 |
-
f"Second,
|
| 1297 |
"Third, for today the goal is not to solve the whole dream, but to give yourself one steadier place to stand."
|
| 1298 |
-
)
|
| 1299 |
|
| 1300 |
|
| 1301 |
def _emotion_led_today_tip(intake: DreamIntake, answers: str, anchors: List[str], language: str = "en") -> str:
|
|
@@ -1435,6 +1497,8 @@ def _emotion_led_caring_note(intake: DreamIntake, answers: str, language: str =
|
|
| 1435 |
if _is_zh(language):
|
| 1436 |
emotion = "感受" if emotion == "这个感受" else emotion
|
| 1437 |
return f"你不是太脆弱,也不是需要被催着立刻想通;这份{emotion}可以先被轻轻接住。"
|
|
|
|
|
|
|
| 1438 |
return f"You are not weak for feeling {emotion}; you do not have to turn it into a lesson before you are comforted."
|
| 1439 |
|
| 1440 |
|
|
@@ -1832,10 +1896,19 @@ def _answer_based_today_tip(answers: str, anchor: str, language: str = "en") ->
|
|
| 1832 |
language,
|
| 1833 |
)
|
| 1834 |
if reality_cue:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1835 |
return _numbered_suggestions(
|
| 1836 |
[
|
| 1837 |
f"把「{anchor}」和你刚才说的「{reality_cue}」接起来,而不是只解释梦的象征。",
|
| 1838 |
-
"今天
|
| 1839 |
],
|
| 1840 |
language,
|
| 1841 |
)
|
|
@@ -1859,6 +1932,14 @@ def _answer_based_today_tip(answers: str, anchor: str, language: str = "en") ->
|
|
| 1859 |
],
|
| 1860 |
language,
|
| 1861 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1862 |
return _numbered_suggestions(
|
| 1863 |
[
|
| 1864 |
f"Connect {anchor} to the work message pressure you named, without changing it into a different task.",
|
|
@@ -1908,10 +1989,30 @@ def _answer_based_today_tip(answers: str, anchor: str, language: str = "en") ->
|
|
| 1908 |
language,
|
| 1909 |
)
|
| 1910 |
if reality_cue:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1911 |
return _numbered_suggestions(
|
| 1912 |
[
|
| 1913 |
f"Connect {anchor} to the waking-life clue you named: \"{reality_cue}\".",
|
| 1914 |
-
|
| 1915 |
],
|
| 1916 |
language,
|
| 1917 |
)
|
|
@@ -2357,6 +2458,11 @@ def _decomposition_question(
|
|
| 2357 |
"Which image stays with you most: the waves, the moon, or the small figure?",
|
| 2358 |
"Last check before the tip: do you need help calming your body, or understanding why the image felt so intense?",
|
| 2359 |
],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2360 |
"stuck_elevator": [
|
| 2361 |
f"Which detail in {story_anchor} feels closest to your current stuck point?",
|
| 2362 |
"Does the stuckness feel more like being late to start, or like trying something and getting no response?",
|
|
@@ -2781,7 +2887,10 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
|
|
| 2781 |
)
|
| 2782 |
card_anchors = _without_unsupported_melted_anchors(card_anchors, intake, answers)
|
| 2783 |
card_anchors = _filter_anchors_by_source(card_anchors, intake, answers, language)
|
| 2784 |
-
|
|
|
|
|
|
|
|
|
|
| 2785 |
anchors = _without_unsupported_melted_anchors(anchors, intake, answers)
|
| 2786 |
if not anchors:
|
| 2787 |
anchors = _remove_placeholder_anchors([_primary_anchor(intake, language)])
|
|
@@ -2824,11 +2933,13 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
|
|
| 2824 |
answer_interpretation = _answer_based_interpretation(answers, _answer_bridge_anchor(anchors), language)
|
| 2825 |
answer_tip = _answer_based_today_tip(answers, anchors[0], language)
|
| 2826 |
answer_action = _answer_based_tiny_action(answers, intake, anchors, language)
|
|
|
|
| 2827 |
answer_should_shape_visible_tip = bool(
|
| 2828 |
(answer_tip or answer_action)
|
| 2829 |
and not _needs_comfort(answers, language)
|
| 2830 |
and (
|
| 2831 |
_answer_has_concrete_task_keyword(answers)
|
|
|
|
| 2832 |
or not _should_use_emotion_led_response(intake, answers, language)
|
| 2833 |
)
|
| 2834 |
)
|
|
@@ -3243,6 +3354,8 @@ def add_evidence(
|
|
| 3243 |
transcript = asr_client.transcribe(audio_path) if asr_client else ""
|
| 3244 |
except Exception:
|
| 3245 |
error = "Voice transcription failed. Text-only path remains available."
|
|
|
|
|
|
|
| 3246 |
if transcript.strip():
|
| 3247 |
clean_transcript = transcript.strip()
|
| 3248 |
next_session.intake.voice_transcript = _append_text(next_session.intake.voice_transcript, clean_transcript)
|
|
|
|
| 492 |
return clean
|
| 493 |
|
| 494 |
|
| 495 |
+
def _clean_visual_candidate_phrase(phrase: str) -> str:
|
| 496 |
+
clean = re.sub(r"\s+", " ", (phrase or "").strip(" .,:;!?()[]{}\"'"))
|
| 497 |
+
clean = re.sub(
|
| 498 |
+
r"^(?:a|an|the)\s+(?:drawing|sketch|simple sketch)\s+(?:showing|of)\s+",
|
| 499 |
+
"",
|
| 500 |
+
clean,
|
| 501 |
+
flags=re.IGNORECASE,
|
| 502 |
+
)
|
| 503 |
+
clean = re.sub(r"^(?:a|an|the|and)\s+", "", clean, flags=re.IGNORECASE)
|
| 504 |
+
clean = re.sub(r"\s+", " ", clean).strip(" .,:;!?()[]{}\"'")
|
| 505 |
+
return clean
|
| 506 |
+
|
| 507 |
+
|
| 508 |
+
def _is_bad_visual_candidate_phrase(phrase: str) -> bool:
|
| 509 |
+
clean = re.sub(r"\s+", " ", (phrase or "").strip().lower())
|
| 510 |
+
if not clean:
|
| 511 |
+
return True
|
| 512 |
+
broken_markers = [
|
| 513 |
+
"various and a",
|
| 514 |
+
"various and an",
|
| 515 |
+
"with various",
|
| 516 |
+
"showing a table with various",
|
| 517 |
+
"visual model unavailable",
|
| 518 |
+
"model unavailable",
|
| 519 |
+
"details need user text confirmation",
|
| 520 |
+
"uploaded dream sketch",
|
| 521 |
+
]
|
| 522 |
+
if any(marker in clean for marker in broken_markers):
|
| 523 |
+
return True
|
| 524 |
+
if re.search(r"\b(?:and|or|with|of|to|for|a|an|the)\s*$", clean):
|
| 525 |
+
return True
|
| 526 |
+
if clean in {"drawing", "sketch", "image", "picture", "dream sketch"}:
|
| 527 |
+
return True
|
| 528 |
+
return False
|
| 529 |
+
|
| 530 |
+
|
| 531 |
def _visual_anchor_candidates(intake: DreamIntake) -> List[str]:
|
| 532 |
+
primary_candidates: List[str] = []
|
| 533 |
+
secondary_candidates: List[str] = []
|
| 534 |
for clue in intake.visual_clues:
|
| 535 |
+
clue_text = clue or ""
|
| 536 |
+
bucket = secondary_candidates if clue_text.lower().startswith("scene:") else primary_candidates
|
| 537 |
clean = _clean_visual_clue_text(clue)
|
| 538 |
if not clean:
|
| 539 |
continue
|
| 540 |
for phrase in re.split(r"\s*(?:,|;|,|;|/|\n)\s*", clean):
|
| 541 |
+
phrase = _clean_visual_candidate_phrase(phrase)
|
| 542 |
if not phrase:
|
| 543 |
continue
|
| 544 |
if len(phrase) > 48:
|
| 545 |
phrase = phrase[:48].rsplit(" ", 1)[0].strip() or phrase[:48].strip()
|
| 546 |
+
if _is_bad_visual_candidate_phrase(phrase):
|
| 547 |
+
continue
|
| 548 |
+
bucket.append(phrase)
|
| 549 |
+
return _dedupe_preserve_order(primary_candidates + secondary_candidates)
|
| 550 |
|
| 551 |
|
| 552 |
def _extract_dream_anchors(intake: DreamIntake) -> List[str]:
|
|
|
|
| 603 |
r"paper|papers|promise|promises|window|windows|suitcase|suitcases|"
|
| 604 |
r"clerk|clerks|sunrise|elevator|elevators|button|buttons|hallway|"
|
| 605 |
r"gate|gates|floor|floors|stamp|stamps|number|numbers|exam|exams|"
|
| 606 |
+
r"pencil|pencils|train|trains|platform|platforms|door|doors|subway|station|stations|snow|snowfield|"
|
| 607 |
+
r"hat|hats|phone|phones|water|puddle|puddles|shoe|shoes|"
|
| 608 |
r"stairwell|stairwells|room|rooms|key|keys|note|notes|rain|moon|moons|curtain|curtains|"
|
| 609 |
r"email|emails|meeting|meetings|deadline|deadlines|classroom|classrooms|teacher|teachers|"
|
| 610 |
r"assignment|assignments|homework|message|messages|airport|airports|glass|bird|birds|child|children|"
|
| 611 |
+
r"cat|cats|forest|forests|sign|signs|alley|alleys|mailbox|mailboxes|sketch|sketches|ceiling|ceilings|"
|
| 612 |
+
r"table|tables|clock|clocks|star|stars|sunrise"
|
| 613 |
r")\b"
|
| 614 |
)
|
| 615 |
for match in pair_pattern.finditer(text):
|
|
|
|
| 621 |
|
| 622 |
noun_pattern = re.compile(
|
| 623 |
r"\b(customs|suitcase|clerk|sunrise|elevator|button|hallway|gate|stamp|number|floor|"
|
| 624 |
+
r"exam|pencil|train|platform|door|subway|station|snow|snowfield|hat|phone|water|puddle|shoe|stairwell|"
|
| 625 |
+
r"room|key|keys|note|rain|moon|curtain|sleep|dream|email|meeting|deadline|classroom|teacher|"
|
| 626 |
+
r"assignment|homework|message|airport|glass|bird|child|cat|forest|sign|alley|mailbox|sketch|ceiling|"
|
| 627 |
+
r"table|tables|clock|clocks|star|stars|sunrise)\b"
|
| 628 |
)
|
| 629 |
candidates.extend(match.group(1) for match in noun_pattern.finditer(text))
|
| 630 |
candidates.extend(visual_candidates)
|
|
|
|
| 921 |
],
|
| 922 |
):
|
| 923 |
return "misunderstood_friend"
|
| 924 |
+
lost_child_terms = ["小孩", "child", "找不到家"]
|
| 925 |
+
transit_terms = ["地铁", "subway"]
|
| 926 |
+
home_terms = ["home", "回家", "找不到家"]
|
| 927 |
+
lost_terms = ["lost", "迷路", "找不到"]
|
| 928 |
+
if (
|
| 929 |
+
_contains_any(text, lost_child_terms)
|
| 930 |
+
or (_contains_any(text, transit_terms) and _contains_any(text, home_terms + lost_terms))
|
| 931 |
+
or (_contains_any(text, lost_terms) and _contains_any(text, home_terms))
|
| 932 |
+
):
|
| 933 |
return "lost_home"
|
| 934 |
+
if _contains_any(text, ["floating table", "loose key", "loose keys", "sunrise", "star shape", "star shapes", "漂浮的桌子", "钥匙"]):
|
| 935 |
+
return "floating_keys"
|
| 936 |
if _contains_any(text, ["海", "海浪", "月牙", "moon", "sea", "wave", "dark sea", "dark water", "漆黑的海"]):
|
| 937 |
return "dark_water"
|
| 938 |
if _contains_any(text, ["电梯", "按钮", "14", "elevator", "button", "floor"]):
|
| 939 |
return "stuck_elevator"
|
| 940 |
if _contains_any(text, ["图书馆", "楼梯", "便签", "call home", "library", "staircase", "sticky note"]):
|
| 941 |
return "library_signal"
|
| 942 |
+
if _contains_any(text, ["前任", "消息", "消失", "former partner", "message", "disappear"]) or re.search(r"\bex\b", text):
|
| 943 |
return "message_loss"
|
| 944 |
if _contains_any(text, ["追", "被追", "chase", "chased", "running away"]):
|
| 945 |
return "chased"
|
|
|
|
| 1074 |
for lead in lead_ins:
|
| 1075 |
if lead in lowered:
|
| 1076 |
return snippet[lowered.find(lead) :].strip(" ,.;:")
|
| 1077 |
+
return snippet.strip(" ,.;:")
|
| 1078 |
|
| 1079 |
|
| 1080 |
def _anchor_with_article(anchor: str) -> str:
|
|
|
|
| 1188 |
clean,
|
| 1189 |
flags=re.IGNORECASE,
|
| 1190 |
).strip()
|
| 1191 |
+
clean = re.sub(r"^(?:and|but|so)\s+", "", clean, flags=re.IGNORECASE).strip()
|
| 1192 |
clean = re.sub(
|
| 1193 |
+
r"^(i\s+)?(woke\s+up\s+)?"
|
| 1194 |
+
r"(want(?:ed)?\s+to\s+know|wonder(?:ed)?|am\s+wondering|need(?:ed)?\s+to\s+know|worr(?:y|ied)|fear(?:ed)?)"
|
| 1195 |
+
r"\s*(if|whether|why|what|how|which)?\s*",
|
| 1196 |
lambda match: (match.group(4) or "").strip() + " ",
|
| 1197 |
clean,
|
| 1198 |
flags=re.IGNORECASE,
|
| 1199 |
).strip()
|
| 1200 |
clean = clean[0].upper() + clean[1:] if clean else ""
|
| 1201 |
+
if clean and re.match(r"^(why|what|how|which|does|do|did|is|am|are|can|could|should|would)\b", clean, re.IGNORECASE):
|
| 1202 |
+
clean = clean.rstrip(".")
|
| 1203 |
+
if clean and not clean.endswith("?") and re.match(r"^(why|what|how|which|does|do|did|is|am|are|can|could|should|would)\b", clean, re.IGNORECASE):
|
| 1204 |
clean += "?"
|
| 1205 |
return clean
|
| 1206 |
|
|
|
|
| 1240 |
"can this",
|
| 1241 |
"should i",
|
| 1242 |
"i wonder",
|
| 1243 |
+
"i wondered",
|
| 1244 |
"want to know",
|
| 1245 |
+
"wanted to know",
|
| 1246 |
"not coping",
|
| 1247 |
"cope",
|
| 1248 |
"a sign",
|
|
|
|
| 1332 |
if "why" in lowered:
|
| 1333 |
return "The ache makes sense; the dream may be enlarging a feeling that has not had enough room yet."
|
| 1334 |
emotion = _emotion_phrase(labels, language)
|
| 1335 |
+
emotion_subject = emotion.capitalize() if emotion.startswith("this ") else f"The {emotion}"
|
| 1336 |
+
return f"{emotion_subject} you named deserves to be met first, not explained away too quickly."
|
| 1337 |
|
| 1338 |
|
| 1339 |
def _emotion_led_interpretation(intake: DreamIntake, answers: str, anchors: List[str], language: str = "en") -> str:
|
|
|
|
| 1352 |
"第三层,今天不急着找唯一答案,先给自己一个能站稳的小支点。"
|
| 1353 |
)
|
| 1354 |
opener = f"You asked, \"{question}\" " if question else ""
|
| 1355 |
+
emotion_subject = emotion if emotion.startswith("this ") else f"the {emotion}"
|
| 1356 |
return (
|
| 1357 |
f"{opener}{direct} First, let the {emotion} be real without blaming yourself. "
|
| 1358 |
+
f"Second, {_anchor_with_article(anchor)} may be the dream's concrete shape for that feeling. "
|
| 1359 |
"Third, for today the goal is not to solve the whole dream, but to give yourself one steadier place to stand."
|
| 1360 |
+
).replace(f"let the {emotion}", f"let {emotion_subject}")
|
| 1361 |
|
| 1362 |
|
| 1363 |
def _emotion_led_today_tip(intake: DreamIntake, answers: str, anchors: List[str], language: str = "en") -> str:
|
|
|
|
| 1497 |
if _is_zh(language):
|
| 1498 |
emotion = "感受" if emotion == "这个感受" else emotion
|
| 1499 |
return f"你不是太脆弱,也不是需要被催着立刻想通;这份{emotion}可以先被轻轻接住。"
|
| 1500 |
+
if emotion == "this feeling":
|
| 1501 |
+
return "You are not weak for needing a clearer next step; the dream can be met one grounded detail at a time."
|
| 1502 |
return f"You are not weak for feeling {emotion}; you do not have to turn it into a lesson before you are comforted."
|
| 1503 |
|
| 1504 |
|
|
|
|
| 1896 |
language,
|
| 1897 |
)
|
| 1898 |
if reality_cue:
|
| 1899 |
+
zh_anchor_context = f"{anchor} {reality_cue}"
|
| 1900 |
+
if any(term in zh_anchor_context for term in ["小孩", "地铁", "箭头", "路标", "哪里走", "带一下", "带路"]):
|
| 1901 |
+
return _numbered_suggestions(
|
| 1902 |
+
[
|
| 1903 |
+
f"把「{anchor}」和你刚才说的「{reality_cue}」接起来:这更像是在承认有一部分你需要被带路。",
|
| 1904 |
+
"今天先写一个很小的路标:我现在最需要谁或哪件事带我过一站;写完就停,不急着把整条路想完。",
|
| 1905 |
+
],
|
| 1906 |
+
language,
|
| 1907 |
+
)
|
| 1908 |
return _numbered_suggestions(
|
| 1909 |
[
|
| 1910 |
f"把「{anchor}」和你刚才说的「{reality_cue}」接起来,而不是只解释梦的象征。",
|
| 1911 |
+
"今天让这个梦里的细节决定下一步的形状:只写下一个你真的想试探的入口,然后先停在那里。",
|
| 1912 |
],
|
| 1913 |
language,
|
| 1914 |
)
|
|
|
|
| 1932 |
],
|
| 1933 |
language,
|
| 1934 |
)
|
| 1935 |
+
if any(term in anchor.lower() for term in ["elevator", "button", "puddle", "clock", "floor 14"]):
|
| 1936 |
+
return _numbered_suggestions(
|
| 1937 |
+
[
|
| 1938 |
+
f"Connect {anchor} to the work message pressure you named, without changing it into a different task.",
|
| 1939 |
+
"Use floor 14 in a puddle and the clock without hands as the boundary: write only what the message needs to convey before deciding when to open or send it.",
|
| 1940 |
+
],
|
| 1941 |
+
language,
|
| 1942 |
+
)
|
| 1943 |
return _numbered_suggestions(
|
| 1944 |
[
|
| 1945 |
f"Connect {anchor} to the work message pressure you named, without changing it into a different task.",
|
|
|
|
| 1989 |
language,
|
| 1990 |
)
|
| 1991 |
if reality_cue:
|
| 1992 |
+
if any(term in anchor.lower() for term in ["train", "platform", "suitcase", "direction sign"]):
|
| 1993 |
+
next_step = (
|
| 1994 |
+
"Use it as a tiny direction check today: write the work direction you named on one note, "
|
| 1995 |
+
"then choose one sign you can test before the day pulls away."
|
| 1996 |
+
)
|
| 1997 |
+
elif any(term in anchor.lower() for term in ["floating table", "loose key", "sunrise", "star shape"]):
|
| 1998 |
+
next_step = (
|
| 1999 |
+
"Use it as a morning key check: choose the one small key you named, try that before the day gets busy, "
|
| 2000 |
+
"and leave the other keys on the table for later."
|
| 2001 |
+
)
|
| 2002 |
+
elif any(term in anchor.lower() for term in ["puddle", "clock", "elevator", "floor 14", "button"]):
|
| 2003 |
+
next_step = (
|
| 2004 |
+
"Use floor 14 in a puddle and the clock without hands as a pressure check: write what the message "
|
| 2005 |
+
"needs to carry, then choose only the next button-sized move."
|
| 2006 |
+
)
|
| 2007 |
+
else:
|
| 2008 |
+
next_step = (
|
| 2009 |
+
"Let that dream detail shape the next step you named: write one line that starts with the real question, "
|
| 2010 |
+
"then stop before turning it into the whole task."
|
| 2011 |
+
)
|
| 2012 |
return _numbered_suggestions(
|
| 2013 |
[
|
| 2014 |
f"Connect {anchor} to the waking-life clue you named: \"{reality_cue}\".",
|
| 2015 |
+
next_step,
|
| 2016 |
],
|
| 2017 |
language,
|
| 2018 |
)
|
|
|
|
| 2458 |
"Which image stays with you most: the waves, the moon, or the small figure?",
|
| 2459 |
"Last check before the tip: do you need help calming your body, or understanding why the image felt so intense?",
|
| 2460 |
],
|
| 2461 |
+
"floating_keys": [
|
| 2462 |
+
"In the floating table, loose keys, and sunrise line, which detail feels most like the small key you want to try today?",
|
| 2463 |
+
"Do the loose keys feel like too many choices, or like one possible key waiting to be picked up?",
|
| 2464 |
+
"Last check before the tip: should the Morning Ticket point to one key, the sunrise line, or the floating table?",
|
| 2465 |
+
],
|
| 2466 |
"stuck_elevator": [
|
| 2467 |
f"Which detail in {story_anchor} feels closest to your current stuck point?",
|
| 2468 |
"Does the stuckness feel more like being late to start, or like trying something and getting no response?",
|
|
|
|
| 2887 |
)
|
| 2888 |
card_anchors = _without_unsupported_melted_anchors(card_anchors, intake, answers)
|
| 2889 |
card_anchors = _filter_anchors_by_source(card_anchors, intake, answers, language)
|
| 2890 |
+
if intake.visual_clues:
|
| 2891 |
+
anchors = _dedupe_preserve_order(intake_anchors + [anchor for anchor in card_anchors if anchor not in intake_anchors])
|
| 2892 |
+
else:
|
| 2893 |
+
anchors = _dedupe_preserve_order(card_anchors + [anchor for anchor in intake_anchors if anchor not in card_anchors])
|
| 2894 |
anchors = _without_unsupported_melted_anchors(anchors, intake, answers)
|
| 2895 |
if not anchors:
|
| 2896 |
anchors = _remove_placeholder_anchors([_primary_anchor(intake, language)])
|
|
|
|
| 2933 |
answer_interpretation = _answer_based_interpretation(answers, _answer_bridge_anchor(anchors), language)
|
| 2934 |
answer_tip = _answer_based_today_tip(answers, anchors[0], language)
|
| 2935 |
answer_action = _answer_based_tiny_action(answers, intake, anchors, language)
|
| 2936 |
+
reality_cue_for_tip = _answer_reality_cue(answers, language)
|
| 2937 |
answer_should_shape_visible_tip = bool(
|
| 2938 |
(answer_tip or answer_action)
|
| 2939 |
and not _needs_comfort(answers, language)
|
| 2940 |
and (
|
| 2941 |
_answer_has_concrete_task_keyword(answers)
|
| 2942 |
+
or bool(reality_cue_for_tip and intake.visual_clues)
|
| 2943 |
or not _should_use_emotion_led_response(intake, answers, language)
|
| 2944 |
)
|
| 2945 |
)
|
|
|
|
| 3354 |
transcript = asr_client.transcribe(audio_path) if asr_client else ""
|
| 3355 |
except Exception:
|
| 3356 |
error = "Voice transcription failed. Text-only path remains available."
|
| 3357 |
+
if not transcript.strip() and not error and getattr(asr_client, "last_error", ""):
|
| 3358 |
+
error = "Voice transcription did not return a transcript. Text-only path remains available."
|
| 3359 |
if transcript.strip():
|
| 3360 |
clean_transcript = transcript.strip()
|
| 3361 |
next_session.intake.voice_transcript = _append_text(next_session.intake.voice_transcript, clean_transcript)
|
dream_customs/ui/actions.py
CHANGED
|
@@ -42,6 +42,19 @@ def _latest_error(session: CustomsSession) -> str:
|
|
| 42 |
return event.body if event else ""
|
| 43 |
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
def _trim_to_one_visible_question(session: CustomsSession, previous_count: int) -> CustomsSession:
|
| 46 |
if len(session.question_history) <= previous_count + 1:
|
| 47 |
return session
|
|
@@ -170,6 +183,7 @@ def _view_payload(
|
|
| 170 |
"dream_anchors": list(session.qa_state.dream_anchors),
|
| 171 |
"followup_questions": list(session.question_history),
|
| 172 |
"user_answers": list(session.answer_history),
|
|
|
|
| 173 |
"interpretation": "",
|
| 174 |
"today_tip": "",
|
| 175 |
"tiny_action": "",
|
|
|
|
| 42 |
return event.body if event else ""
|
| 43 |
|
| 44 |
|
| 45 |
+
def _evidence_payload(session: CustomsSession) -> List[Dict[str, str]]:
|
| 46 |
+
return [
|
| 47 |
+
{
|
| 48 |
+
"type": item.type,
|
| 49 |
+
"label": item.label,
|
| 50 |
+
"status": item.status,
|
| 51 |
+
"content": item.content,
|
| 52 |
+
"error": item.error,
|
| 53 |
+
}
|
| 54 |
+
for item in session.evidence_items
|
| 55 |
+
]
|
| 56 |
+
|
| 57 |
+
|
| 58 |
def _trim_to_one_visible_question(session: CustomsSession, previous_count: int) -> CustomsSession:
|
| 59 |
if len(session.question_history) <= previous_count + 1:
|
| 60 |
return session
|
|
|
|
| 183 |
"dream_anchors": list(session.qa_state.dream_anchors),
|
| 184 |
"followup_questions": list(session.question_history),
|
| 185 |
"user_answers": list(session.answer_history),
|
| 186 |
+
"evidence_items": _evidence_payload(session),
|
| 187 |
"interpretation": "",
|
| 188 |
"today_tip": "",
|
| 189 |
"tiny_action": "",
|
dream_customs/ui/app.py
CHANGED
|
@@ -609,11 +609,13 @@ def _question_markdown(view: dict, language: str = DEFAULT_LANGUAGE) -> str:
|
|
| 609 |
if question
|
| 610 |
else ""
|
| 611 |
)
|
|
|
|
| 612 |
return f"""
|
| 613 |
<div class="dc-question-card">
|
| 614 |
<span class="dc-question-kicker">{copy['question_kicker']}</span>
|
| 615 |
<h2>{copy['question_title']}</h2>
|
| 616 |
<p>{copy['question_body']}</p>
|
|
|
|
| 617 |
{anchor_strip}
|
| 618 |
{optional_question}
|
| 619 |
{context_html}
|
|
@@ -965,8 +967,63 @@ def _field_tip_html(language: str = DEFAULT_LANGUAGE, view=None) -> str:
|
|
| 965 |
""".strip()
|
| 966 |
|
| 967 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 968 |
def _processing_html(language: str = DEFAULT_LANGUAGE) -> str:
|
| 969 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 970 |
|
| 971 |
|
| 972 |
def _side_stamp_html(language: str = DEFAULT_LANGUAGE) -> str:
|
|
|
|
| 609 |
if question
|
| 610 |
else ""
|
| 611 |
)
|
| 612 |
+
evidence_status = _evidence_status_html(view, language)
|
| 613 |
return f"""
|
| 614 |
<div class="dc-question-card">
|
| 615 |
<span class="dc-question-kicker">{copy['question_kicker']}</span>
|
| 616 |
<h2>{copy['question_title']}</h2>
|
| 617 |
<p>{copy['question_body']}</p>
|
| 618 |
+
{evidence_status}
|
| 619 |
{anchor_strip}
|
| 620 |
{optional_question}
|
| 621 |
{context_html}
|
|
|
|
| 967 |
""".strip()
|
| 968 |
|
| 969 |
|
| 970 |
+
def _evidence_status_html(view: dict, language: str = DEFAULT_LANGUAGE) -> str:
|
| 971 |
+
items = view.get("evidence_items") or []
|
| 972 |
+
if not items:
|
| 973 |
+
return ""
|
| 974 |
+
is_zh = normalize_language(language) == "zh"
|
| 975 |
+
type_labels = {
|
| 976 |
+
"text": "文字" if is_zh else "Text",
|
| 977 |
+
"image": "图片" if is_zh else "Image",
|
| 978 |
+
"audio": "语音" if is_zh else "Voice",
|
| 979 |
+
"mood": "心情" if is_zh else "Mood",
|
| 980 |
+
}
|
| 981 |
+
status_labels = {
|
| 982 |
+
"selected": "已记录" if is_zh else "noted",
|
| 983 |
+
"extracted": "已提取" if is_zh else "extracted",
|
| 984 |
+
"failed": "未提取" if is_zh else "not extracted",
|
| 985 |
+
"queued": "等待中" if is_zh else "queued",
|
| 986 |
+
}
|
| 987 |
+
pills = []
|
| 988 |
+
for item in items[-6:]:
|
| 989 |
+
item_type = str(item.get("type") or "")
|
| 990 |
+
status = str(item.get("status") or "queued")
|
| 991 |
+
label = type_labels.get(item_type, str(item.get("label") or item_type or "Evidence"))
|
| 992 |
+
status_text = status_labels.get(status, status)
|
| 993 |
+
detail = str(item.get("error") or item.get("content") or "").strip()
|
| 994 |
+
if len(detail) > 96:
|
| 995 |
+
detail = detail[:93].rstrip() + "..."
|
| 996 |
+
title = f" title=\"{escape(detail)}\"" if detail else ""
|
| 997 |
+
pills.append(
|
| 998 |
+
f"<span class='dc-evidence-pill is-{escape(status)}'{title}>"
|
| 999 |
+
f"<i aria-hidden='true'></i>{escape(label)} · {escape(status_text)}</span>"
|
| 1000 |
+
)
|
| 1001 |
+
if not pills:
|
| 1002 |
+
return ""
|
| 1003 |
+
caption = "输入证据" if is_zh else "Input evidence"
|
| 1004 |
+
return f"""
|
| 1005 |
+
<div class="dc-evidence-status" aria-label="{escape(caption)}">
|
| 1006 |
+
{''.join(pills)}
|
| 1007 |
+
</div>
|
| 1008 |
+
""".strip()
|
| 1009 |
+
|
| 1010 |
+
|
| 1011 |
def _processing_html(language: str = DEFAULT_LANGUAGE) -> str:
|
| 1012 |
+
if language == "zh":
|
| 1013 |
+
steps = ["文字线索", "图片线索", "语音转写"]
|
| 1014 |
+
aria = "处理通道"
|
| 1015 |
+
else:
|
| 1016 |
+
steps = ["Text clues", "Image clues", "Voice transcript"]
|
| 1017 |
+
aria = "Processing routes"
|
| 1018 |
+
step_html = "".join(f"<span>{escape(step)}</span>" for step in steps)
|
| 1019 |
+
return f"""
|
| 1020 |
+
<div class="dc-processing-panel" aria-live="polite">
|
| 1021 |
+
<p class='dc-processing-note'>{escape(copy_for(language)['processing_note'])}</p>
|
| 1022 |
+
<div class="dc-processing-steps" aria-label="{escape(aria)}">
|
| 1023 |
+
{step_html}
|
| 1024 |
+
</div>
|
| 1025 |
+
</div>
|
| 1026 |
+
""".strip()
|
| 1027 |
|
| 1028 |
|
| 1029 |
def _side_stamp_html(language: str = DEFAULT_LANGUAGE) -> str:
|
dream_customs/ui/copy.py
CHANGED
|
@@ -28,10 +28,10 @@ APP_COPY = {
|
|
| 28 |
"mic_listening": "Recording. Tap again to stop, or pause and MiMo ASR will transcribe.",
|
| 29 |
"mic_transcribing": "Transcribing with MiMo ASR...",
|
| 30 |
"mic_waking": "MiMo ASR is waking on Modal. This first pass can take a moment.",
|
| 31 |
-
"mic_done": "
|
| 32 |
"mic_empty": "No speech detected. Tap again if you want to retry.",
|
| 33 |
-
"mic_error": "Voice
|
| 34 |
-
"mic_timeout": "MiMo ASR timed out
|
| 35 |
"voice_label": "Voice note",
|
| 36 |
"voice_help": "Record or upload a short voice note. It is sent to MiMo ASR when you continue.",
|
| 37 |
"field_tip": "The desk looks for three anchors: a place, an object, and the question the dream left behind.",
|
|
@@ -103,10 +103,10 @@ APP_COPY = {
|
|
| 103 |
"mic_listening": "正在录音。再次点击可停止,停顿后会交给 MiMo ASR 转写。",
|
| 104 |
"mic_transcribing": "正在用 MiMo ASR 转写...",
|
| 105 |
"mic_waking": "Modal 上的 MiMo ASR 正在唤醒,首次转写可能需要等一下。",
|
| 106 |
-
"mic_done": "
|
| 107 |
"mic_empty": "没有检测到语音。想重试的话,再点一次麦克风。",
|
| 108 |
-
"mic_error": "语音转写
|
| 109 |
-
"mic_timeout": "MiMo ASR 转写超时
|
| 110 |
"voice_label": "语音片段",
|
| 111 |
"voice_help": "可以录音或上传一小段语音。点击继续后,会直接发送给 MiMo ASR 转写。",
|
| 112 |
"field_tip": "问讯室会优先寻找三个锚点:地点、物件、以及梦醒后留下的问题。",
|
|
|
|
| 28 |
"mic_listening": "Recording. Tap again to stop, or pause and MiMo ASR will transcribe.",
|
| 29 |
"mic_transcribing": "Transcribing with MiMo ASR...",
|
| 30 |
"mic_waking": "MiMo ASR is waking on Modal. This first pass can take a moment.",
|
| 31 |
+
"mic_done": "ASR transcript added to the dream note.",
|
| 32 |
"mic_empty": "No speech detected. Tap again if you want to retry.",
|
| 33 |
+
"mic_error": "Voice was not transcribed. Type that fragment or try again.",
|
| 34 |
+
"mic_timeout": "MiMo ASR timed out; no transcript was saved. Type it or retry.",
|
| 35 |
"voice_label": "Voice note",
|
| 36 |
"voice_help": "Record or upload a short voice note. It is sent to MiMo ASR when you continue.",
|
| 37 |
"field_tip": "The desk looks for three anchors: a place, an object, and the question the dream left behind.",
|
|
|
|
| 103 |
"mic_listening": "正在录音。再次点击可停止,停顿后会交给 MiMo ASR 转写。",
|
| 104 |
"mic_transcribing": "正在用 MiMo ASR 转写...",
|
| 105 |
"mic_waking": "Modal 上的 MiMo ASR 正在唤醒,首次转写可能需要等一下。",
|
| 106 |
+
"mic_done": "ASR 转写已加入梦境记录。",
|
| 107 |
"mic_empty": "没有检测到语音。想重试的话,再点一次麦克风。",
|
| 108 |
+
"mic_error": "语音没有完成转写。可以手动输入这一段,或再试一次。",
|
| 109 |
+
"mic_timeout": "MiMo ASR 转写超时,没有保存转写。可以手动输入或重试。",
|
| 110 |
"voice_label": "语音片段",
|
| 111 |
"voice_help": "可以录音或上传一小段语音。点击继续后,会直接发送给 MiMo ASR 转写。",
|
| 112 |
"field_tip": "问讯室会优先寻找三个锚点:地点、物件、以及梦醒后留下的问题。",
|
dream_customs/ui/styles.py
CHANGED
|
@@ -670,6 +670,32 @@ button.secondary {
|
|
| 670 |
font-weight: 850;
|
| 671 |
}
|
| 672 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 673 |
.dc-attachment-drawer {
|
| 674 |
background: transparent !important;
|
| 675 |
border: 0 !important;
|
|
@@ -1627,6 +1653,45 @@ body,
|
|
| 1627 |
width: 104px;
|
| 1628 |
}
|
| 1629 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1630 |
.dc-question-anchor-wrap {
|
| 1631 |
background: rgba(255, 253, 248, 0.62);
|
| 1632 |
border: 1px dashed rgba(95, 143, 104, 0.28);
|
|
@@ -3414,4 +3479,33 @@ CSS += """
|
|
| 3414 |
border-radius: 14px;
|
| 3415 |
}
|
| 3416 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3417 |
"""
|
|
|
|
| 670 |
font-weight: 850;
|
| 671 |
}
|
| 672 |
|
| 673 |
+
.dc-processing-panel {
|
| 674 |
+
margin-top: 12px;
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
.dc-processing-panel .dc-processing-note {
|
| 678 |
+
margin-top: 0;
|
| 679 |
+
}
|
| 680 |
+
|
| 681 |
+
.dc-processing-steps {
|
| 682 |
+
display: flex;
|
| 683 |
+
flex-wrap: wrap;
|
| 684 |
+
gap: 6px;
|
| 685 |
+
margin: 8px 2px 0;
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
.dc-processing-steps span {
|
| 689 |
+
background: rgba(255, 255, 255, 0.7);
|
| 690 |
+
border: 1px solid rgba(96, 85, 68, 0.16);
|
| 691 |
+
border-radius: 999px;
|
| 692 |
+
color: var(--dc-muted);
|
| 693 |
+
font-size: 0.76rem;
|
| 694 |
+
font-weight: 760;
|
| 695 |
+
line-height: 1;
|
| 696 |
+
padding: 6px 9px;
|
| 697 |
+
}
|
| 698 |
+
|
| 699 |
.dc-attachment-drawer {
|
| 700 |
background: transparent !important;
|
| 701 |
border: 0 !important;
|
|
|
|
| 1653 |
width: 104px;
|
| 1654 |
}
|
| 1655 |
|
| 1656 |
+
.dc-evidence-status {
|
| 1657 |
+
display: flex;
|
| 1658 |
+
flex-wrap: wrap;
|
| 1659 |
+
gap: 8px;
|
| 1660 |
+
margin: 14px 0 4px;
|
| 1661 |
+
}
|
| 1662 |
+
|
| 1663 |
+
.dc-evidence-pill {
|
| 1664 |
+
align-items: center;
|
| 1665 |
+
background: rgba(255, 253, 248, 0.8);
|
| 1666 |
+
border: 1px solid rgba(95, 143, 104, 0.24);
|
| 1667 |
+
border-radius: 999px;
|
| 1668 |
+
color: #35553d;
|
| 1669 |
+
display: inline-flex;
|
| 1670 |
+
font-size: 0.76rem;
|
| 1671 |
+
font-weight: 780;
|
| 1672 |
+
gap: 6px;
|
| 1673 |
+
line-height: 1.2;
|
| 1674 |
+
min-height: 30px;
|
| 1675 |
+
padding: 6px 10px;
|
| 1676 |
+
}
|
| 1677 |
+
|
| 1678 |
+
.dc-evidence-pill i {
|
| 1679 |
+
background: #5f8f68;
|
| 1680 |
+
border-radius: 999px;
|
| 1681 |
+
height: 8px;
|
| 1682 |
+
width: 8px;
|
| 1683 |
+
}
|
| 1684 |
+
|
| 1685 |
+
.dc-evidence-pill.is-failed {
|
| 1686 |
+
background: rgba(255, 247, 237, 0.92);
|
| 1687 |
+
border-color: rgba(181, 103, 47, 0.34);
|
| 1688 |
+
color: #7a4325;
|
| 1689 |
+
}
|
| 1690 |
+
|
| 1691 |
+
.dc-evidence-pill.is-failed i {
|
| 1692 |
+
background: #b5672f;
|
| 1693 |
+
}
|
| 1694 |
+
|
| 1695 |
.dc-question-anchor-wrap {
|
| 1696 |
background: rgba(255, 253, 248, 0.62);
|
| 1697 |
border: 1px dashed rgba(95, 143, 104, 0.28);
|
|
|
|
| 3479 |
border-radius: 14px;
|
| 3480 |
}
|
| 3481 |
}
|
| 3482 |
+
|
| 3483 |
+
.dc-processing-note.is-active {
|
| 3484 |
+
align-items: center;
|
| 3485 |
+
background: #fff5d8 !important;
|
| 3486 |
+
border: 2px solid rgba(181, 103, 47, 0.42) !important;
|
| 3487 |
+
border-radius: 8px !important;
|
| 3488 |
+
box-shadow: 0 10px 24px rgba(85, 65, 35, 0.12);
|
| 3489 |
+
color: #3f311f !important;
|
| 3490 |
+
display: flex;
|
| 3491 |
+
font-size: 0.9rem !important;
|
| 3492 |
+
font-weight: 720;
|
| 3493 |
+
gap: 10px;
|
| 3494 |
+
line-height: 1.45 !important;
|
| 3495 |
+
margin-top: 14px !important;
|
| 3496 |
+
min-height: 42px;
|
| 3497 |
+
padding: 10px 12px !important;
|
| 3498 |
+
}
|
| 3499 |
+
|
| 3500 |
+
.dc-processing-note.is-active::before {
|
| 3501 |
+
animation: dc-processing-pulse 1.1s ease-in-out infinite;
|
| 3502 |
+
background: #b5672f !important;
|
| 3503 |
+
border-radius: 999px;
|
| 3504 |
+
content: "" !important;
|
| 3505 |
+
display: inline-block;
|
| 3506 |
+
flex: 0 0 auto;
|
| 3507 |
+
height: 10px;
|
| 3508 |
+
position: static !important;
|
| 3509 |
+
width: 10px;
|
| 3510 |
+
}
|
| 3511 |
"""
|
tests/test_pipeline.py
CHANGED
|
@@ -110,6 +110,26 @@ def test_elevator_floor14_demo_anchors_are_clean_for_question_stage():
|
|
| 110 |
assert "for floor" not in state.dream_anchors
|
| 111 |
|
| 112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
def test_zh_company_nap_splashed_water_keeps_real_anchors():
|
| 114 |
session = add_evidence(
|
| 115 |
create_session(language="zh"),
|
|
@@ -1347,3 +1367,197 @@ def test_witness_failure_keeps_text_path_alive():
|
|
| 1347 |
|
| 1348 |
assert "Text still works." in intake.merged_text()
|
| 1349 |
assert "fallback clue" in intake.merged_text()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
assert "for floor" not in state.dream_anchors
|
| 111 |
|
| 112 |
|
| 113 |
+
def test_english_question_cleanup_handles_wanted_and_wondered_forms():
|
| 114 |
+
wanted = build_qa_state(
|
| 115 |
+
build_intake("I wanted to know which way to go next after the platform sign kept spinning."),
|
| 116 |
+
language="en",
|
| 117 |
+
)
|
| 118 |
+
leading_and = build_qa_state(
|
| 119 |
+
build_intake("The direction sign kept spinning, and I wanted to know which way to go next."),
|
| 120 |
+
language="en",
|
| 121 |
+
)
|
| 122 |
+
wondered = build_qa_state(
|
| 123 |
+
build_intake("I wondered why choosing a floor felt impossible."),
|
| 124 |
+
language="en",
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
assert wanted.main_question == "Which way to go next after the platform sign kept spinning?"
|
| 128 |
+
assert leading_and.main_question == "Which way to go next?"
|
| 129 |
+
assert wondered.main_question == "Why choosing a floor felt impossible?"
|
| 130 |
+
assert "Ed why" not in wondered.main_question
|
| 131 |
+
|
| 132 |
+
|
| 133 |
def test_zh_company_nap_splashed_water_keeps_real_anchors():
|
| 134 |
session = add_evidence(
|
| 135 |
create_session(language="zh"),
|
|
|
|
| 1367 |
|
| 1368 |
assert "Text still works." in intake.merged_text()
|
| 1369 |
assert "fallback clue" in intake.merged_text()
|
| 1370 |
+
|
| 1371 |
+
|
| 1372 |
+
def test_train_platform_sketch_does_not_inherit_elevator_or_lost_child_defaults():
|
| 1373 |
+
class TrainPlatformVision:
|
| 1374 |
+
def extract_witness(self, image_path):
|
| 1375 |
+
return VisionWitness(
|
| 1376 |
+
scene_summary="A train platform with a suitcase, a direction sign, and a tall clock.",
|
| 1377 |
+
objects=["train platform", "suitcase", "direction sign", "platform clock"],
|
| 1378 |
+
visible_text=["TRACK 3", "NORTH EXIT"],
|
| 1379 |
+
mood_cues=["waiting", "uncertain direction"],
|
| 1380 |
+
)
|
| 1381 |
+
|
| 1382 |
+
def extract_clues(self, image_path):
|
| 1383 |
+
return ["flat fallback should not win"]
|
| 1384 |
+
|
| 1385 |
+
session = add_evidence(
|
| 1386 |
+
create_session(),
|
| 1387 |
+
dream_text=(
|
| 1388 |
+
"I was waiting on a train platform with a suitcase. "
|
| 1389 |
+
"The direction sign kept spinning, and I wanted to know which way to go next."
|
| 1390 |
+
),
|
| 1391 |
+
image_path="ux01_train_platform_sketch.png",
|
| 1392 |
+
audio_path="ux01_voice.wav",
|
| 1393 |
+
mood="uneasy",
|
| 1394 |
+
vision_client=TrainPlatformVision(),
|
| 1395 |
+
asr_client=FakeASRClient(),
|
| 1396 |
+
)
|
| 1397 |
+
session = ask_questions(session, FakeTextClient())
|
| 1398 |
+
card = generate_today_tip(
|
| 1399 |
+
session.intake,
|
| 1400 |
+
"It feels like my real question is whether I am waiting for permission to choose a direction at work today.",
|
| 1401 |
+
FakeTextClient(),
|
| 1402 |
+
followup_questions=session.question_history,
|
| 1403 |
+
)
|
| 1404 |
+
combined = "\n".join(
|
| 1405 |
+
[
|
| 1406 |
+
session.question_history[0],
|
| 1407 |
+
",".join(card.dream_anchors),
|
| 1408 |
+
card.dream_summary,
|
| 1409 |
+
card.interpretation,
|
| 1410 |
+
card.today_tip,
|
| 1411 |
+
card.tiny_action,
|
| 1412 |
+
card.caring_note,
|
| 1413 |
+
]
|
| 1414 |
+
).lower()
|
| 1415 |
+
|
| 1416 |
+
assert any(anchor in card.dream_anchors for anchor in ["train platform", "suitcase", "direction sign"])
|
| 1417 |
+
assert "elevator" not in combined
|
| 1418 |
+
assert "floor 14" not in combined
|
| 1419 |
+
assert "melted" not in combined
|
| 1420 |
+
assert "lost child" not in combined
|
| 1421 |
+
assert "message parachute" not in combined
|
| 1422 |
+
assert "the this feeling" not in combined
|
| 1423 |
+
assert "the the" not in combined
|
| 1424 |
+
assert "reversible first step" not in card.today_tip.lower()
|
| 1425 |
+
assert "direction check" in card.today_tip.lower() or "one sign" in card.today_tip.lower()
|
| 1426 |
+
|
| 1427 |
+
|
| 1428 |
+
def test_elevator_buttons_sketch_preserves_puddle_clock_and_choice_pressure():
|
| 1429 |
+
session = add_evidence(
|
| 1430 |
+
create_session(),
|
| 1431 |
+
dream_text=(
|
| 1432 |
+
"I dreamed I was in an apartment elevator lobby. The doors would not open, "
|
| 1433 |
+
"floor fourteen appeared in a puddle, and the buttons melted before I could press anything."
|
| 1434 |
+
),
|
| 1435 |
+
image_path="ux02_elevator_buttons_sketch.png",
|
| 1436 |
+
audio_path="ux02_voice.wav",
|
| 1437 |
+
mood="anxious",
|
| 1438 |
+
vision_client=FakeVisionClient(),
|
| 1439 |
+
asr_client=FakeASRClient(),
|
| 1440 |
+
)
|
| 1441 |
+
session = ask_questions(session, FakeTextClient())
|
| 1442 |
+
card = generate_today_tip(
|
| 1443 |
+
session.intake,
|
| 1444 |
+
"The strongest feeling was that every option would make me late, especially a message I have been avoiding.",
|
| 1445 |
+
FakeTextClient(),
|
| 1446 |
+
followup_questions=session.question_history,
|
| 1447 |
+
)
|
| 1448 |
+
combined = "\n".join(
|
| 1449 |
+
[
|
| 1450 |
+
session.question_history[0],
|
| 1451 |
+
",".join(card.dream_anchors),
|
| 1452 |
+
card.dream_summary,
|
| 1453 |
+
card.interpretation,
|
| 1454 |
+
card.today_tip,
|
| 1455 |
+
card.tiny_action,
|
| 1456 |
+
card.caring_note,
|
| 1457 |
+
]
|
| 1458 |
+
).lower()
|
| 1459 |
+
|
| 1460 |
+
assert "floor 14" in combined
|
| 1461 |
+
assert "puddle" in combined
|
| 1462 |
+
assert "clock" in combined
|
| 1463 |
+
assert any("puddle" in anchor for anchor in card.dream_anchors)
|
| 1464 |
+
assert any("clock" in anchor for anchor in card.dream_anchors)
|
| 1465 |
+
assert "choice pressure" in session.intake.merged_text().lower()
|
| 1466 |
+
assert "train platform" not in combined
|
| 1467 |
+
assert "lost child" not in combined
|
| 1468 |
+
assert "feeling this feeling" not in combined
|
| 1469 |
+
assert "reversible first step" not in card.today_tip.lower()
|
| 1470 |
+
assert "floor 14" in card.today_tip.lower() or "clock without hands" in card.today_tip.lower()
|
| 1471 |
+
|
| 1472 |
+
|
| 1473 |
+
def test_broken_visual_sentence_is_not_used_as_anchor_or_tip_copy():
|
| 1474 |
+
class BrokenTableVision:
|
| 1475 |
+
def extract_witness(self, image_path):
|
| 1476 |
+
return VisionWitness(
|
| 1477 |
+
scene_summary="A drawing showing a table with various and a",
|
| 1478 |
+
objects=["floating table", "loose keys", "star shapes"],
|
| 1479 |
+
visible_text=["SUNRISE"],
|
| 1480 |
+
spatial_relations=["keys hover over dark water"],
|
| 1481 |
+
)
|
| 1482 |
+
|
| 1483 |
+
def extract_clues(self, image_path):
|
| 1484 |
+
return ["a drawing showing a table with various and a", "floating table", "loose keys"]
|
| 1485 |
+
|
| 1486 |
+
session = add_evidence(
|
| 1487 |
+
create_session(),
|
| 1488 |
+
dream_text="I saw a floating table above dark water, with keys, stars, and a sunrise line.",
|
| 1489 |
+
image_path="ux03_floating_table_keys_sketch.png",
|
| 1490 |
+
mood="curious",
|
| 1491 |
+
vision_client=BrokenTableVision(),
|
| 1492 |
+
asr_client=FakeASRClient(),
|
| 1493 |
+
)
|
| 1494 |
+
session = ask_questions(session, FakeTextClient())
|
| 1495 |
+
card = generate_today_tip(
|
| 1496 |
+
session.intake,
|
| 1497 |
+
"I think the question is which small key to try first before the day gets busy.",
|
| 1498 |
+
FakeTextClient(),
|
| 1499 |
+
followup_questions=session.question_history,
|
| 1500 |
+
)
|
| 1501 |
+
combined = "\n".join(
|
| 1502 |
+
[
|
| 1503 |
+
session.question_history[0],
|
| 1504 |
+
",".join(card.dream_anchors),
|
| 1505 |
+
card.dream_summary,
|
| 1506 |
+
card.interpretation,
|
| 1507 |
+
card.today_tip,
|
| 1508 |
+
card.tiny_action,
|
| 1509 |
+
card.caring_note,
|
| 1510 |
+
]
|
| 1511 |
+
).lower()
|
| 1512 |
+
|
| 1513 |
+
assert any(anchor in card.dream_anchors for anchor in ["floating table", "loose key", "dark water", "sunrise"])
|
| 1514 |
+
assert "various and a" not in combined
|
| 1515 |
+
assert "drawing showing" not in combined
|
| 1516 |
+
assert "the this feeling" not in combined
|
| 1517 |
+
assert "the the" not in combined
|
| 1518 |
+
assert "reversible first step" not in card.today_tip.lower()
|
| 1519 |
+
assert "morning key check" in card.today_tip.lower() or "before the day gets busy" in card.today_tip.lower()
|
| 1520 |
+
|
| 1521 |
+
|
| 1522 |
+
def test_floating_table_demo_keeps_star_and_sunrise_image_details():
|
| 1523 |
+
session = add_evidence(
|
| 1524 |
+
create_session(),
|
| 1525 |
+
dream_text=(
|
| 1526 |
+
"I dreamed my kitchen table was floating on shallow water. "
|
| 1527 |
+
"Three keys were arranged in a triangle, a cup spilled tiny stars, "
|
| 1528 |
+
"and morning light was coming through a square window."
|
| 1529 |
+
),
|
| 1530 |
+
image_path="ux03_floating_table_keys_sketch.png",
|
| 1531 |
+
audio_path="ux03_voice.wav",
|
| 1532 |
+
mood="curious",
|
| 1533 |
+
vision_client=FakeVisionClient(),
|
| 1534 |
+
asr_client=FakeASRClient(),
|
| 1535 |
+
)
|
| 1536 |
+
session = ask_questions(session, FakeTextClient())
|
| 1537 |
+
card = generate_today_tip(
|
| 1538 |
+
session.intake,
|
| 1539 |
+
"I think the question is which small key to try first before the day gets busy.",
|
| 1540 |
+
FakeTextClient(),
|
| 1541 |
+
followup_questions=session.question_history,
|
| 1542 |
+
)
|
| 1543 |
+
combined = "\n".join(
|
| 1544 |
+
[
|
| 1545 |
+
session.question_history[0],
|
| 1546 |
+
",".join(card.dream_anchors),
|
| 1547 |
+
card.dream_summary,
|
| 1548 |
+
card.interpretation,
|
| 1549 |
+
card.today_tip,
|
| 1550 |
+
card.tiny_action,
|
| 1551 |
+
card.caring_note,
|
| 1552 |
+
]
|
| 1553 |
+
).lower()
|
| 1554 |
+
|
| 1555 |
+
assert "floating table" in combined
|
| 1556 |
+
assert "key" in combined
|
| 1557 |
+
assert "star" in combined
|
| 1558 |
+
assert "sunrise" in combined
|
| 1559 |
+
assert "floating table" in session.question_history[0].lower()
|
| 1560 |
+
assert "loose keys" in session.question_history[0].lower()
|
| 1561 |
+
assert "sunrise" in session.question_history[0].lower()
|
| 1562 |
+
assert "floor 14" not in combined
|
| 1563 |
+
assert "elevator" not in combined
|
tests/test_ui_actions.py
CHANGED
|
@@ -275,6 +275,33 @@ def test_question_stage_shows_anchor_chips_before_the_question():
|
|
| 275 |
assert html.index("When floor 14") < html.index("Why this question")
|
| 276 |
|
| 277 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
def test_long_running_buttons_show_processing_feedback():
|
| 279 |
source = inspect.getsource(ui_app.build_demo)
|
| 280 |
app_source = inspect.getsource(ui_app)
|
|
@@ -292,6 +319,11 @@ def test_long_running_buttons_show_processing_feedback():
|
|
| 292 |
assert "dc-is-loading" in ui_app.VOICE_JS
|
| 293 |
assert "Asking from another angle" in ui_app.VOICE_JS
|
| 294 |
assert "one more grounded question" in ui_app.VOICE_JS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 295 |
assert "js=SUBMIT_PROCESSING_JS" not in source
|
| 296 |
assert "js=TIP_PROCESSING_JS" not in source
|
| 297 |
assert source.count("scroll_to_output=True") >= 5
|
|
@@ -299,6 +331,7 @@ def test_long_running_buttons_show_processing_feedback():
|
|
| 299 |
assert "Folding the follow-up answer" in app_source
|
| 300 |
assert ".dc-notice.is-processing" in CSS
|
| 301 |
assert ".dc-processing-note.is-active" in CSS
|
|
|
|
| 302 |
assert ".dc-stage button.dc-is-loading" in CSS
|
| 303 |
assert "endpoint" not in ui_app.VOICE_JS.lower()
|
| 304 |
assert "debug" not in ui_app.VOICE_JS.lower()
|
|
|
|
| 275 |
assert html.index("When floor 14") < html.index("Why this question")
|
| 276 |
|
| 277 |
|
| 278 |
+
def test_question_stage_surfaces_evidence_status_and_failed_asr():
|
| 279 |
+
_state, view_json = submit_dream_action(
|
| 280 |
+
dream_text="I was waiting on a train platform with a suitcase and a spinning direction sign.",
|
| 281 |
+
image_value="ux01_train_platform_sketch.png",
|
| 282 |
+
audio_value="ux01_voice.wav",
|
| 283 |
+
mood="Uneasy",
|
| 284 |
+
text_backend="demo",
|
| 285 |
+
vision_backend="demo",
|
| 286 |
+
language="en",
|
| 287 |
+
asr_backend="modal",
|
| 288 |
+
asr_endpoint="",
|
| 289 |
+
)
|
| 290 |
+
view = json.loads(view_json)
|
| 291 |
+
statuses = {(item["type"], item["status"]) for item in view["evidence_items"]}
|
| 292 |
+
html = ui_app._question_markdown(view, "en")
|
| 293 |
+
|
| 294 |
+
assert ("text", "selected") in statuses
|
| 295 |
+
assert ("image", "extracted") in statuses
|
| 296 |
+
assert ("audio", "failed") in statuses
|
| 297 |
+
assert "dc-evidence-status" in html
|
| 298 |
+
assert "Text · noted" in html
|
| 299 |
+
assert "Image · extracted" in html
|
| 300 |
+
assert "Voice · not extracted" in html
|
| 301 |
+
assert "Voice transcription did not return a transcript" in html
|
| 302 |
+
assert ".dc-evidence-pill.is-failed" in CSS
|
| 303 |
+
|
| 304 |
+
|
| 305 |
def test_long_running_buttons_show_processing_feedback():
|
| 306 |
source = inspect.getsource(ui_app.build_demo)
|
| 307 |
app_source = inspect.getsource(ui_app)
|
|
|
|
| 319 |
assert "dc-is-loading" in ui_app.VOICE_JS
|
| 320 |
assert "Asking from another angle" in ui_app.VOICE_JS
|
| 321 |
assert "one more grounded question" in ui_app.VOICE_JS
|
| 322 |
+
processing = ui_app._processing_html("en")
|
| 323 |
+
assert "dc-processing-steps" in processing
|
| 324 |
+
assert "Text clues" in processing
|
| 325 |
+
assert "Image clues" in processing
|
| 326 |
+
assert "Voice transcript" in processing
|
| 327 |
assert "js=SUBMIT_PROCESSING_JS" not in source
|
| 328 |
assert "js=TIP_PROCESSING_JS" not in source
|
| 329 |
assert source.count("scroll_to_output=True") >= 5
|
|
|
|
| 331 |
assert "Folding the follow-up answer" in app_source
|
| 332 |
assert ".dc-notice.is-processing" in CSS
|
| 333 |
assert ".dc-processing-note.is-active" in CSS
|
| 334 |
+
assert "#fff5d8" in CSS
|
| 335 |
assert ".dc-stage button.dc-is-loading" in CSS
|
| 336 |
assert "endpoint" not in ui_app.VOICE_JS.lower()
|
| 337 |
assert "debug" not in ui_app.VOICE_JS.lower()
|
videos/2026-06-15-dreamqa-v2/ux01_evidence.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "ux01",
|
| 3 |
+
"title": "Train platform",
|
| 4 |
+
"url": "http://127.0.0.1:7862",
|
| 5 |
+
"imagePath": "/Users/junhaocheng/working-dir/ai-competitions/build-small-hackthon/videos/2026-06-15-dreamqa-full-ux/assets/ux01_train_platform_sketch.png",
|
| 6 |
+
"audioPath": "/Users/junhaocheng/working-dir/ai-competitions/build-small-hackthon/videos/2026-06-15-dreamqa-full-ux/assets/ux01_voice.wav",
|
| 7 |
+
"startedAt": "2026-06-15T04:39:39.530Z",
|
| 8 |
+
"steps": [
|
| 9 |
+
"opened_local_app",
|
| 10 |
+
"filled_text_dream",
|
| 11 |
+
"uploaded_image_sketch",
|
| 12 |
+
"uploaded_voice_wav",
|
| 13 |
+
"submitted_intake",
|
| 14 |
+
"received_followup_question",
|
| 15 |
+
"answered_followup",
|
| 16 |
+
"received_morning_ticket"
|
| 17 |
+
],
|
| 18 |
+
"status": "passed",
|
| 19 |
+
"voiceStatusAfterInput": "Voice was not transcribed. Type that fragment or try again.",
|
| 20 |
+
"questionText": "\n Step 3 of 4 · Answer or skip\n One useful question\n Answer in one or two lines, or skip and let the existing anchors carry the ticket.\n \n Text · notedMood · notedImage · extractedVoice · not extracted\n\n Sticky details already on the desktrain platformsuitcasedirection sign\n Morning Question DeskIn train platform, suitcase, and direction sign, what feeling stayed strongest after waking?\n Why this questionWhat I hear first is that the strongest trail begins with train platform, suitcase, and direction sign, so I will connect it to what you may need today. I will unpack this in three layers: the feeling after waking, the concrete image (train platform, suitcase, and direction sign), and what it may be asking you to care for today. One question that will shape the final tip\n This step makes the tip more specific. It is not diagnosis.\n",
|
| 21 |
+
"ticketText": "\n \n \n Morning Ticket\n Not a diagnosis, not a prophecy. Just one small note for today, grounded in the dream.\n \n \n Take one note today\n Grounded in the dream\n \n \n train platformsuitcasedirection sign\n \n Because your dream kept returning to:train platform suitcase direction sign\n \n Today Tip\n Connect train platform to the waking-life clue you named: \"It feels like my real question is whether I am waiting for permission to choose a direction at work today\". Use it as a tiny direction check today: write the work direction you named on one note, then choose one sign you can test before the day pulls away.\n \n Tiny 5-minute actionPlace two pens like two roads, put a note labeled train platform, suitcase, and direction sign between them, spin the note once, and check only the entrance it points to.\n \n Supporting reflection\n Maybe the train platform is not asking for a fixed dream meaning. It is pointing back to the waking-life clue you named: \"It feels like my real question is whether I am waiting for permission to choose a direction at work today\".\n \n \n \n How this was made small\n MiniCPM-V reads sketches, notes, and image clues; MiniCPM5-1B writes the follow-up question, reflection, and ticket.\n \n Question recordWhat I hear first is that the strongest trail begins with train platform, suitcase, and direction sign, so I will connect it to what you may need today. I will unpack this in three layers: the feeling after waking, the concrete image (train platform, suitcase, and direction sign), and what it may be asking you to care for today. One question that will shape the final tip: In train platform, suitcase, and direction sign, what feeling stayed strongest after wakingIt feels like my real question is whether I am waiting for permission to choose a direction at work today\n A gentle reflection for today, not a diagnosis or prophecy.\n Care noteYou do not have to solve the whole dream this morning; start by noticing one detail and one small next step.\n",
|
| 22 |
+
"evidenceStatus": "\n Text · notedMood · notedImage · extractedVoice · not extracted\n",
|
| 23 |
+
"processingRoutes": "\n Text cluesImage cluesVoice transcript\n ",
|
| 24 |
+
"assertions": {
|
| 25 |
+
"requiredTermsPresent": true,
|
| 26 |
+
"bannedTermsAbsent": true,
|
| 27 |
+
"noBrokenVisualPhrase": true,
|
| 28 |
+
"evidenceStatusVisible": true,
|
| 29 |
+
"processingRoutesVisible": true,
|
| 30 |
+
"todayTipUsesAnswerClue": true,
|
| 31 |
+
"todayTipUsesDemoSpecificCue": true,
|
| 32 |
+
"questionUsesDemoSpecificCue": true,
|
| 33 |
+
"voiceFallbackHonest": true,
|
| 34 |
+
"noTemplateTipPhrase": true,
|
| 35 |
+
"noSafetyBoundaryLeak": true
|
| 36 |
+
},
|
| 37 |
+
"endedAt": "2026-06-15T04:40:03.195Z",
|
| 38 |
+
"videoWebm": "/Users/junhaocheng/working-dir/ai-competitions/build-small-hackthon/videos/2026-06-15-dreamqa-full-ux/ux01_full_ux.webm",
|
| 39 |
+
"videoPath": "videos/2026-06-15-dreamqa-v2/ux01_full_ux.mp4",
|
| 40 |
+
"duration": 24.32,
|
| 41 |
+
"sizeBytes": 1642562,
|
| 42 |
+
"questionVisibleForSeconds": 3.6,
|
| 43 |
+
"finalTicketVisibleForSeconds": 4.3,
|
| 44 |
+
"voiceEvidenceStatus": "fallback: Voice was not transcribed. Type that fragment or try again.",
|
| 45 |
+
"finalAnchors": [
|
| 46 |
+
"train platform",
|
| 47 |
+
"suitcase",
|
| 48 |
+
"direction sign"
|
| 49 |
+
],
|
| 50 |
+
"todayTipText": "Connect train platform to the waking-life clue you named: \"It feels like my real question is whether I am waiting for permission to choose a direction at work today\". Use it as a tiny direction check today: write the work direction you named on one note, then choose one sign you can test before the day pulls away.",
|
| 51 |
+
"requiredAnswerClues": [
|
| 52 |
+
"permission",
|
| 53 |
+
"direction at work"
|
| 54 |
+
],
|
| 55 |
+
"requiredTipClues": [
|
| 56 |
+
"direction check",
|
| 57 |
+
"one sign"
|
| 58 |
+
],
|
| 59 |
+
"requiredQuestionClues": [
|
| 60 |
+
"train platform",
|
| 61 |
+
"direction"
|
| 62 |
+
]
|
| 63 |
+
}
|
videos/2026-06-15-dreamqa-v2/ux01_final_ticket.png
ADDED
|
Git LFS Details
|
videos/2026-06-15-dreamqa-v2/ux01_full_ux.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c9207c017c7d788c1f6a612097b256cdedbc7899d5f27929d1ed7020a2ca8caf
|
| 3 |
+
size 1642562
|
videos/2026-06-15-dreamqa-v2/ux01_timeline.jpg
ADDED
|
Git LFS Details
|
videos/2026-06-15-dreamqa-v2/ux02_evidence.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "ux02",
|
| 3 |
+
"title": "Elevator buttons",
|
| 4 |
+
"url": "http://127.0.0.1:7862",
|
| 5 |
+
"imagePath": "/Users/junhaocheng/working-dir/ai-competitions/build-small-hackthon/videos/2026-06-15-dreamqa-full-ux/assets/ux02_elevator_buttons_sketch.png",
|
| 6 |
+
"audioPath": "/Users/junhaocheng/working-dir/ai-competitions/build-small-hackthon/videos/2026-06-15-dreamqa-full-ux/assets/ux02_voice.wav",
|
| 7 |
+
"startedAt": "2026-06-15T04:40:26.093Z",
|
| 8 |
+
"steps": [
|
| 9 |
+
"opened_local_app",
|
| 10 |
+
"filled_text_dream",
|
| 11 |
+
"uploaded_image_sketch",
|
| 12 |
+
"uploaded_voice_wav",
|
| 13 |
+
"submitted_intake",
|
| 14 |
+
"received_followup_question",
|
| 15 |
+
"answered_followup",
|
| 16 |
+
"received_morning_ticket"
|
| 17 |
+
],
|
| 18 |
+
"status": "passed",
|
| 19 |
+
"voiceStatusAfterInput": "Voice was not transcribed. Type that fragment or try again.",
|
| 20 |
+
"questionText": "\n Step 3 of 4 · Answer or skip\n One useful question\n Answer in one or two lines, or skip and let the existing anchors carry the ticket.\n \n Text · notedMood · notedImage · extractedVoice · not extracted\n\n Sticky details already on the deskmelted elevator buttonsfloor 14 in a puddleclock without hands\n Morning Question DeskWhich detail in melted elevator buttons, elevator, and floor 14 in a puddle feels closest to your current stuck point?\n Why this questionWhat I hear first is that the strongest trail begins with melted elevator buttons, elevator, and floor 14 in a puddle, so I will connect it to what you may need today. I will unpack this in three layers: the feeling after waking, the concrete image (melted elevator buttons, elevator, and floor 14 in a puddle), and what it may be asking you to care for today. One question that will shape the final tip\n This step makes the tip more specific. It is not diagnosis.\n",
|
| 21 |
+
"ticketText": "\n \n \n Morning Ticket\n Not a diagnosis, not a prophecy. Just one small note for today, grounded in the dream.\n \n \n Take one note today\n Grounded in the dream\n \n \n melted elevator buttonsfloor 14 in a puddleclock without hands\n \n Because your dream kept returning to:melted elevator buttons floor 14 in a puddle clock without hands\n \n Today Tip\n Connect melted elevator buttons to the work message pressure you named, without changing it into a different task. Use floor 14 in a puddle and the clock without hands as the boundary: write only what the message needs to convey before deciding when to open or send it.\n \n Tiny 5-minute actionDraw a tiny elevator button labeled melted elevator buttons, elevator, and floor 14 in a puddle, press it once with your finger, then name the smallest doorway action you can try today.\n \n Supporting reflection\n Maybe the floor 14 in a puddle is not asking you to finish every message at once. It is pointing to the gentler threshold: naming what the message needs to carry.\n \n \n \n How this was made small\n MiniCPM-V reads sketches, notes, and image clues; MiniCPM5-1B writes the follow-up question, reflection, and ticket.\n \n Question recordWhat I hear first is that the strongest trail begins with melted elevator buttons, elevator, and floor 14 in a puddle, so I will connect it to what you may need today. I will unpack this in three layers: the feeling after waking, the concrete image (melted elevator buttons, elevator, and floor 14 in a puddle), and what it may be asking you to care for today. One question that will shape the final tip: Which detail in melted elevator buttons, elevator, and floor 14 in a puddle feels closest to your current stuck pointThe strongest feeling was that every option would make me late, especially a message I have been avoiding\n A gentle reflection for today, not a diagnosis or prophecy.\n Care noteYou do not have to solve the whole dream this morning; start by noticing one detail and one small next step.\n",
|
| 22 |
+
"evidenceStatus": "\n Text · notedMood · notedImage · extractedVoice · not extracted\n",
|
| 23 |
+
"processingRoutes": "\n Text cluesImage cluesVoice transcript\n ",
|
| 24 |
+
"assertions": {
|
| 25 |
+
"requiredTermsPresent": true,
|
| 26 |
+
"bannedTermsAbsent": true,
|
| 27 |
+
"noBrokenVisualPhrase": true,
|
| 28 |
+
"evidenceStatusVisible": true,
|
| 29 |
+
"processingRoutesVisible": true,
|
| 30 |
+
"todayTipUsesAnswerClue": true,
|
| 31 |
+
"todayTipUsesDemoSpecificCue": true,
|
| 32 |
+
"questionUsesDemoSpecificCue": true,
|
| 33 |
+
"voiceFallbackHonest": true,
|
| 34 |
+
"noTemplateTipPhrase": true,
|
| 35 |
+
"noSafetyBoundaryLeak": true
|
| 36 |
+
},
|
| 37 |
+
"endedAt": "2026-06-15T04:40:48.693Z",
|
| 38 |
+
"videoWebm": "/Users/junhaocheng/working-dir/ai-competitions/build-small-hackthon/videos/2026-06-15-dreamqa-full-ux/ux02_full_ux.webm",
|
| 39 |
+
"videoPath": "videos/2026-06-15-dreamqa-v2/ux02_full_ux.mp4",
|
| 40 |
+
"duration": 23.36,
|
| 41 |
+
"sizeBytes": 1803192,
|
| 42 |
+
"questionVisibleForSeconds": 3.6,
|
| 43 |
+
"finalTicketVisibleForSeconds": 4.3,
|
| 44 |
+
"voiceEvidenceStatus": "fallback: Voice was not transcribed. Type that fragment or try again.",
|
| 45 |
+
"finalAnchors": [
|
| 46 |
+
"melted elevator buttons",
|
| 47 |
+
"floor 14 in a puddle",
|
| 48 |
+
"clock without hands"
|
| 49 |
+
],
|
| 50 |
+
"todayTipText": "Connect melted elevator buttons to the work message pressure you named, without changing it into a different task. Use floor 14 in a puddle and the clock without hands as the boundary: write only what the message needs to convey before deciding when to open or send it.",
|
| 51 |
+
"requiredAnswerClues": [
|
| 52 |
+
"message",
|
| 53 |
+
"late"
|
| 54 |
+
],
|
| 55 |
+
"requiredTipClues": [
|
| 56 |
+
"floor 14 in a puddle",
|
| 57 |
+
"clock without hands",
|
| 58 |
+
"message needs to convey"
|
| 59 |
+
],
|
| 60 |
+
"requiredQuestionClues": [
|
| 61 |
+
"melted elevator buttons",
|
| 62 |
+
"floor 14"
|
| 63 |
+
]
|
| 64 |
+
}
|
videos/2026-06-15-dreamqa-v2/ux02_final_ticket.png
ADDED
|
Git LFS Details
|
videos/2026-06-15-dreamqa-v2/ux02_full_ux.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:43a960dd96fff4c8f16e0bacf908f3d6ae0750dcc355549a2129409d0a7b7425
|
| 3 |
+
size 1803192
|
videos/2026-06-15-dreamqa-v2/ux02_timeline.jpg
ADDED
|
Git LFS Details
|
videos/2026-06-15-dreamqa-v2/ux03_evidence.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"id": "ux03",
|
| 3 |
+
"title": "Floating kitchen table",
|
| 4 |
+
"url": "http://127.0.0.1:7862",
|
| 5 |
+
"imagePath": "/Users/junhaocheng/working-dir/ai-competitions/build-small-hackthon/videos/2026-06-15-dreamqa-full-ux/assets/ux03_floating_table_keys_sketch.png",
|
| 6 |
+
"audioPath": "/Users/junhaocheng/working-dir/ai-competitions/build-small-hackthon/videos/2026-06-15-dreamqa-full-ux/assets/ux03_voice.wav",
|
| 7 |
+
"startedAt": "2026-06-15T04:41:12.340Z",
|
| 8 |
+
"steps": [
|
| 9 |
+
"opened_local_app",
|
| 10 |
+
"filled_text_dream",
|
| 11 |
+
"uploaded_image_sketch",
|
| 12 |
+
"uploaded_voice_wav",
|
| 13 |
+
"submitted_intake",
|
| 14 |
+
"received_followup_question",
|
| 15 |
+
"answered_followup",
|
| 16 |
+
"received_morning_ticket"
|
| 17 |
+
],
|
| 18 |
+
"status": "passed",
|
| 19 |
+
"voiceStatusAfterInput": "Voice was not transcribed. Type that fragment or try again.",
|
| 20 |
+
"questionText": "\n Step 3 of 4 · Answer or skip\n One useful question\n Answer in one or two lines, or skip and let the existing anchors carry the ticket.\n \n Text · notedMood · notedImage · extractedVoice · not extracted\n\n Sticky details already on the deskfloating tableloose keysstar shapes by a sunrise line\n Morning Question DeskIn the floating table, loose keys, and sunrise line, which detail feels most like the small key you want to try today?\n Why this questionWhat I hear first is that the strongest trail begins with floating table, loose keys, and star shapes by a sunrise line, so I will connect it to what you may need today. I will unpack this in three layers: the feeling after waking, the concrete image (floating table, loose keys, and star shapes by a sunrise line), and what it may be asking you to care for today. One question that will shape the final tip\n This step makes the tip more specific. It is not diagnosis.\n",
|
| 21 |
+
"ticketText": "\n \n \n Morning Ticket\n Not a diagnosis, not a prophecy. Just one small note for today, grounded in the dream.\n \n \n Take one note today\n Grounded in the dream\n \n \n floating tableloose keysstar shapes by a sunrise line\n \n Because your dream kept returning to:floating table loose keys star shapes by a sunrise line\n \n Today Tip\n Connect floating table to the waking-life clue you named: \"I think the question is which small key to try first before the day gets busy\". Use it as a morning key check: choose the one small key you named, try that before the day gets busy, and leave the other keys on the table for later.\n \n Tiny 5-minute actionChoose one desk object as a stand-in for floating table, loose keys, and star shapes by a sunrise line, move it one centimeter, then name the smallest waking-life action it points to.\n \n Supporting reflection\n Maybe the floating table is not asking for a fixed dream meaning. It is pointing back to the waking-life clue you named: \"I think the question is which small key to try first before the day gets busy\".\n \n \n \n How this was made small\n MiniCPM-V reads sketches, notes, and image clues; MiniCPM5-1B writes the follow-up question, reflection, and ticket.\n \n Question recordWhat I hear first is that the strongest trail begins with floating table, loose keys, and star shapes by a sunrise line, so I will connect it to what you may need today. I will unpack this in three layers: the feeling after waking, the concrete image (floating table, loose keys, and star shapes by a sunrise line), and what it may be asking you to care for today. One question that will shape the final tip: In the floating table, loose keys, and sunrise line, which detail feels most like the small key you want to try todayI think the question is which small key to try first before the day gets busy\n A gentle reflection for today, not a diagnosis or prophecy.\n Care noteYou do not have to solve the whole dream this morning; start by noticing one detail and one small next step.\n",
|
| 22 |
+
"evidenceStatus": "\n Text · notedMood · notedImage · extractedVoice · not extracted\n",
|
| 23 |
+
"processingRoutes": "\n Text cluesImage cluesVoice transcript\n ",
|
| 24 |
+
"assertions": {
|
| 25 |
+
"requiredTermsPresent": true,
|
| 26 |
+
"bannedTermsAbsent": true,
|
| 27 |
+
"noBrokenVisualPhrase": true,
|
| 28 |
+
"evidenceStatusVisible": true,
|
| 29 |
+
"processingRoutesVisible": true,
|
| 30 |
+
"todayTipUsesAnswerClue": true,
|
| 31 |
+
"todayTipUsesDemoSpecificCue": true,
|
| 32 |
+
"questionUsesDemoSpecificCue": true,
|
| 33 |
+
"voiceFallbackHonest": true,
|
| 34 |
+
"noTemplateTipPhrase": true,
|
| 35 |
+
"noSafetyBoundaryLeak": true
|
| 36 |
+
},
|
| 37 |
+
"endedAt": "2026-06-15T04:41:34.960Z",
|
| 38 |
+
"videoWebm": "/Users/junhaocheng/working-dir/ai-competitions/build-small-hackthon/videos/2026-06-15-dreamqa-full-ux/ux03_full_ux.webm",
|
| 39 |
+
"videoPath": "videos/2026-06-15-dreamqa-v2/ux03_full_ux.mp4",
|
| 40 |
+
"duration": 23.36,
|
| 41 |
+
"sizeBytes": 1782024,
|
| 42 |
+
"questionVisibleForSeconds": 3.6,
|
| 43 |
+
"finalTicketVisibleForSeconds": 4.3,
|
| 44 |
+
"voiceEvidenceStatus": "fallback: Voice was not transcribed. Type that fragment or try again.",
|
| 45 |
+
"finalAnchors": [
|
| 46 |
+
"floating table",
|
| 47 |
+
"loose keys",
|
| 48 |
+
"star shapes by a sunrise line"
|
| 49 |
+
],
|
| 50 |
+
"todayTipText": "Connect floating table to the waking-life clue you named: \"I think the question is which small key to try first before the day gets busy\". Use it as a morning key check: choose the one small key you named, try that before the day gets busy, and leave the other keys on the table for later.",
|
| 51 |
+
"requiredAnswerClues": [
|
| 52 |
+
"small key",
|
| 53 |
+
"before the day gets busy"
|
| 54 |
+
],
|
| 55 |
+
"requiredTipClues": [
|
| 56 |
+
"morning key check",
|
| 57 |
+
"leave the other keys on the table"
|
| 58 |
+
],
|
| 59 |
+
"requiredQuestionClues": [
|
| 60 |
+
"floating table",
|
| 61 |
+
"loose keys",
|
| 62 |
+
"sunrise"
|
| 63 |
+
]
|
| 64 |
+
}
|
videos/2026-06-15-dreamqa-v2/ux03_final_ticket.png
ADDED
|
Git LFS Details
|
videos/2026-06-15-dreamqa-v2/ux03_full_ux.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b520601a44983a82652cf99e37294be8bb3e6b9fd74a8da1c4694989a72ac590
|
| 3 |
+
size 1782024
|
videos/2026-06-15-dreamqa-v2/ux03_timeline.jpg
ADDED
|
Git LFS Details
|