Deploy Dream QA post-refactor polish
#17
by ADJCJH - opened
- PRODUCT.md +7 -3
- README.md +13 -1
- docs/handoff.md +18 -3
- docs/prd.md +29 -1
- docs/smoke/2026-06-08-post-refactor-polish-smoke.md +137 -0
- docs/spec.md +21 -3
- docs/superpowers/plans/2026-06-08-post-refactor-polish.md +254 -0
- dream_customs/app_logic.py +19 -1
- dream_customs/models.py +31 -0
- dream_customs/pipeline.py +194 -74
- dream_customs/prompts.py +23 -6
- dream_customs/render.py +29 -12
- dream_customs/ui/actions.py +82 -31
- dream_customs/ui/app.py +223 -113
- dream_customs/ui/copy.py +146 -12
- scripts/evaluate_today_tip_quality.py +93 -0
- tests/fixtures/today_tip_eval_cases.json +73 -0
- tests/test_app_logic.py +3 -3
- tests/test_dream_qa_refactor.py +32 -8
- tests/test_pipeline.py +4 -1
- tests/test_today_tip_quality_eval.py +21 -0
- tests/test_ui_actions.py +25 -4
PRODUCT.md
CHANGED
|
@@ -6,11 +6,13 @@ product
|
|
| 6 |
|
| 7 |
## Users
|
| 8 |
|
| 9 |
-
Dream QA / 梦境问答台 serves people who wake up with vivid, strange, or unsettling dreams and want a small morning conversation before the day takes over. The primary user may still be half asleep, on a phone, and emotionally sticky from the dream. The secondary user is
|
| 10 |
|
| 11 |
## Product Purpose
|
| 12 |
|
| 13 |
-
The product helps a user unpack a dream through a guided, step-by-step Q&A. It accepts text, image, and voice fragments, turns them into one structured dream intake, asks gentle follow-up questions, drafts a grounded interpretation, and ends with one
|
|
|
|
|
|
|
| 14 |
|
| 15 |
Success means the user receives something more useful than a one-shot interpretation: a short summary of the dream, one clear question they were trying to answer, a non-certain explanation grounded in concrete dream details, and a tiny suggestion for today. The tip can be practical, playful, or simply caring, but it must feel connected to the dream.
|
| 16 |
|
|
@@ -38,7 +40,9 @@ The voice is a calm dream companion who asks good questions and takes odd detail
|
|
| 38 |
4. Every final output should lead to one small next-day reference or action.
|
| 39 |
5. The final tip must cite a concrete dream anchor.
|
| 40 |
6. The demo path must survive Space constraints and still have a model-backed route.
|
|
|
|
|
|
|
| 41 |
|
| 42 |
## Accessibility & Inclusion
|
| 43 |
|
| 44 |
-
Target WCAG AA contrast for body text, controls, and placeholder text. Support mobile-first use, keyboard navigation, visible focus states, reduced motion, and
|
|
|
|
| 6 |
|
| 7 |
## Users
|
| 8 |
|
| 9 |
+
Dream QA / 梦境问答台 serves people who wake up with vivid, strange, or unsettling dreams and want a small morning conversation before the day takes over. The primary user may still be half asleep, on a phone, and emotionally sticky from the dream. The secondary user is an international hackathon judge or playful AI explorer who needs to understand the loop quickly and see why small multimodal models matter.
|
| 10 |
|
| 11 |
## Product Purpose
|
| 12 |
|
| 13 |
+
The product helps a user unpack a dream through a guided, step-by-step Q&A. It accepts text, image, and voice fragments, turns them into one structured dream intake, asks gentle follow-up questions, drafts a grounded interpretation, and ends with one Today Tip.
|
| 14 |
+
|
| 15 |
+
The hackathon-facing experience is English-first. A visible `English / 中文` language control keeps the Chinese experience available without making judges start from a Chinese-only first screen.
|
| 16 |
|
| 17 |
Success means the user receives something more useful than a one-shot interpretation: a short summary of the dream, one clear question they were trying to answer, a non-certain explanation grounded in concrete dream details, and a tiny suggestion for today. The tip can be practical, playful, or simply caring, but it must feel connected to the dream.
|
| 18 |
|
|
|
|
| 40 |
4. Every final output should lead to one small next-day reference or action.
|
| 41 |
5. The final tip must cite a concrete dream anchor.
|
| 42 |
6. The demo path must survive Space constraints and still have a model-backed route.
|
| 43 |
+
7. English mode is the default public path; Chinese mode is a first-class toggle, not a separate product.
|
| 44 |
+
8. Recommendation quality is tested with a deterministic Today Tip eval set before deployment.
|
| 45 |
|
| 46 |
## Accessibility & Inclusion
|
| 47 |
|
| 48 |
+
Target WCAG AA contrast for body text, controls, and placeholder text. Support mobile-first use, keyboard navigation, visible focus states, reduced motion, and English/Chinese text without layout breakage. Avoid flashing effects, fear-based copy, or overconfident claims about dream meaning. Severe distress, self-harm, harm to others, or inability to function must show a clear support note.
|
README.md
CHANGED
|
@@ -26,9 +26,11 @@ A Build Small Hackathon Gradio app that helps users unpack a dream step by step
|
|
| 26 |
|
| 27 |
The Hugging Face Space may still be named `Dream Customs` for continuity, but the current product direction is Dream QA: record a dream, answer or skip gentle follow-up questions, read a grounded interpretation draft, and receive a `今日小 Tips`.
|
| 28 |
|
|
|
|
|
|
|
| 29 |
## Concept
|
| 30 |
|
| 31 |
-
Dream QA accepts dream fragments by text, image, or voice. It turns those fragments into a shared dream intake, asks a
|
| 32 |
|
| 33 |
This is not a therapy, diagnosis, or prophecy product.
|
| 34 |
|
|
@@ -39,6 +41,7 @@ This is not a therapy, diagnosis, or prophecy product.
|
|
| 39 |
- A small ASR adapter may be used only for voice transcription.
|
| 40 |
- The app defaults to a stable demo backend so the local Gradio flow always works.
|
| 41 |
- Optional Ollama adapters are included for local MiniCPM testing.
|
|
|
|
| 42 |
|
| 43 |
## User Flow
|
| 44 |
|
|
@@ -48,6 +51,13 @@ This is not a therapy, diagnosis, or prophecy product.
|
|
| 48 |
4. Review a grounded interpretation.
|
| 49 |
5. Receive one Today Tip tied to concrete dream details.
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
## Current Direction References
|
| 52 |
|
| 53 |
- Product spec: `docs/spec.md`
|
|
@@ -135,6 +145,7 @@ PY
|
|
| 135 |
|
| 136 |
```bash
|
| 137 |
python -m pytest -q
|
|
|
|
| 138 |
```
|
| 139 |
|
| 140 |
## Deployment Smoke Status
|
|
@@ -142,6 +153,7 @@ python -m pytest -q
|
|
| 142 |
Latest local Dream QA refactor smoke:
|
| 143 |
|
| 144 |
- `docs/smoke/2026-06-08-dream-qa-refactor-smoke.md`
|
|
|
|
| 145 |
|
| 146 |
Historical smoke notes under `docs/smoke/` describe earlier pact/customs UI passes. They remain useful deployment history, but the current implementation target is the Dream QA flow documented in `docs/handoff.md`, `docs/spec.md`, and `docs/prd.md`.
|
| 147 |
|
|
|
|
| 26 |
|
| 27 |
The Hugging Face Space may still be named `Dream Customs` for continuity, but the current product direction is Dream QA: record a dream, answer or skip gentle follow-up questions, read a grounded interpretation draft, and receive a `今日小 Tips`.
|
| 28 |
|
| 29 |
+
The public hackathon demo is English-first for international judges. A visible in-app language toggle keeps the Chinese experience available as `中文`.
|
| 30 |
+
|
| 31 |
## Concept
|
| 32 |
|
| 33 |
+
Dream QA accepts dream fragments by text, image, or voice. It turns those fragments into a shared dream intake, asks a grounded follow-up question, and returns a Today Tip card: a short non-certain interpretation, one practical or caring suggestion for today, and an optional tiny action.
|
| 34 |
|
| 35 |
This is not a therapy, diagnosis, or prophecy product.
|
| 36 |
|
|
|
|
| 41 |
- A small ASR adapter may be used only for voice transcription.
|
| 42 |
- The app defaults to a stable demo backend so the local Gradio flow always works.
|
| 43 |
- Optional Ollama adapters are included for local MiniCPM testing.
|
| 44 |
+
- MiniCPM prompts are language-aware: English by default, Chinese when the user chooses `中文`.
|
| 45 |
|
| 46 |
## User Flow
|
| 47 |
|
|
|
|
| 51 |
4. Review a grounded interpretation.
|
| 52 |
5. Receive one Today Tip tied to concrete dream details.
|
| 53 |
|
| 54 |
+
## Language
|
| 55 |
+
|
| 56 |
+
- Default public UI: English.
|
| 57 |
+
- Toggle: `English / 中文`.
|
| 58 |
+
- English mode may preserve user-provided dream anchors as written, but all UI labels, guidance, and generated helper text should be English.
|
| 59 |
+
- Chinese mode keeps the warm `梦境问答台` wording.
|
| 60 |
+
|
| 61 |
## Current Direction References
|
| 62 |
|
| 63 |
- Product spec: `docs/spec.md`
|
|
|
|
| 145 |
|
| 146 |
```bash
|
| 147 |
python -m pytest -q
|
| 148 |
+
python scripts/evaluate_today_tip_quality.py
|
| 149 |
```
|
| 150 |
|
| 151 |
## Deployment Smoke Status
|
|
|
|
| 153 |
Latest local Dream QA refactor smoke:
|
| 154 |
|
| 155 |
- `docs/smoke/2026-06-08-dream-qa-refactor-smoke.md`
|
| 156 |
+
- `docs/smoke/2026-06-08-post-refactor-polish-smoke.md`
|
| 157 |
|
| 158 |
Historical smoke notes under `docs/smoke/` describe earlier pact/customs UI passes. They remain useful deployment history, but the current implementation target is the Dream QA flow documented in `docs/handoff.md`, `docs/spec.md`, and `docs/prd.md`.
|
| 159 |
|
docs/handoff.md
CHANGED
|
@@ -10,6 +10,8 @@ The repo and Space still use the `Dream Customs` name for continuity, but future
|
|
| 10 |
|
| 11 |
> 输入梦境,回答或跳过几个温和追问,最后得到一个和梦境细节相关的今日小 Tips。
|
| 12 |
|
|
|
|
|
|
|
| 13 |
## Deprecated Context
|
| 14 |
|
| 15 |
Earlier implementation and smoke docs mention:
|
|
@@ -47,7 +49,7 @@ Build a guided dream interpretation app, not a diagnosis app, not a fortune-tell
|
|
| 47 |
|
| 48 |
Final positioning:
|
| 49 |
|
| 50 |
-
>
|
| 51 |
|
| 52 |
## Model Decision
|
| 53 |
|
|
@@ -82,6 +84,13 @@ Voice input is allowed, but current MiniCPM pair does not directly transcribe ra
|
|
| 82 |
9. MiniCPM5-1B generates a final `TodayTipCard`.
|
| 83 |
10. Gradio renders a mobile-readable result card and plain text.
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
## Required Output Fields
|
| 86 |
|
| 87 |
- `dream_summary`
|
|
@@ -142,6 +151,7 @@ Likely code files to change:
|
|
| 142 |
5. Render one clear final tip, not a multi-field customs certificate.
|
| 143 |
6. Keep mobile readability.
|
| 144 |
7. Run tests and local Gradio smoke before any Space sync.
|
|
|
|
| 145 |
|
| 146 |
## Repository Status
|
| 147 |
|
|
@@ -168,10 +178,15 @@ The Space defaults to `model` for both text and vision backends, so a configured
|
|
| 168 |
|
| 169 |
The public Space can run on ZeroGPU after `dream_customs.zerogpu` registers the lightweight `@spaces.GPU` startup probe. Modal credits still pay for the hidden GPU backend; the ZeroGPU probe is only there to make the HF hardware setting valid for this Gradio frontend.
|
| 170 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
## Open Questions For User
|
| 172 |
|
| 173 |
These do not block document alignment:
|
| 174 |
|
| 175 |
-
- Should the public name remain `Dream Customs`, or should visible UI switch fully to `梦境问答台 / Dream QA`?
|
| 176 |
-
- Should final demo language be Chinese-first, English-first, or bilingual?
|
| 177 |
- Which ASR adapter is acceptable for the hackathon submission?
|
|
|
|
|
|
| 10 |
|
| 11 |
> 输入梦境,回答或跳过几个温和追问,最后得到一个和梦境细节相关的今日小 Tips。
|
| 12 |
|
| 13 |
+
The public hackathon experience is English-first. Keep the visible `English / 中文` toggle so Chinese remains a first-class path, but do not make judges land on a Chinese-only first screen.
|
| 14 |
+
|
| 15 |
## Deprecated Context
|
| 16 |
|
| 17 |
Earlier implementation and smoke docs mention:
|
|
|
|
| 49 |
|
| 50 |
Final positioning:
|
| 51 |
|
| 52 |
+
> Dream QA helps you record a dream, answer or skip one gentle question, and leave with one grounded Today Tip.
|
| 53 |
|
| 54 |
## Model Decision
|
| 55 |
|
|
|
|
| 84 |
9. MiniCPM5-1B generates a final `TodayTipCard`.
|
| 85 |
10. Gradio renders a mobile-readable result card and plain text.
|
| 86 |
|
| 87 |
+
Language behavior:
|
| 88 |
+
|
| 89 |
+
- Default: English.
|
| 90 |
+
- Toggle: `中文`.
|
| 91 |
+
- The same intake/state/card pipeline serves both languages.
|
| 92 |
+
- MiniCPM prompts should instruct the selected output language instead of duplicating the product flow.
|
| 93 |
+
|
| 94 |
## Required Output Fields
|
| 95 |
|
| 96 |
- `dream_summary`
|
|
|
|
| 151 |
5. Render one clear final tip, not a multi-field customs certificate.
|
| 152 |
6. Keep mobile readability.
|
| 153 |
7. Run tests and local Gradio smoke before any Space sync.
|
| 154 |
+
8. Run `python scripts/evaluate_today_tip_quality.py` before deployment.
|
| 155 |
|
| 156 |
## Repository Status
|
| 157 |
|
|
|
|
| 178 |
|
| 179 |
The public Space can run on ZeroGPU after `dream_customs.zerogpu` registers the lightweight `@spaces.GPU` startup probe. Modal credits still pay for the hidden GPU backend; the ZeroGPU probe is only there to make the HF hardware setting valid for this Gradio frontend.
|
| 180 |
|
| 181 |
+
## Resolved Decisions
|
| 182 |
+
|
| 183 |
+
- Public UI name: `Dream QA`, with `Dream Customs` retained as project/Space continuity.
|
| 184 |
+
- Language: English-first for the international hackathon, with Chinese available through an in-app toggle.
|
| 185 |
+
- Recommendation quality: deterministic Today Tip eval is required before deployment.
|
| 186 |
+
|
| 187 |
## Open Questions For User
|
| 188 |
|
| 189 |
These do not block document alignment:
|
| 190 |
|
|
|
|
|
|
|
| 191 |
- Which ASR adapter is acceptable for the hackathon submission?
|
| 192 |
+
- Should `refs/pr/*` cleanup be done after HF Space merges, or left for auditability?
|
docs/prd.md
CHANGED
|
@@ -10,10 +10,12 @@ Users who wake up from vivid dreams may feel unsettled, curious, or mentally clu
|
|
| 10 |
|
| 11 |
### Proposed Solution
|
| 12 |
|
| 13 |
-
Dream QA is a Gradio app that accepts voice, image, and text dream fragments, normalizes them into a structured dream intake, asks gentle follow-up questions, drafts a grounded interpretation, and ends with one
|
| 14 |
|
| 15 |
The repository and Space may continue to use `Dream Customs` as the deployed project name, but the product experience should no longer be built around permits, contraband, or sealed pact cards.
|
| 16 |
|
|
|
|
|
|
|
| 17 |
### Success Criteria
|
| 18 |
|
| 19 |
- A user can complete the core text-only Q&A flow in <= 90 seconds on the hosted demo.
|
|
@@ -22,6 +24,8 @@ The repository and Space may continue to use `Dream Customs` as the deployed pro
|
|
| 22 |
- Final output gives exactly one primary today tip.
|
| 23 |
- Ordinary cases contain no diagnostic, prophetic, or frightening language.
|
| 24 |
- Distress-edge cases show a support note.
|
|
|
|
|
|
|
| 25 |
- The app runs with total model parameters <= 32B and is packaged for Gradio/Hugging Face Space.
|
| 26 |
|
| 27 |
## 2. User Experience & Functionality
|
|
@@ -39,6 +43,7 @@ The repository and Space may continue to use `Dream Customs` as the deployed pro
|
|
| 39 |
3. Demo viewer / judge.
|
| 40 |
- Needs to understand the interaction in under one minute.
|
| 41 |
- Looks for clear small-model fit and Gradio polish.
|
|
|
|
| 42 |
|
| 43 |
### User Stories
|
| 44 |
|
|
@@ -98,6 +103,18 @@ Acceptance criteria:
|
|
| 98 |
- Result avoids medical, prophetic, or deterministic framing.
|
| 99 |
- Today tip references a concrete dream anchor.
|
| 100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
### Non-Goals
|
| 102 |
|
| 103 |
- Medical diagnosis or therapy.
|
|
@@ -129,6 +146,7 @@ Prompts must:
|
|
| 129 |
- Include a safety boundary for severe distress.
|
| 130 |
- Keep final advice small enough to execute today.
|
| 131 |
- Require at least one dream anchor in the final tip.
|
|
|
|
| 132 |
|
| 133 |
### Evaluation Strategy
|
| 134 |
|
|
@@ -147,6 +165,8 @@ Measure:
|
|
| 147 |
- Dream-anchor grounding.
|
| 148 |
- Practicality of today tip.
|
| 149 |
- Ability to recover from missing modality.
|
|
|
|
|
|
|
| 150 |
|
| 151 |
Pass thresholds:
|
| 152 |
|
|
@@ -156,6 +176,14 @@ Pass thresholds:
|
|
| 156 |
- At least one concrete dream anchor in final result: >= 90%.
|
| 157 |
- Human usefulness/delight rating on 5 demo cases: average >= 4/5.
|
| 158 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
## 4. Technical Specifications
|
| 160 |
|
| 161 |
### Architecture Overview
|
|
|
|
| 10 |
|
| 11 |
### Proposed Solution
|
| 12 |
|
| 13 |
+
Dream QA is a Gradio app that accepts voice, image, and text dream fragments, normalizes them into a structured dream intake, asks gentle follow-up questions, drafts a grounded interpretation, and ends with one Today Tip.
|
| 14 |
|
| 15 |
The repository and Space may continue to use `Dream Customs` as the deployed project name, but the product experience should no longer be built around permits, contraband, or sealed pact cards.
|
| 16 |
|
| 17 |
+
The public demo defaults to English for the international hackathon audience. A visible language toggle preserves the Chinese experience.
|
| 18 |
+
|
| 19 |
### Success Criteria
|
| 20 |
|
| 21 |
- A user can complete the core text-only Q&A flow in <= 90 seconds on the hosted demo.
|
|
|
|
| 24 |
- Final output gives exactly one primary today tip.
|
| 25 |
- Ordinary cases contain no diagnostic, prophetic, or frightening language.
|
| 26 |
- Distress-edge cases show a support note.
|
| 27 |
+
- English is the default public language; Chinese remains available through one visible toggle.
|
| 28 |
+
- `python scripts/evaluate_today_tip_quality.py` passes before deployment.
|
| 29 |
- The app runs with total model parameters <= 32B and is packaged for Gradio/Hugging Face Space.
|
| 30 |
|
| 31 |
## 2. User Experience & Functionality
|
|
|
|
| 43 |
3. Demo viewer / judge.
|
| 44 |
- Needs to understand the interaction in under one minute.
|
| 45 |
- Looks for clear small-model fit and Gradio polish.
|
| 46 |
+
- Should land on an English first screen without needing translation.
|
| 47 |
|
| 48 |
### User Stories
|
| 49 |
|
|
|
|
| 103 |
- Result avoids medical, prophetic, or deterministic framing.
|
| 104 |
- Today tip references a concrete dream anchor.
|
| 105 |
|
| 106 |
+
#### Story 6: Bilingual Demo
|
| 107 |
+
|
| 108 |
+
As an international judge, I want the app to open in English, while Chinese users can still switch to 中文.
|
| 109 |
+
|
| 110 |
+
Acceptance criteria:
|
| 111 |
+
|
| 112 |
+
- Default UI title is `Dream QA`.
|
| 113 |
+
- A visible `English / 中文` control exists.
|
| 114 |
+
- English mode renders English labels and card headings.
|
| 115 |
+
- Chinese mode renders `梦境问答台` and Chinese Today Tip labels.
|
| 116 |
+
- Both modes use the same model/pipeline route.
|
| 117 |
+
|
| 118 |
### Non-Goals
|
| 119 |
|
| 120 |
- Medical diagnosis or therapy.
|
|
|
|
| 146 |
- Include a safety boundary for severe distress.
|
| 147 |
- Keep final advice small enough to execute today.
|
| 148 |
- Require at least one dream anchor in the final tip.
|
| 149 |
+
- Follow selected language: English by default, Chinese when selected.
|
| 150 |
|
| 151 |
### Evaluation Strategy
|
| 152 |
|
|
|
|
| 165 |
- Dream-anchor grounding.
|
| 166 |
- Practicality of today tip.
|
| 167 |
- Ability to recover from missing modality.
|
| 168 |
+
- Old customs words do not appear in the Dream QA result: permit, contraband, clearance, sealed, pact.
|
| 169 |
+
- English default output does not contain Chinese UI labels.
|
| 170 |
|
| 171 |
Pass thresholds:
|
| 172 |
|
|
|
|
| 176 |
- At least one concrete dream anchor in final result: >= 90%.
|
| 177 |
- Human usefulness/delight rating on 5 demo cases: average >= 4/5.
|
| 178 |
|
| 179 |
+
Current deterministic gate:
|
| 180 |
+
|
| 181 |
+
```bash
|
| 182 |
+
python scripts/evaluate_today_tip_quality.py
|
| 183 |
+
```
|
| 184 |
+
|
| 185 |
+
This gate covers 10 representative cases and should be expanded toward the 12-case target as image/voice remote smoke matures.
|
| 186 |
+
|
| 187 |
## 4. Technical Specifications
|
| 188 |
|
| 189 |
### Architecture Overview
|
docs/smoke/2026-06-08-post-refactor-polish-smoke.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dream QA Post-Refactor Polish Smoke - 2026-06-08
|
| 2 |
+
|
| 3 |
+
## Scope
|
| 4 |
+
|
| 5 |
+
Follow-up polish after the Dream QA refactor:
|
| 6 |
+
|
| 7 |
+
- English-first public hackathon experience.
|
| 8 |
+
- Visible `English / 中文` language toggle.
|
| 9 |
+
- Chinese mode remains available.
|
| 10 |
+
- Deterministic Today Tip quality gate.
|
| 11 |
+
- Follow-up questions and Today Tips remain grounded in dream anchors.
|
| 12 |
+
|
| 13 |
+
## Local Runtime
|
| 14 |
+
|
| 15 |
+
Command:
|
| 16 |
+
|
| 17 |
+
```bash
|
| 18 |
+
GRADIO_SERVER_PORT=7862 .venv/bin/python app.py
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
Observed:
|
| 22 |
+
|
| 23 |
+
```text
|
| 24 |
+
Running on local URL: http://0.0.0.0:7862
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
## Local Config Check
|
| 28 |
+
|
| 29 |
+
Command:
|
| 30 |
+
|
| 31 |
+
```bash
|
| 32 |
+
.venv/bin/python - <<'PY'
|
| 33 |
+
import json
|
| 34 |
+
from urllib.request import urlopen
|
| 35 |
+
with urlopen("http://127.0.0.1:7862/config", timeout=30) as response:
|
| 36 |
+
cfg = json.load(response)
|
| 37 |
+
print({"title": cfg.get("title"), "version": cfg.get("version"), "mode": cfg.get("mode"), "component_count": len(cfg.get("components", []))})
|
| 38 |
+
PY
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
Observed config included:
|
| 42 |
+
|
| 43 |
+
```text
|
| 44 |
+
title=Dream QA
|
| 45 |
+
version=4.44.1
|
| 46 |
+
mode=blocks
|
| 47 |
+
component_count=72
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
The config also showed English default UI copy including `Dream QA`, `Record`, `Question`, `Interpret`, `Today Tip`, `Dream note`, `Try example`, `Continue`, `Language`, and choices `English` / `中文`.
|
| 51 |
+
|
| 52 |
+
## Automated Checks
|
| 53 |
+
|
| 54 |
+
Command:
|
| 55 |
+
|
| 56 |
+
```bash
|
| 57 |
+
.venv/bin/python -m pytest -q
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
Observed:
|
| 61 |
+
|
| 62 |
+
```text
|
| 63 |
+
94 passed, 2 warnings
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
Command:
|
| 67 |
+
|
| 68 |
+
```bash
|
| 69 |
+
.venv/bin/python scripts/evaluate_today_tip_quality.py
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
Observed:
|
| 73 |
+
|
| 74 |
+
```json
|
| 75 |
+
{
|
| 76 |
+
"case_count": 10,
|
| 77 |
+
"failures": {},
|
| 78 |
+
"passes": true
|
| 79 |
+
}
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
Command:
|
| 83 |
+
|
| 84 |
+
```bash
|
| 85 |
+
git diff --check
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
Observed: no output.
|
| 89 |
+
|
| 90 |
+
## Action Smoke
|
| 91 |
+
|
| 92 |
+
English default input:
|
| 93 |
+
|
| 94 |
+
```text
|
| 95 |
+
I dreamed I was in an old apartment building. The elevator button melted like wax, and the floor number stayed on 14. I woke up anxious.
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
Observed action payload:
|
| 99 |
+
|
| 100 |
+
```text
|
| 101 |
+
submit_status=ask
|
| 102 |
+
language=en
|
| 103 |
+
question=When you think about the elevator and the melted button, what real thing today feels hard to start?
|
| 104 |
+
tip_status=tip
|
| 105 |
+
title=Today Tip
|
| 106 |
+
has_elevator=True
|
| 107 |
+
has_old_words=False
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
Chinese toggle input:
|
| 111 |
+
|
| 112 |
+
```text
|
| 113 |
+
我梦到电梯按钮融化,楼层数字停在 14。
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
Observed action payload:
|
| 117 |
+
|
| 118 |
+
```text
|
| 119 |
+
zh_status=tip
|
| 120 |
+
zh_title=今日小 Tips
|
| 121 |
+
has_zh_anchor=True
|
| 122 |
+
```
|
| 123 |
+
|
| 124 |
+
## Browser Notes
|
| 125 |
+
|
| 126 |
+
- In-app Browser reached `http://127.0.0.1:7862/` and saw title `Dream QA`, but the DOM snapshot stayed at Gradio's `加载中...` placeholder during this run.
|
| 127 |
+
- Chrome extension control returned a closed native pipe while opening the same local URL.
|
| 128 |
+
- Because both browser automation surfaces were unstable, no visual screenshot claim is made here. The local Gradio `/config`, app build check, action flow, full pytest suite, and deterministic quality evaluator passed.
|
| 129 |
+
|
| 130 |
+
## Deployment Status
|
| 131 |
+
|
| 132 |
+
Not yet deployed in this smoke file. After commit and GitHub/HF sync, update this file or add a follow-up remote smoke section with:
|
| 133 |
+
|
| 134 |
+
- GitHub PR URL and merge commit.
|
| 135 |
+
- HF Space PR discussion URL and Space `main` SHA.
|
| 136 |
+
- Public Space `/config` result.
|
| 137 |
+
- Public app flow check.
|
docs/spec.md
CHANGED
|
@@ -4,7 +4,9 @@ Last updated: 2026-06-08
|
|
| 4 |
|
| 5 |
## 1. One-Line Concept
|
| 6 |
|
| 7 |
-
|
|
|
|
|
|
|
| 8 |
|
| 9 |
## 2. Contest Fit
|
| 10 |
|
|
@@ -29,6 +31,7 @@ The user wakes up after a vivid or unsettling dream and wants help making sense
|
|
| 29 |
5. Ground every final answer in concrete dream details.
|
| 30 |
6. End with one small today tip, not a pile of advice.
|
| 31 |
7. Keep the MVP stable enough for Gradio Space and demo-video recording.
|
|
|
|
| 32 |
|
| 33 |
## 5. User Inputs
|
| 34 |
|
|
@@ -61,6 +64,10 @@ The user speaks the dream immediately after waking.
|
|
| 61 |
|
| 62 |
Implementation note: MiniCPM-V-4.6 and MiniCPM5-1B are not raw audio ASR models. Voice input needs a small transcription adapter. The adapter only converts audio to text; dream understanding remains MiniCPM-driven.
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
## 6. Data Model
|
| 65 |
|
| 66 |
The refactor should move from `DreamIntake -> PactCard` toward:
|
|
@@ -124,7 +131,7 @@ Responsibilities:
|
|
| 124 |
|
| 125 |
- Read sketches, notes, screenshots, and photos.
|
| 126 |
- Extract objects, locations, visible text, colors, spatial relationships, and emotional cues.
|
| 127 |
-
- Return concise JSON-compatible visual
|
| 128 |
|
| 129 |
Non-responsibilities:
|
| 130 |
|
|
@@ -141,9 +148,11 @@ Responsibilities:
|
|
| 141 |
- Summarize the dream without overexplaining.
|
| 142 |
- Infer or ask the user's main question.
|
| 143 |
- Ask 1-3 gentle follow-up questions.
|
|
|
|
| 144 |
- Draft a non-certain interpretation grounded in dream anchors.
|
| 145 |
- Generate exactly one primary today tip, with optional tiny action and caring note.
|
| 146 |
- Keep tone playful, gentle, and non-clinical.
|
|
|
|
| 147 |
|
| 148 |
## 8. MVP User Flow
|
| 149 |
|
|
@@ -177,6 +186,7 @@ The final result must:
|
|
| 177 |
- Avoid diagnosis, prophecy, or frightening certainty.
|
| 178 |
- Avoid generic advice that could fit any dream.
|
| 179 |
- Keep the today tip small enough to try today.
|
|
|
|
| 180 |
|
| 181 |
## 10. UX Structure
|
| 182 |
|
|
@@ -192,7 +202,7 @@ Inputs:
|
|
| 192 |
|
| 193 |
Primary action:
|
| 194 |
|
| 195 |
-
- "
|
| 196 |
|
| 197 |
### Screen 2: Ask
|
| 198 |
|
|
@@ -218,6 +228,14 @@ Content:
|
|
| 218 |
- One primary today tip.
|
| 219 |
- Optional tiny action.
|
| 220 |
- Optional caring note.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
- Copy text button.
|
| 222 |
- Reset action.
|
| 223 |
|
|
|
|
| 4 |
|
| 5 |
## 1. One-Line Concept
|
| 6 |
|
| 7 |
+
Dream QA helps users unpack last night's dream step by step: they record a dream with text, image, or voice; the system asks one grounded follow-up question; then it gives a non-diagnostic, non-fatalistic interpretation draft and one gentle Today Tip.
|
| 8 |
+
|
| 9 |
+
The public hackathon demo is English-first because the event is international. Chinese remains available through the in-app `English / 中文` toggle.
|
| 10 |
|
| 11 |
## 2. Contest Fit
|
| 12 |
|
|
|
|
| 31 |
5. Ground every final answer in concrete dream details.
|
| 32 |
6. End with one small today tip, not a pile of advice.
|
| 33 |
7. Keep the MVP stable enough for Gradio Space and demo-video recording.
|
| 34 |
+
8. Default to English for public judging; keep Chinese as a first-class toggle.
|
| 35 |
|
| 36 |
## 5. User Inputs
|
| 37 |
|
|
|
|
| 64 |
|
| 65 |
Implementation note: MiniCPM-V-4.6 and MiniCPM5-1B are not raw audio ASR models. Voice input needs a small transcription adapter. The adapter only converts audio to text; dream understanding remains MiniCPM-driven.
|
| 66 |
|
| 67 |
+
### 5.4 Language Input
|
| 68 |
+
|
| 69 |
+
The app defaults to English. A visible language toggle allows `中文`. The same `DreamQuestionIntake -> DreamQAState -> TodayTipCard` pipeline is used for both languages; only prompt instructions, UI labels, and rendered output labels change.
|
| 70 |
+
|
| 71 |
## 6. Data Model
|
| 72 |
|
| 73 |
The refactor should move from `DreamIntake -> PactCard` toward:
|
|
|
|
| 131 |
|
| 132 |
- Read sketches, notes, screenshots, and photos.
|
| 133 |
- Extract objects, locations, visible text, colors, spatial relationships, and emotional cues.
|
| 134 |
+
- Return concise JSON-compatible visual witness reports.
|
| 135 |
|
| 136 |
Non-responsibilities:
|
| 137 |
|
|
|
|
| 148 |
- Summarize the dream without overexplaining.
|
| 149 |
- Infer or ask the user's main question.
|
| 150 |
- Ask 1-3 gentle follow-up questions.
|
| 151 |
+
- Prefer exactly one visible follow-up question in the public MVP, with another-angle actions available after the first result.
|
| 152 |
- Draft a non-certain interpretation grounded in dream anchors.
|
| 153 |
- Generate exactly one primary today tip, with optional tiny action and caring note.
|
| 154 |
- Keep tone playful, gentle, and non-clinical.
|
| 155 |
+
- Follow the selected language: English by default, Chinese when selected.
|
| 156 |
|
| 157 |
## 8. MVP User Flow
|
| 158 |
|
|
|
|
| 186 |
- Avoid diagnosis, prophecy, or frightening certainty.
|
| 187 |
- Avoid generic advice that could fit any dream.
|
| 188 |
- Keep the today tip small enough to try today.
|
| 189 |
+
- Use English UI/output labels by default and Chinese labels only when `中文` is selected.
|
| 190 |
|
| 191 |
## 10. UX Structure
|
| 192 |
|
|
|
|
| 202 |
|
| 203 |
Primary action:
|
| 204 |
|
| 205 |
+
- "Continue"
|
| 206 |
|
| 207 |
### Screen 2: Ask
|
| 208 |
|
|
|
|
| 228 |
- One primary today tip.
|
| 229 |
- Optional tiny action.
|
| 230 |
- Optional caring note.
|
| 231 |
+
|
| 232 |
+
### Screen 0: Language
|
| 233 |
+
|
| 234 |
+
Default: `English`.
|
| 235 |
+
|
| 236 |
+
Secondary option: `中文`.
|
| 237 |
+
|
| 238 |
+
The toggle should be visible but not distract from the dream composer.
|
| 239 |
- Copy text button.
|
| 240 |
- Reset action.
|
| 241 |
|
docs/superpowers/plans/2026-06-08-post-refactor-polish.md
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Post-Refactor Polish Implementation Plan
|
| 2 |
+
|
| 3 |
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
| 4 |
+
|
| 5 |
+
**Goal:** Finish the highest-value Dream QA follow-up after the refactor: align GitHub/HF state, verify the deployed flow, make English the default hackathon experience with a Chinese toggle, and improve recommendation/follow-up quality.
|
| 6 |
+
|
| 7 |
+
**Architecture:** Keep the current Dream QA state machine. Add a lightweight language layer around UI copy, prompt language, card rendering, and fake/model fallbacks rather than cloning the app into separate English and Chinese flows. Add quality gates as deterministic tests and a small evaluator script so product quality does not regress after deployment.
|
| 8 |
+
|
| 9 |
+
**Tech Stack:** Python 3.9, Gradio 4.44.1, Pydantic, MiniCPM5-1B text route, MiniCPM-V-4.6 vision witness route, Hugging Face Space, GitHub PR workflow.
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## Current Evidence
|
| 14 |
+
|
| 15 |
+
- Current branch: `feature/dream-customs-ui-voice-settings`.
|
| 16 |
+
- Current latest commit: `122266f feat: refactor dream qa flow`.
|
| 17 |
+
- GitHub `main` previously merged only through `ff796db`; this branch has post-merge commits that still need a GitHub PR/merge.
|
| 18 |
+
- HF Space PR #16 has already merged this branch to Space `main` at `19c54925bbb525405bc2540391a434ddaeba4139`.
|
| 19 |
+
- Public `/config` returns title `梦境问答台`, Gradio `4.44.1`, and 70 components.
|
| 20 |
+
- Local checks before this plan: `91 passed, 2 warnings`; `git diff --check` clean.
|
| 21 |
+
|
| 22 |
+
## Task 1: GitHub Main Alignment
|
| 23 |
+
|
| 24 |
+
**Files:**
|
| 25 |
+
- No source edits required.
|
| 26 |
+
- Record result in `docs/smoke/2026-06-08-post-refactor-polish-smoke.md`.
|
| 27 |
+
|
| 28 |
+
- [ ] **Step 1: Verify branch relationship**
|
| 29 |
+
|
| 30 |
+
Run:
|
| 31 |
+
|
| 32 |
+
```bash
|
| 33 |
+
git fetch origin
|
| 34 |
+
git log --oneline origin/main..HEAD
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
Expected: shows the post-merge Dream QA commits that are on `feature/dream-customs-ui-voice-settings` but not `origin/main`.
|
| 38 |
+
|
| 39 |
+
- [ ] **Step 2: Create or update GitHub PR**
|
| 40 |
+
|
| 41 |
+
Run:
|
| 42 |
+
|
| 43 |
+
```bash
|
| 44 |
+
gh pr list --head feature/dream-customs-ui-voice-settings --base main --json number,url,state,title
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
If no open PR exists, create one:
|
| 48 |
+
|
| 49 |
+
```bash
|
| 50 |
+
gh pr create \
|
| 51 |
+
--base main \
|
| 52 |
+
--head feature/dream-customs-ui-voice-settings \
|
| 53 |
+
--title "Dream QA post-refactor polish" \
|
| 54 |
+
--body-file /tmp/dream-qa-post-refactor-pr.md
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
- [ ] **Step 3: Merge GitHub PR**
|
| 58 |
+
|
| 59 |
+
Use the authenticated browser/CLI path available in the session. Verify with:
|
| 60 |
+
|
| 61 |
+
```bash
|
| 62 |
+
gh pr view <number> --json state,mergedAt,mergeCommit,url
|
| 63 |
+
```
|
| 64 |
+
|
| 65 |
+
Expected: `state` is `MERGED`.
|
| 66 |
+
|
| 67 |
+
## Task 2: Deployed Space Smoke
|
| 68 |
+
|
| 69 |
+
**Files:**
|
| 70 |
+
- Create or update: `docs/smoke/2026-06-08-post-refactor-polish-smoke.md`
|
| 71 |
+
- Test: no new unit test required for this task.
|
| 72 |
+
|
| 73 |
+
- [ ] **Step 1: Verify deployed config**
|
| 74 |
+
|
| 75 |
+
Run:
|
| 76 |
+
|
| 77 |
+
```bash
|
| 78 |
+
.venv/bin/python - <<'PY'
|
| 79 |
+
import json
|
| 80 |
+
from urllib.request import urlopen
|
| 81 |
+
with urlopen("https://build-small-hackathon-dream-customs.hf.space/config", timeout=30) as response:
|
| 82 |
+
cfg = json.load(response)
|
| 83 |
+
print({"title": cfg.get("title"), "version": cfg.get("version"), "mode": cfg.get("mode"), "component_count": len(cfg.get("components", []))})
|
| 84 |
+
PY
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
Expected: public app config responds with Dream QA metadata.
|
| 88 |
+
|
| 89 |
+
- [ ] **Step 2: Run a browser smoke on the public app**
|
| 90 |
+
|
| 91 |
+
Use Chrome or in-app browser to open:
|
| 92 |
+
|
| 93 |
+
```text
|
| 94 |
+
https://build-small-hackathon-dream-customs.hf.space/?v=<space-main-sha>
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
Submit an English dream, answer or skip one follow-up, and verify the result:
|
| 98 |
+
|
| 99 |
+
```text
|
| 100 |
+
I dreamed I was in an old apartment building. The elevator button melted like wax, and the floor number stayed on 14. I woke up anxious.
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
Expected: the app reaches a Today Tip card, references elevator/button/floor 14, and does not show old permit/contraband/seal language.
|
| 104 |
+
|
| 105 |
+
## Task 3: English-First Bilingual Experience
|
| 106 |
+
|
| 107 |
+
**Files:**
|
| 108 |
+
- Modify: `dream_customs/ui/copy.py`
|
| 109 |
+
- Modify: `dream_customs/ui/app.py`
|
| 110 |
+
- Modify: `dream_customs/ui/actions.py`
|
| 111 |
+
- Modify: `dream_customs/render.py`
|
| 112 |
+
- Modify: `dream_customs/prompts.py`
|
| 113 |
+
- Modify: `dream_customs/pipeline.py`
|
| 114 |
+
- Modify: `dream_customs/models.py`
|
| 115 |
+
- Test: `tests/test_dream_qa_refactor.py`
|
| 116 |
+
- Test: `tests/test_ui_actions.py`
|
| 117 |
+
- Test: `tests/test_render.py`
|
| 118 |
+
|
| 119 |
+
- [x] **Step 1: Add language copy contract**
|
| 120 |
+
|
| 121 |
+
Add a `LANGUAGE_OPTIONS`, `DEFAULT_LANGUAGE = "en"`, and copy helpers in `dream_customs/ui/copy.py`. English copy must be the default; Chinese copy must remain available through a visible control.
|
| 122 |
+
|
| 123 |
+
- [x] **Step 2: Thread language through actions**
|
| 124 |
+
|
| 125 |
+
Add `language` to `initial_mobile_state`, `submit_dream_action`, `answer_to_card_action`, `skip_to_card_action`, `revise_card_action`, `reset_mobile_action`, `_view_payload`, and `_seal_view`.
|
| 126 |
+
|
| 127 |
+
- [x] **Step 3: Localize prompts and fallbacks**
|
| 128 |
+
|
| 129 |
+
Pass `language` into `dream_qa_state_prompt`, `today_tip_prompt`, `generate_today_tip`, and fallback Today Tip helpers. English output must avoid Chinese labels by default; Chinese mode must keep the current gentle Chinese tone.
|
| 130 |
+
|
| 131 |
+
- [x] **Step 4: Add visible UI language toggle**
|
| 132 |
+
|
| 133 |
+
Add a top-level `gr.Radio` or segmented-style control with English selected by default and Chinese as the second option. The visible first screen should read as English for hackathon judges, while the Chinese option remains one click away.
|
| 134 |
+
|
| 135 |
+
- [x] **Step 5: Add tests**
|
| 136 |
+
|
| 137 |
+
Add tests proving:
|
| 138 |
+
|
| 139 |
+
```python
|
| 140 |
+
assert DEFAULT_LANGUAGE == "en"
|
| 141 |
+
assert "Dream QA" in APP_COPY["en"]["title"]
|
| 142 |
+
assert "梦境问答台" in APP_COPY["zh"]["title"]
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
Also test that a default `submit_dream_action(..., text_backend="demo") -> skip_to_card_action(...)` returns English labels and English card text, while `language="zh"` keeps Chinese output.
|
| 146 |
+
|
| 147 |
+
## Task 4: Today Tip Quality Gate
|
| 148 |
+
|
| 149 |
+
**Files:**
|
| 150 |
+
- Create: `tests/fixtures/today_tip_eval_cases.json`
|
| 151 |
+
- Create: `scripts/evaluate_today_tip_quality.py`
|
| 152 |
+
- Test: `tests/test_today_tip_quality_eval.py`
|
| 153 |
+
|
| 154 |
+
- [x] **Step 1: Add 10 representative cases**
|
| 155 |
+
|
| 156 |
+
Cover elevator, exam, train, phone full of water, missing shoes, locked room, rain note, abstract image clue, voice fragment, and severe distress.
|
| 157 |
+
|
| 158 |
+
- [x] **Step 2: Implement deterministic evaluator**
|
| 159 |
+
|
| 160 |
+
The evaluator must fail when:
|
| 161 |
+
|
| 162 |
+
- `today_tip` does not reference at least one required anchor.
|
| 163 |
+
- final text contains old customs words: `permit`, `contraband`, `clearance`, `sealed`, `pact`.
|
| 164 |
+
- ordinary cases include diagnosis, prophecy, or frightening certainty.
|
| 165 |
+
- English default output contains Chinese UI labels.
|
| 166 |
+
|
| 167 |
+
- [x] **Step 3: Wire test coverage**
|
| 168 |
+
|
| 169 |
+
Run:
|
| 170 |
+
|
| 171 |
+
```bash
|
| 172 |
+
.venv/bin/python scripts/evaluate_today_tip_quality.py
|
| 173 |
+
.venv/bin/python -m pytest tests/test_today_tip_quality_eval.py -q
|
| 174 |
+
```
|
| 175 |
+
|
| 176 |
+
Expected: evaluator prints zero failures and pytest passes.
|
| 177 |
+
|
| 178 |
+
## Task 5: MiniCPM Follow-Up Quality
|
| 179 |
+
|
| 180 |
+
**Files:**
|
| 181 |
+
- Modify: `dream_customs/prompts.py`
|
| 182 |
+
- Modify: `dream_customs/pipeline.py`
|
| 183 |
+
- Modify: `dream_customs/models.py`
|
| 184 |
+
- Test: `tests/test_pipeline.py`
|
| 185 |
+
- Test: `tests/test_dream_qa_refactor.py`
|
| 186 |
+
|
| 187 |
+
- [x] **Step 1: Strengthen follow-up prompt**
|
| 188 |
+
|
| 189 |
+
Update the question prompt to ask exactly one useful follow-up in the selected language, grounded in dream anchors and user mood.
|
| 190 |
+
|
| 191 |
+
- [x] **Step 2: Add fallback question quality guard**
|
| 192 |
+
|
| 193 |
+
Ensure fallback questions cite an anchor and avoid generic "how do you feel" unless tied to a detail.
|
| 194 |
+
|
| 195 |
+
- [x] **Step 3: Add tests**
|
| 196 |
+
|
| 197 |
+
Add tests proving an English elevator dream gets an English question mentioning elevator/button/floor 14, and a Chinese dream in `language="zh"` gets a Chinese question with the same anchors.
|
| 198 |
+
|
| 199 |
+
## Task 6: Documentation And Deployment
|
| 200 |
+
|
| 201 |
+
**Files:**
|
| 202 |
+
- Modify: `README.md`
|
| 203 |
+
- Modify: `PRODUCT.md`
|
| 204 |
+
- Modify: `docs/spec.md`
|
| 205 |
+
- Modify: `docs/prd.md`
|
| 206 |
+
- Modify: `docs/handoff.md`
|
| 207 |
+
- Modify: `docs/superpowers/plans/2026-06-08-post-refactor-polish.md`
|
| 208 |
+
- Create/update: `docs/smoke/2026-06-08-post-refactor-polish-smoke.md`
|
| 209 |
+
|
| 210 |
+
- [x] **Step 1: Update docs**
|
| 211 |
+
|
| 212 |
+
Docs must state:
|
| 213 |
+
|
| 214 |
+
- English is the default public hackathon experience.
|
| 215 |
+
- Chinese is available through an in-app language toggle.
|
| 216 |
+
- MiniCPM5 handles follow-up and Today Tip drafting.
|
| 217 |
+
- MiniCPM-V handles visual witness reports.
|
| 218 |
+
- The app remains non-diagnostic and not therapy.
|
| 219 |
+
|
| 220 |
+
- [x] **Step 2: Verify locally**
|
| 221 |
+
|
| 222 |
+
Run:
|
| 223 |
+
|
| 224 |
+
```bash
|
| 225 |
+
.venv/bin/python -m pytest -q
|
| 226 |
+
.venv/bin/python scripts/evaluate_today_tip_quality.py
|
| 227 |
+
git diff --check
|
| 228 |
+
```
|
| 229 |
+
|
| 230 |
+
- [ ] **Step 3: Commit and push**
|
| 231 |
+
|
| 232 |
+
Run:
|
| 233 |
+
|
| 234 |
+
```bash
|
| 235 |
+
git add <changed files>
|
| 236 |
+
git commit -m "feat: polish dream qa post-refactor flow"
|
| 237 |
+
git push origin feature/dream-customs-ui-voice-settings
|
| 238 |
+
```
|
| 239 |
+
|
| 240 |
+
- [ ] **Step 4: Sync HF Space**
|
| 241 |
+
|
| 242 |
+
Use `huggingface_hub.upload_folder(..., create_pr=True)` if direct push is not appropriate. Merge the Space PR in Chrome and verify:
|
| 243 |
+
|
| 244 |
+
```bash
|
| 245 |
+
git ls-remote space refs/heads/main
|
| 246 |
+
```
|
| 247 |
+
|
| 248 |
+
Expected: Space `main` advances to the deployment commit.
|
| 249 |
+
|
| 250 |
+
## Self-Review
|
| 251 |
+
|
| 252 |
+
- Spec coverage: covers GitHub main alignment, deployed smoke, English-first toggle, quality evaluation, MiniCPM follow-up quality, docs, tests, commit, push, and HF Space sync.
|
| 253 |
+
- Placeholder scan: no TBD/TODO placeholders remain.
|
| 254 |
+
- Type consistency: language values use `"en"` and `"zh"` across UI, actions, prompts, pipeline, tests, and docs.
|
dream_customs/app_logic.py
CHANGED
|
@@ -229,6 +229,24 @@ def _notice(session: CustomsSession) -> str:
|
|
| 229 |
return "<div class='dc-inline-notice'>File any fragment. Text-only stays available if image or voice fails.</div>"
|
| 230 |
|
| 231 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
def _view(
|
| 233 |
session: CustomsSession,
|
| 234 |
text_backend: str = DEFAULT_TEXT_BACKEND,
|
|
@@ -437,4 +455,4 @@ def run_customs_once(
|
|
| 437 |
"negotiation": negotiation,
|
| 438 |
"today_tip": card.model_dump(),
|
| 439 |
}
|
| 440 |
-
return negotiation_text,
|
|
|
|
| 229 |
return "<div class='dc-inline-notice'>File any fragment. Text-only stays available if image or voice fails.</div>"
|
| 230 |
|
| 231 |
|
| 232 |
+
def _today_tip_plain_text(card) -> str:
|
| 233 |
+
lines = [
|
| 234 |
+
"Today Tip",
|
| 235 |
+
f"Dream summary: {card.dream_summary}",
|
| 236 |
+
f"Question: {card.main_question}",
|
| 237 |
+
f"Dream anchors: {', '.join(card.dream_anchors)}",
|
| 238 |
+
f"Interpretation: {card.interpretation}",
|
| 239 |
+
f"Today Tip: {card.today_tip}",
|
| 240 |
+
]
|
| 241 |
+
if card.tiny_action:
|
| 242 |
+
lines.append(f"Tiny action: {card.tiny_action}")
|
| 243 |
+
if card.caring_note:
|
| 244 |
+
lines.append(f"Caring note: {card.caring_note}")
|
| 245 |
+
if card.safety_note:
|
| 246 |
+
lines.append(f"Safety note: {card.safety_note}")
|
| 247 |
+
return "\n".join(lines)
|
| 248 |
+
|
| 249 |
+
|
| 250 |
def _view(
|
| 251 |
session: CustomsSession,
|
| 252 |
text_backend: str = DEFAULT_TEXT_BACKEND,
|
|
|
|
| 455 |
"negotiation": negotiation,
|
| 456 |
"today_tip": card.model_dump(),
|
| 457 |
}
|
| 458 |
+
return negotiation_text, _today_tip_plain_text(card), html, json.dumps(debug, ensure_ascii=False, indent=2)
|
dream_customs/models.py
CHANGED
|
@@ -38,6 +38,9 @@ class FakeASRClient:
|
|
| 38 |
|
| 39 |
|
| 40 |
class FakeTextClient:
|
|
|
|
|
|
|
|
|
|
| 41 |
def generate_brief(self, prompt: str) -> DreamBrief:
|
| 42 |
return DreamBrief(
|
| 43 |
anchors=["elevator", "melted buttons", "floor 14"],
|
|
@@ -49,6 +52,16 @@ class FakeTextClient:
|
|
| 49 |
)
|
| 50 |
|
| 51 |
def generate_negotiation(self, prompt: str) -> Dict[str, Any]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
return {
|
| 53 |
"visitor_name": "迟到的电梯",
|
| 54 |
"questions": [
|
|
@@ -72,6 +85,24 @@ class FakeTextClient:
|
|
| 72 |
)
|
| 73 |
|
| 74 |
def generate_today_tip(self, prompt: str) -> TodayTipCard:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
return TodayTipCard(
|
| 76 |
dream_summary="你梦见电梯迟迟不到,按钮像蜡一样融化,楼层数字停在 14。",
|
| 77 |
main_question="这个梦是否和最近卡在开始之前有关?",
|
|
|
|
| 38 |
|
| 39 |
|
| 40 |
class FakeTextClient:
|
| 41 |
+
def _wants_zh(self, prompt: str) -> bool:
|
| 42 |
+
return "自然中文" in prompt or "所有面向用户的字段都用自然中文" in prompt
|
| 43 |
+
|
| 44 |
def generate_brief(self, prompt: str) -> DreamBrief:
|
| 45 |
return DreamBrief(
|
| 46 |
anchors=["elevator", "melted buttons", "floor 14"],
|
|
|
|
| 52 |
)
|
| 53 |
|
| 54 |
def generate_negotiation(self, prompt: str) -> Dict[str, Any]:
|
| 55 |
+
if not self._wants_zh(prompt):
|
| 56 |
+
return {
|
| 57 |
+
"visitor_name": "Late elevator",
|
| 58 |
+
"questions": [
|
| 59 |
+
"When you think about the elevator and the melted button, what real thing today feels hard to start?",
|
| 60 |
+
"Was the strongest feeling urgency, tiredness, curiosity, or something else?",
|
| 61 |
+
"Would a practical suggestion, a tiny action, or a caring sentence help most today?",
|
| 62 |
+
],
|
| 63 |
+
"tone_note": "The dream may be asking you to make the first step smaller, not to finish everything at once.",
|
| 64 |
+
}
|
| 65 |
return {
|
| 66 |
"visitor_name": "迟到的电梯",
|
| 67 |
"questions": [
|
|
|
|
| 85 |
)
|
| 86 |
|
| 87 |
def generate_today_tip(self, prompt: str) -> TodayTipCard:
|
| 88 |
+
if not self._wants_zh(prompt):
|
| 89 |
+
return TodayTipCard(
|
| 90 |
+
dream_summary="You dreamed that an elevator would not arrive, its button melted like wax, and floor 14 stayed frozen.",
|
| 91 |
+
main_question="Could this dream connect to feeling stuck before starting?",
|
| 92 |
+
dream_anchors=["elevator", "melted button", "floor 14"],
|
| 93 |
+
followup_questions=[
|
| 94 |
+
"Was the strongest feeling urgency, fear, or absurdity?",
|
| 95 |
+
"Is there one real task that feels late before it has even started?",
|
| 96 |
+
],
|
| 97 |
+
user_answers=[],
|
| 98 |
+
interpretation=(
|
| 99 |
+
"Maybe this dream is not predicting lateness. It turns the feeling of being stuck before "
|
| 100 |
+
"starting into an elevator paused at floor 14."
|
| 101 |
+
),
|
| 102 |
+
today_tip="For today, press one very small elevator button: open the task, without asking yourself to arrive yet.",
|
| 103 |
+
tiny_action="Spend five minutes writing which floor you want to stop at first today.",
|
| 104 |
+
caring_note="You can move one floor at a time; the whole building does not need to be solved this morning.",
|
| 105 |
+
)
|
| 106 |
return TodayTipCard(
|
| 107 |
dream_summary="你梦见电梯迟迟不到,按钮像蜡一样融化,楼层数字停在 14。",
|
| 108 |
main_question="这个梦是否和最近卡在开始之前有关?",
|
dream_customs/pipeline.py
CHANGED
|
@@ -19,6 +19,14 @@ from dream_customs.safety import needs_escalation, safety_note
|
|
| 19 |
from dream_customs.schema import CustomsSession, DreamIntake, DreamQAState, EvidenceItem, PactCard, TimelineEvent, TodayTipCard
|
| 20 |
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
def build_intake(
|
| 23 |
dream_text: str = "",
|
| 24 |
voice_transcript: str = "",
|
|
@@ -142,7 +150,9 @@ def _extract_dream_anchors(intake: DreamIntake) -> List[str]:
|
|
| 142 |
r"\b([a-z][a-z'-]+)\s+("
|
| 143 |
r"paper|papers|promise|promises|window|windows|suitcase|suitcases|"
|
| 144 |
r"clerk|clerks|sunrise|elevator|elevators|button|buttons|hallway|"
|
| 145 |
-
r"gate|gates|floor|floors|stamp|stamps|number|numbers"
|
|
|
|
|
|
|
| 146 |
r")\b"
|
| 147 |
)
|
| 148 |
for match in pair_pattern.finditer(text):
|
|
@@ -152,7 +162,8 @@ def _extract_dream_anchors(intake: DreamIntake) -> List[str]:
|
|
| 152 |
candidates.append(phrase)
|
| 153 |
|
| 154 |
noun_pattern = re.compile(
|
| 155 |
-
r"\b(customs|suitcase|clerk|sunrise|elevator|button|hallway|gate|stamp|number|floor
|
|
|
|
| 156 |
)
|
| 157 |
candidates.extend(match.group(1) for match in noun_pattern.finditer(text))
|
| 158 |
candidates.extend(clue.lower() for clue in intake.visual_clues if clue.strip())
|
|
@@ -160,50 +171,63 @@ def _extract_dream_anchors(intake: DreamIntake) -> List[str]:
|
|
| 160 |
return _dedupe_preserve_order(candidates)[:5]
|
| 161 |
|
| 162 |
|
| 163 |
-
def _primary_anchor(intake: DreamIntake) -> str:
|
| 164 |
anchors = _extract_dream_anchors(intake)
|
| 165 |
-
|
|
|
|
|
|
|
| 166 |
|
| 167 |
|
| 168 |
-
def _secondary_anchor(intake: DreamIntake) -> str:
|
| 169 |
anchors = _extract_dream_anchors(intake)
|
| 170 |
-
return anchors[1] if len(anchors) > 1 else _primary_anchor(intake)
|
| 171 |
|
| 172 |
|
| 173 |
def _title_anchor(text: str) -> str:
|
| 174 |
return " ".join(part.capitalize() for part in text.split())
|
| 175 |
|
| 176 |
|
| 177 |
-
def _summary_from_intake(intake: DreamIntake) -> str:
|
| 178 |
merged = intake.dream_text.strip() or intake.voice_transcript.strip()
|
| 179 |
if not merged and intake.visual_clues:
|
| 180 |
-
merged = "、".join(intake.visual_clues[:3])
|
| 181 |
if not merged:
|
| 182 |
-
return "你记录了一个还在整理中的梦。"
|
| 183 |
clean = re.sub(r"\s+", " ", merged).strip()
|
| 184 |
if len(clean) > 72:
|
| 185 |
clean = clean[:69].rstrip() + "..."
|
|
|
|
|
|
|
| 186 |
return f"你梦见{clean}" if not clean.startswith(("你", "我", "I ", "i ")) else clean
|
| 187 |
|
| 188 |
|
| 189 |
-
def _main_question_from_intake(intake: DreamIntake) -> str:
|
| 190 |
if intake.main_question.strip():
|
| 191 |
return intake.main_question.strip()
|
| 192 |
-
primary = _primary_anchor(intake)
|
|
|
|
|
|
|
| 193 |
return f"这个梦里的「{primary}」可能在提醒我什么?"
|
| 194 |
|
| 195 |
|
| 196 |
-
def _fallback_interpretation(intake: DreamIntake) -> str:
|
| 197 |
-
primary = _primary_anchor(intake)
|
| 198 |
-
secondary = _secondary_anchor(intake)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
return (
|
| 200 |
f"也许这个梦不是在给你一个确定答案,而是把「{primary}」和「{secondary}」放到一起,"
|
| 201 |
"提醒你先看见今天最卡住的一小处。"
|
| 202 |
)
|
| 203 |
|
| 204 |
|
| 205 |
-
def _grounded_today_tip(intake: DreamIntake) -> str:
|
| 206 |
-
primary = _primary_anchor(intake)
|
|
|
|
|
|
|
| 207 |
return f"今天先从「{primary}」借一个动作:只做最小的第一步,不急着把整件事完成。"
|
| 208 |
|
| 209 |
|
|
@@ -352,20 +376,27 @@ def _grounded_alliance_reading(intake: DreamIntake) -> str:
|
|
| 352 |
)
|
| 353 |
|
| 354 |
|
| 355 |
-
def _grounded_question(intake: DreamIntake, question: str) -> str:
|
| 356 |
anchors = _extract_dream_anchors(intake)
|
| 357 |
if not anchors or _text_uses_anchor(question, anchors):
|
| 358 |
return question
|
| 359 |
-
primary = _primary_anchor(intake)
|
| 360 |
-
secondary = _secondary_anchor(intake)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
return (
|
| 362 |
f"当你想到「{primary}」和「{secondary}」时,今天有没有一件真实的小事,"
|
| 363 |
"你希望它变得更容易开始?"
|
| 364 |
)
|
| 365 |
|
| 366 |
|
| 367 |
-
def _grounded_followup_question(intake: DreamIntake) -> str:
|
| 368 |
-
primary = _primary_anchor(intake)
|
|
|
|
|
|
|
| 369 |
return f"如果「{primary}」能递给你一个更小的第一步,今天那一步会是什么?"
|
| 370 |
|
| 371 |
|
|
@@ -446,11 +477,16 @@ def intake_from_modalities(
|
|
| 446 |
)
|
| 447 |
|
| 448 |
|
| 449 |
-
def build_qa_state(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 450 |
anchors = _extract_dream_anchors(intake)
|
| 451 |
return DreamQAState(
|
| 452 |
-
dream_summary=_summary_from_intake(intake),
|
| 453 |
-
main_question=_main_question_from_intake(intake),
|
| 454 |
dream_anchors=anchors,
|
| 455 |
followup_questions=questions or [],
|
| 456 |
user_answers=answers or [],
|
|
@@ -458,37 +494,50 @@ def build_qa_state(intake: DreamIntake, questions: Optional[List[str]] = None, a
|
|
| 458 |
)
|
| 459 |
|
| 460 |
|
| 461 |
-
def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = "") -> TodayTipCard:
|
| 462 |
polished = card.model_copy(deep=True)
|
| 463 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 464 |
if not anchors:
|
| 465 |
-
anchors = [_primary_anchor(intake)]
|
| 466 |
polished.dream_anchors = anchors
|
| 467 |
if not polished.dream_summary.strip():
|
| 468 |
-
polished.dream_summary = _summary_from_intake(intake)
|
| 469 |
if not polished.main_question.strip():
|
| 470 |
-
polished.main_question = _main_question_from_intake(intake)
|
| 471 |
if not polished.interpretation.strip() or not _anchor_in_text(polished.interpretation, anchors):
|
| 472 |
-
polished.interpretation = _fallback_interpretation(intake)
|
| 473 |
generic_tip_markers = ["drink water", "hydrate", "多休息", "保持积极", "take a walk"]
|
| 474 |
if (
|
| 475 |
not polished.today_tip.strip()
|
| 476 |
or any(marker in polished.today_tip.lower() for marker in generic_tip_markers)
|
| 477 |
or not _anchor_in_text(polished.today_tip, anchors)
|
| 478 |
):
|
| 479 |
-
polished.today_tip = _grounded_today_tip(intake)
|
| 480 |
if not polished.tiny_action.strip():
|
| 481 |
-
|
|
|
|
|
|
|
|
|
|
| 482 |
if not polished.caring_note.strip():
|
| 483 |
-
polished.caring_note =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 484 |
merged = "\n".join([intake.merged_text(), answers or ""])
|
| 485 |
polished.safety_note = safety_note() if needs_escalation(merged) else ""
|
| 486 |
return polished
|
| 487 |
|
| 488 |
|
| 489 |
-
def generate_today_tip(intake: DreamIntake, answers: str, text_client) -> TodayTipCard:
|
| 490 |
-
|
| 491 |
-
|
|
|
|
| 492 |
try:
|
| 493 |
if hasattr(text_client, "generate_today_tip"):
|
| 494 |
card = text_client.generate_today_tip(prompt)
|
|
@@ -513,10 +562,10 @@ def generate_today_tip(intake: DreamIntake, answers: str, text_client) -> TodayT
|
|
| 513 |
dream_anchors=qa_state.dream_anchors,
|
| 514 |
followup_questions=qa_state.followup_questions,
|
| 515 |
user_answers=qa_state.user_answers,
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
return _polish_today_tip(card, intake, answers)
|
| 520 |
|
| 521 |
|
| 522 |
def generate_negotiation(intake: DreamIntake, text_client) -> Dict:
|
|
@@ -579,13 +628,18 @@ def _supports_model_led_pact(text_client) -> bool:
|
|
| 579 |
)
|
| 580 |
|
| 581 |
|
| 582 |
-
def create_session() -> CustomsSession:
|
|
|
|
| 583 |
return CustomsSession(
|
| 584 |
events=[
|
| 585 |
TimelineEvent(
|
| 586 |
role="system",
|
| 587 |
-
title="梦境问答台已打开",
|
| 588 |
-
body=
|
|
|
|
|
|
|
|
|
|
|
|
|
| 589 |
status="ready",
|
| 590 |
)
|
| 591 |
]
|
|
@@ -640,18 +694,34 @@ def add_evidence(
|
|
| 640 |
mood: str = "",
|
| 641 |
vision_client=None,
|
| 642 |
asr_client=None,
|
|
|
|
| 643 |
) -> CustomsSession:
|
|
|
|
| 644 |
next_session = session.model_copy(deep=True)
|
| 645 |
added_items: List[EvidenceItem] = []
|
| 646 |
|
| 647 |
if dream_text and dream_text.strip():
|
| 648 |
clean_text = dream_text.strip()
|
| 649 |
next_session.intake.dream_text = _append_text(next_session.intake.dream_text, clean_text)
|
| 650 |
-
added_items.append(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 651 |
|
| 652 |
if mood and mood.strip() and mood.strip() != next_session.intake.mood:
|
| 653 |
next_session.intake.mood = mood.strip()
|
| 654 |
-
added_items.append(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 655 |
|
| 656 |
if image_path:
|
| 657 |
clues: List[str] = []
|
|
@@ -666,7 +736,7 @@ def add_evidence(
|
|
| 666 |
added_items.append(
|
| 667 |
EvidenceItem(
|
| 668 |
type="image",
|
| 669 |
-
label=f"Image clues ({len(clues)})",
|
| 670 |
status="extracted",
|
| 671 |
content=", ".join(clues),
|
| 672 |
source_path=image_path,
|
|
@@ -676,10 +746,15 @@ def add_evidence(
|
|
| 676 |
added_items.append(
|
| 677 |
EvidenceItem(
|
| 678 |
type="image",
|
| 679 |
-
label="Image evidence",
|
| 680 |
status="failed",
|
| 681 |
source_path=image_path,
|
| 682 |
-
error=error
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 683 |
)
|
| 684 |
)
|
| 685 |
|
|
@@ -696,7 +771,7 @@ def add_evidence(
|
|
| 696 |
added_items.append(
|
| 697 |
EvidenceItem(
|
| 698 |
type="audio",
|
| 699 |
-
label="Voice transcript",
|
| 700 |
status="extracted",
|
| 701 |
content=clean_transcript,
|
| 702 |
source_path=audio_path,
|
|
@@ -706,52 +781,78 @@ def add_evidence(
|
|
| 706 |
added_items.append(
|
| 707 |
EvidenceItem(
|
| 708 |
type="audio",
|
| 709 |
-
label="Voice evidence",
|
| 710 |
status="failed",
|
| 711 |
source_path=audio_path,
|
| 712 |
-
error=error
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 713 |
)
|
| 714 |
)
|
| 715 |
|
| 716 |
if not added_items:
|
| 717 |
next_session.phase = "error"
|
| 718 |
next_session.events.append(
|
| 719 |
-
_event(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 720 |
)
|
| 721 |
return next_session
|
| 722 |
|
| 723 |
next_session.evidence_items.extend(added_items)
|
| 724 |
next_session.phase = "record"
|
| 725 |
-
next_session.qa_state = build_qa_state(next_session.intake)
|
| 726 |
summary = "\n".join(f"{item.label}: {item.content or item.error}" for item in added_items)
|
| 727 |
-
next_session.events.append(
|
|
|
|
|
|
|
| 728 |
_record_safety(next_session)
|
| 729 |
return next_session
|
| 730 |
|
| 731 |
|
| 732 |
-
def ask_questions(session: CustomsSession, text_client, force_another: bool = False) -> CustomsSession:
|
|
|
|
| 733 |
next_session = session.model_copy(deep=True)
|
| 734 |
if not next_session.intake.merged_text():
|
| 735 |
next_session.phase = "error"
|
| 736 |
next_session.events.append(
|
| 737 |
-
_event(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 738 |
)
|
| 739 |
return next_session
|
| 740 |
|
| 741 |
prompt = (
|
| 742 |
-
followup_question_prompt(next_session.intake, next_session.question_history, next_session.answer_history)
|
| 743 |
if force_another
|
| 744 |
-
else negotiation_prompt(next_session.intake)
|
| 745 |
)
|
| 746 |
negotiation = text_client.generate_negotiation(prompt)
|
| 747 |
questions = [question for question in negotiation.get("questions", []) if question]
|
| 748 |
fresh_questions = [question for question in questions if question not in next_session.question_history]
|
| 749 |
if force_another and not fresh_questions:
|
| 750 |
-
fresh_questions = [
|
|
|
|
|
|
|
|
|
|
|
|
|
| 751 |
if not fresh_questions:
|
| 752 |
fresh_questions = questions[:3]
|
| 753 |
if fresh_questions:
|
| 754 |
-
fresh_questions = [_grounded_question(next_session.intake, fresh_questions[0])] + fresh_questions[1:]
|
| 755 |
seen_questions = set(next_session.question_history)
|
| 756 |
deduped_questions: List[str] = []
|
| 757 |
for question in fresh_questions:
|
|
@@ -759,7 +860,7 @@ def ask_questions(session: CustomsSession, text_client, force_another: bool = Fa
|
|
| 759 |
deduped_questions.append(question)
|
| 760 |
seen_questions.add(question)
|
| 761 |
if force_another and not deduped_questions:
|
| 762 |
-
deduped_questions = [_grounded_followup_question(next_session.intake)]
|
| 763 |
fresh_questions = deduped_questions
|
| 764 |
|
| 765 |
next_session.question_history.extend(fresh_questions[:3])
|
|
@@ -768,12 +869,15 @@ def ask_questions(session: CustomsSession, text_client, force_another: bool = Fa
|
|
| 768 |
next_session.intake,
|
| 769 |
questions=next_session.question_history,
|
| 770 |
answers=next_session.answer_history,
|
|
|
|
| 771 |
)
|
| 772 |
next_session.qa_state.current_step = "ask"
|
| 773 |
next_session.events.append(
|
| 774 |
_event(
|
| 775 |
"assistant",
|
| 776 |
-
"梦境助手追问" if len(fresh_questions) == 1 else "梦境助手追问清单"
|
|
|
|
|
|
|
| 777 |
"\n".join(fresh_questions[:3]),
|
| 778 |
meta=str(negotiation.get("visitor_name", "")),
|
| 779 |
status="question",
|
|
@@ -782,7 +886,8 @@ def ask_questions(session: CustomsSession, text_client, force_another: bool = Fa
|
|
| 782 |
return next_session
|
| 783 |
|
| 784 |
|
| 785 |
-
def answer_question(session: CustomsSession, answer: str) -> CustomsSession:
|
|
|
|
| 786 |
next_session = session.model_copy(deep=True)
|
| 787 |
if not answer or not answer.strip():
|
| 788 |
next_session.phase = "error"
|
|
@@ -792,33 +897,48 @@ def answer_question(session: CustomsSession, answer: str) -> CustomsSession:
|
|
| 792 |
return next_session
|
| 793 |
next_session.answer_history.append(answer.strip())
|
| 794 |
next_session.phase = "ask"
|
| 795 |
-
next_session.qa_state = build_qa_state(
|
| 796 |
-
|
|
|
|
|
|
|
| 797 |
_record_safety(next_session)
|
| 798 |
return next_session
|
| 799 |
|
| 800 |
|
| 801 |
-
def skip_question(session: CustomsSession) -> CustomsSession:
|
|
|
|
| 802 |
next_session = session.model_copy(deep=True)
|
| 803 |
-
skip_text = "用户选择跳过这个追问。"
|
| 804 |
next_session.answer_history.append(skip_text)
|
| 805 |
next_session.phase = "ask"
|
| 806 |
-
next_session.qa_state = build_qa_state(
|
| 807 |
-
|
|
|
|
|
|
|
| 808 |
return next_session
|
| 809 |
|
| 810 |
|
| 811 |
-
def finish_today_tip(session: CustomsSession, text_client) -> CustomsSession:
|
|
|
|
| 812 |
next_session = session.model_copy(deep=True)
|
| 813 |
if not next_session.intake.merged_text():
|
| 814 |
next_session.phase = "error"
|
| 815 |
next_session.events.append(
|
| 816 |
-
_event(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 817 |
)
|
| 818 |
return next_session
|
| 819 |
answers = next_session.answers_text()
|
| 820 |
-
card = generate_today_tip(next_session.intake, answers, text_client)
|
| 821 |
-
next_session.qa_state = build_qa_state(
|
|
|
|
|
|
|
| 822 |
next_session.qa_state.dream_summary = card.dream_summary
|
| 823 |
next_session.qa_state.main_question = card.main_question
|
| 824 |
next_session.qa_state.dream_anchors = card.dream_anchors
|
|
@@ -831,7 +951,7 @@ def finish_today_tip(session: CustomsSession, text_client) -> CustomsSession:
|
|
| 831 |
next_session.events.append(
|
| 832 |
_event(
|
| 833 |
"tip",
|
| 834 |
-
"今日小 Tips 已生成",
|
| 835 |
f"{card.interpretation}\n{card.today_tip}",
|
| 836 |
status="tip",
|
| 837 |
)
|
|
|
|
| 19 |
from dream_customs.schema import CustomsSession, DreamIntake, DreamQAState, EvidenceItem, PactCard, TimelineEvent, TodayTipCard
|
| 20 |
|
| 21 |
|
| 22 |
+
def _normalize_language(language: str = "en") -> str:
|
| 23 |
+
return "zh" if language == "zh" else "en"
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _is_zh(language: str = "en") -> bool:
|
| 27 |
+
return _normalize_language(language) == "zh"
|
| 28 |
+
|
| 29 |
+
|
| 30 |
def build_intake(
|
| 31 |
dream_text: str = "",
|
| 32 |
voice_transcript: str = "",
|
|
|
|
| 150 |
r"\b([a-z][a-z'-]+)\s+("
|
| 151 |
r"paper|papers|promise|promises|window|windows|suitcase|suitcases|"
|
| 152 |
r"clerk|clerks|sunrise|elevator|elevators|button|buttons|hallway|"
|
| 153 |
+
r"gate|gates|floor|floors|stamp|stamps|number|numbers|exam|exams|"
|
| 154 |
+
r"pencil|pencils|train|trains|door|doors|phone|phones|water|shoe|shoes|"
|
| 155 |
+
r"stairwell|stairwells|room|rooms|key|keys|note|notes|rain|moon|moons|curtain|curtains"
|
| 156 |
r")\b"
|
| 157 |
)
|
| 158 |
for match in pair_pattern.finditer(text):
|
|
|
|
| 162 |
candidates.append(phrase)
|
| 163 |
|
| 164 |
noun_pattern = re.compile(
|
| 165 |
+
r"\b(customs|suitcase|clerk|sunrise|elevator|button|hallway|gate|stamp|number|floor|"
|
| 166 |
+
r"exam|pencil|train|door|phone|water|shoe|stairwell|room|key|note|rain|moon|curtain|sleep|dream)\b"
|
| 167 |
)
|
| 168 |
candidates.extend(match.group(1) for match in noun_pattern.finditer(text))
|
| 169 |
candidates.extend(clue.lower() for clue in intake.visual_clues if clue.strip())
|
|
|
|
| 171 |
return _dedupe_preserve_order(candidates)[:5]
|
| 172 |
|
| 173 |
|
| 174 |
+
def _primary_anchor(intake: DreamIntake, language: str = "en") -> str:
|
| 175 |
anchors = _extract_dream_anchors(intake)
|
| 176 |
+
if anchors:
|
| 177 |
+
return anchors[0]
|
| 178 |
+
return "梦里的那个细节" if _is_zh(language) else "that dream detail"
|
| 179 |
|
| 180 |
|
| 181 |
+
def _secondary_anchor(intake: DreamIntake, language: str = "en") -> str:
|
| 182 |
anchors = _extract_dream_anchors(intake)
|
| 183 |
+
return anchors[1] if len(anchors) > 1 else _primary_anchor(intake, language)
|
| 184 |
|
| 185 |
|
| 186 |
def _title_anchor(text: str) -> str:
|
| 187 |
return " ".join(part.capitalize() for part in text.split())
|
| 188 |
|
| 189 |
|
| 190 |
+
def _summary_from_intake(intake: DreamIntake, language: str = "en") -> str:
|
| 191 |
merged = intake.dream_text.strip() or intake.voice_transcript.strip()
|
| 192 |
if not merged and intake.visual_clues:
|
| 193 |
+
merged = "、".join(intake.visual_clues[:3]) if _is_zh(language) else ", ".join(intake.visual_clues[:3])
|
| 194 |
if not merged:
|
| 195 |
+
return "你记录了一个还在整理中的梦。" if _is_zh(language) else "You recorded a dream that is still taking shape."
|
| 196 |
clean = re.sub(r"\s+", " ", merged).strip()
|
| 197 |
if len(clean) > 72:
|
| 198 |
clean = clean[:69].rstrip() + "..."
|
| 199 |
+
if not _is_zh(language):
|
| 200 |
+
return clean if clean.lower().startswith(("i ", "you ")) else f"You dreamed about {clean}"
|
| 201 |
return f"你梦见{clean}" if not clean.startswith(("你", "我", "I ", "i ")) else clean
|
| 202 |
|
| 203 |
|
| 204 |
+
def _main_question_from_intake(intake: DreamIntake, language: str = "en") -> str:
|
| 205 |
if intake.main_question.strip():
|
| 206 |
return intake.main_question.strip()
|
| 207 |
+
primary = _primary_anchor(intake, language)
|
| 208 |
+
if not _is_zh(language):
|
| 209 |
+
return f"What might the {primary} be asking me to notice today?"
|
| 210 |
return f"这个梦里的「{primary}」可能在提醒我什么?"
|
| 211 |
|
| 212 |
|
| 213 |
+
def _fallback_interpretation(intake: DreamIntake, language: str = "en") -> str:
|
| 214 |
+
primary = _primary_anchor(intake, language)
|
| 215 |
+
secondary = _secondary_anchor(intake, language)
|
| 216 |
+
if not _is_zh(language):
|
| 217 |
+
return (
|
| 218 |
+
f"Maybe this dream is not giving you a fixed answer. It is placing the {primary} "
|
| 219 |
+
f"beside the {secondary} so you can notice one small stuck point today."
|
| 220 |
+
)
|
| 221 |
return (
|
| 222 |
f"也许这个梦不是在给你一个确定答案,而是把「{primary}」和「{secondary}」放到一起,"
|
| 223 |
"提醒你先看见今天最卡住的一小处。"
|
| 224 |
)
|
| 225 |
|
| 226 |
|
| 227 |
+
def _grounded_today_tip(intake: DreamIntake, language: str = "en") -> str:
|
| 228 |
+
primary = _primary_anchor(intake, language)
|
| 229 |
+
if not _is_zh(language):
|
| 230 |
+
return f"For today, borrow one action from the {primary}: open the smallest first step and stop there."
|
| 231 |
return f"今天先从「{primary}」借一个动作:只做最小的第一步,不急着把整件事完成。"
|
| 232 |
|
| 233 |
|
|
|
|
| 376 |
)
|
| 377 |
|
| 378 |
|
| 379 |
+
def _grounded_question(intake: DreamIntake, question: str, language: str = "en") -> str:
|
| 380 |
anchors = _extract_dream_anchors(intake)
|
| 381 |
if not anchors or _text_uses_anchor(question, anchors):
|
| 382 |
return question
|
| 383 |
+
primary = _primary_anchor(intake, language)
|
| 384 |
+
secondary = _secondary_anchor(intake, language)
|
| 385 |
+
if not _is_zh(language):
|
| 386 |
+
return (
|
| 387 |
+
f"When you think about the {primary} and the {secondary}, is there one real thing today "
|
| 388 |
+
"that you want to make easier to start?"
|
| 389 |
+
)
|
| 390 |
return (
|
| 391 |
f"当你想到「{primary}」和「{secondary}」时,今天有没有一件真实的小事,"
|
| 392 |
"你希望它变得更容易开始?"
|
| 393 |
)
|
| 394 |
|
| 395 |
|
| 396 |
+
def _grounded_followup_question(intake: DreamIntake, language: str = "en") -> str:
|
| 397 |
+
primary = _primary_anchor(intake, language)
|
| 398 |
+
if not _is_zh(language):
|
| 399 |
+
return f"If the {primary} could hand you one smaller first step today, what would that step be?"
|
| 400 |
return f"如果「{primary}」能递给你一个更小的第一步,今天那一步会是什么?"
|
| 401 |
|
| 402 |
|
|
|
|
| 477 |
)
|
| 478 |
|
| 479 |
|
| 480 |
+
def build_qa_state(
|
| 481 |
+
intake: DreamIntake,
|
| 482 |
+
questions: Optional[List[str]] = None,
|
| 483 |
+
answers: Optional[List[str]] = None,
|
| 484 |
+
language: str = "en",
|
| 485 |
+
) -> DreamQAState:
|
| 486 |
anchors = _extract_dream_anchors(intake)
|
| 487 |
return DreamQAState(
|
| 488 |
+
dream_summary=_summary_from_intake(intake, language),
|
| 489 |
+
main_question=_main_question_from_intake(intake, language),
|
| 490 |
dream_anchors=anchors,
|
| 491 |
followup_questions=questions or [],
|
| 492 |
user_answers=answers or [],
|
|
|
|
| 494 |
)
|
| 495 |
|
| 496 |
|
| 497 |
+
def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = "", language: str = "en") -> TodayTipCard:
|
| 498 |
polished = card.model_copy(deep=True)
|
| 499 |
+
intake_anchors = _extract_dream_anchors(intake)
|
| 500 |
+
card_anchors = polished.dream_anchors
|
| 501 |
+
if intake_anchors and not any(_text_uses_anchor(anchor, intake_anchors) for anchor in card_anchors):
|
| 502 |
+
anchors = intake_anchors
|
| 503 |
+
else:
|
| 504 |
+
anchors = card_anchors or intake_anchors
|
| 505 |
if not anchors:
|
| 506 |
+
anchors = [_primary_anchor(intake, language)]
|
| 507 |
polished.dream_anchors = anchors
|
| 508 |
if not polished.dream_summary.strip():
|
| 509 |
+
polished.dream_summary = _summary_from_intake(intake, language)
|
| 510 |
if not polished.main_question.strip():
|
| 511 |
+
polished.main_question = _main_question_from_intake(intake, language)
|
| 512 |
if not polished.interpretation.strip() or not _anchor_in_text(polished.interpretation, anchors):
|
| 513 |
+
polished.interpretation = _fallback_interpretation(intake, language)
|
| 514 |
generic_tip_markers = ["drink water", "hydrate", "多休息", "保持积极", "take a walk"]
|
| 515 |
if (
|
| 516 |
not polished.today_tip.strip()
|
| 517 |
or any(marker in polished.today_tip.lower() for marker in generic_tip_markers)
|
| 518 |
or not _anchor_in_text(polished.today_tip, anchors)
|
| 519 |
):
|
| 520 |
+
polished.today_tip = _grounded_today_tip(intake, language)
|
| 521 |
if not polished.tiny_action.strip():
|
| 522 |
+
if _is_zh(language):
|
| 523 |
+
polished.tiny_action = f"用 5 分钟写下:今天和「{anchors[0]}」有关的第一小步是什么?"
|
| 524 |
+
else:
|
| 525 |
+
polished.tiny_action = f"Spend five minutes writing the first small step connected to the {anchors[0]}."
|
| 526 |
if not polished.caring_note.strip():
|
| 527 |
+
polished.caring_note = (
|
| 528 |
+
"你不需要一醒来就解决整个梦,先把一个细节照亮就很好。"
|
| 529 |
+
if _is_zh(language)
|
| 530 |
+
else "You do not have to solve the whole dream this morning; noticing one detail is enough."
|
| 531 |
+
)
|
| 532 |
merged = "\n".join([intake.merged_text(), answers or ""])
|
| 533 |
polished.safety_note = safety_note() if needs_escalation(merged) else ""
|
| 534 |
return polished
|
| 535 |
|
| 536 |
|
| 537 |
+
def generate_today_tip(intake: DreamIntake, answers: str, text_client, language: str = "en") -> TodayTipCard:
|
| 538 |
+
language = _normalize_language(language)
|
| 539 |
+
qa_state = build_qa_state(intake, answers=[answer for answer in [answers] if answer], language=language)
|
| 540 |
+
prompt = today_tip_prompt(qa_state, language=language)
|
| 541 |
try:
|
| 542 |
if hasattr(text_client, "generate_today_tip"):
|
| 543 |
card = text_client.generate_today_tip(prompt)
|
|
|
|
| 562 |
dream_anchors=qa_state.dream_anchors,
|
| 563 |
followup_questions=qa_state.followup_questions,
|
| 564 |
user_answers=qa_state.user_answers,
|
| 565 |
+
interpretation=_fallback_interpretation(intake, language),
|
| 566 |
+
today_tip=_grounded_today_tip(intake, language),
|
| 567 |
+
)
|
| 568 |
+
return _polish_today_tip(card, intake, answers, language)
|
| 569 |
|
| 570 |
|
| 571 |
def generate_negotiation(intake: DreamIntake, text_client) -> Dict:
|
|
|
|
| 628 |
)
|
| 629 |
|
| 630 |
|
| 631 |
+
def create_session(language: str = "en") -> CustomsSession:
|
| 632 |
+
language = _normalize_language(language)
|
| 633 |
return CustomsSession(
|
| 634 |
events=[
|
| 635 |
TimelineEvent(
|
| 636 |
role="system",
|
| 637 |
+
title="梦境问答台已打开" if _is_zh(language) else "Dream QA is open",
|
| 638 |
+
body=(
|
| 639 |
+
"先记录一个梦境片段。文字永远可用,图片和语音会变成同一个梦境 intake 的线索。"
|
| 640 |
+
if _is_zh(language)
|
| 641 |
+
else "Record a dream fragment first. Text always works; image and voice become clues in the same intake."
|
| 642 |
+
),
|
| 643 |
status="ready",
|
| 644 |
)
|
| 645 |
]
|
|
|
|
| 694 |
mood: str = "",
|
| 695 |
vision_client=None,
|
| 696 |
asr_client=None,
|
| 697 |
+
language: str = "en",
|
| 698 |
) -> CustomsSession:
|
| 699 |
+
language = _normalize_language(language)
|
| 700 |
next_session = session.model_copy(deep=True)
|
| 701 |
added_items: List[EvidenceItem] = []
|
| 702 |
|
| 703 |
if dream_text and dream_text.strip():
|
| 704 |
clean_text = dream_text.strip()
|
| 705 |
next_session.intake.dream_text = _append_text(next_session.intake.dream_text, clean_text)
|
| 706 |
+
added_items.append(
|
| 707 |
+
EvidenceItem(
|
| 708 |
+
type="text",
|
| 709 |
+
label="Dream note" if not _is_zh(language) else "梦境记录",
|
| 710 |
+
status="selected",
|
| 711 |
+
content=clean_text,
|
| 712 |
+
)
|
| 713 |
+
)
|
| 714 |
|
| 715 |
if mood and mood.strip() and mood.strip() != next_session.intake.mood:
|
| 716 |
next_session.intake.mood = mood.strip()
|
| 717 |
+
added_items.append(
|
| 718 |
+
EvidenceItem(
|
| 719 |
+
type="mood",
|
| 720 |
+
label=f"Mood: {mood.strip()}" if not _is_zh(language) else f"心情:{mood.strip()}",
|
| 721 |
+
status="selected",
|
| 722 |
+
content=mood.strip(),
|
| 723 |
+
)
|
| 724 |
+
)
|
| 725 |
|
| 726 |
if image_path:
|
| 727 |
clues: List[str] = []
|
|
|
|
| 736 |
added_items.append(
|
| 737 |
EvidenceItem(
|
| 738 |
type="image",
|
| 739 |
+
label=f"Image clues ({len(clues)})" if not _is_zh(language) else f"图片线索({len(clues)})",
|
| 740 |
status="extracted",
|
| 741 |
content=", ".join(clues),
|
| 742 |
source_path=image_path,
|
|
|
|
| 746 |
added_items.append(
|
| 747 |
EvidenceItem(
|
| 748 |
type="image",
|
| 749 |
+
label="Image evidence" if not _is_zh(language) else "图片证据",
|
| 750 |
status="failed",
|
| 751 |
source_path=image_path,
|
| 752 |
+
error=error
|
| 753 |
+
or (
|
| 754 |
+
"No visual clues extracted. Continue with text or voice."
|
| 755 |
+
if not _is_zh(language)
|
| 756 |
+
else "没有提取到视觉线索,可以继续使用文字或语音。"
|
| 757 |
+
),
|
| 758 |
)
|
| 759 |
)
|
| 760 |
|
|
|
|
| 771 |
added_items.append(
|
| 772 |
EvidenceItem(
|
| 773 |
type="audio",
|
| 774 |
+
label="Voice transcript" if not _is_zh(language) else "语音转写",
|
| 775 |
status="extracted",
|
| 776 |
content=clean_transcript,
|
| 777 |
source_path=audio_path,
|
|
|
|
| 781 |
added_items.append(
|
| 782 |
EvidenceItem(
|
| 783 |
type="audio",
|
| 784 |
+
label="Voice evidence" if not _is_zh(language) else "语音证据",
|
| 785 |
status="failed",
|
| 786 |
source_path=audio_path,
|
| 787 |
+
error=error
|
| 788 |
+
or (
|
| 789 |
+
"No transcript returned. Continue by typing the fragment."
|
| 790 |
+
if not _is_zh(language)
|
| 791 |
+
else "没有返回转写结果,可以继续手动输入片段。"
|
| 792 |
+
),
|
| 793 |
)
|
| 794 |
)
|
| 795 |
|
| 796 |
if not added_items:
|
| 797 |
next_session.phase = "error"
|
| 798 |
next_session.events.append(
|
| 799 |
+
_event(
|
| 800 |
+
"error",
|
| 801 |
+
"还没有梦境材料" if _is_zh(language) else "No dream material yet",
|
| 802 |
+
"请先添加文字、图片或语音;text-only 路径始终可用。"
|
| 803 |
+
if _is_zh(language)
|
| 804 |
+
else "Add text, image, or voice first; the text-only path always works.",
|
| 805 |
+
status="empty",
|
| 806 |
+
)
|
| 807 |
)
|
| 808 |
return next_session
|
| 809 |
|
| 810 |
next_session.evidence_items.extend(added_items)
|
| 811 |
next_session.phase = "record"
|
| 812 |
+
next_session.qa_state = build_qa_state(next_session.intake, language=language)
|
| 813 |
summary = "\n".join(f"{item.label}: {item.content or item.error}" for item in added_items)
|
| 814 |
+
next_session.events.append(
|
| 815 |
+
_event("user", "梦境已记录" if _is_zh(language) else "Dream recorded", summary, status="record")
|
| 816 |
+
)
|
| 817 |
_record_safety(next_session)
|
| 818 |
return next_session
|
| 819 |
|
| 820 |
|
| 821 |
+
def ask_questions(session: CustomsSession, text_client, force_another: bool = False, language: str = "en") -> CustomsSession:
|
| 822 |
+
language = _normalize_language(language)
|
| 823 |
next_session = session.model_copy(deep=True)
|
| 824 |
if not next_session.intake.merged_text():
|
| 825 |
next_session.phase = "error"
|
| 826 |
next_session.events.append(
|
| 827 |
+
_event(
|
| 828 |
+
"error",
|
| 829 |
+
"还没有梦境记录" if _is_zh(language) else "No dream note yet",
|
| 830 |
+
"请先添加一个梦境片段,再进入追问。"
|
| 831 |
+
if _is_zh(language)
|
| 832 |
+
else "Add a dream fragment before the follow-up question.",
|
| 833 |
+
status="empty",
|
| 834 |
+
)
|
| 835 |
)
|
| 836 |
return next_session
|
| 837 |
|
| 838 |
prompt = (
|
| 839 |
+
followup_question_prompt(next_session.intake, next_session.question_history, next_session.answer_history, language)
|
| 840 |
if force_another
|
| 841 |
+
else negotiation_prompt(next_session.intake, language)
|
| 842 |
)
|
| 843 |
negotiation = text_client.generate_negotiation(prompt)
|
| 844 |
questions = [question for question in negotiation.get("questions", []) if question]
|
| 845 |
fresh_questions = [question for question in questions if question not in next_session.question_history]
|
| 846 |
if force_another and not fresh_questions:
|
| 847 |
+
fresh_questions = [
|
| 848 |
+
"如果今天只需要一个更小的第一步,它会是什么?"
|
| 849 |
+
if _is_zh(language)
|
| 850 |
+
else "If today only needed one smaller first step, what would it be?"
|
| 851 |
+
]
|
| 852 |
if not fresh_questions:
|
| 853 |
fresh_questions = questions[:3]
|
| 854 |
if fresh_questions:
|
| 855 |
+
fresh_questions = [_grounded_question(next_session.intake, fresh_questions[0], language)] + fresh_questions[1:]
|
| 856 |
seen_questions = set(next_session.question_history)
|
| 857 |
deduped_questions: List[str] = []
|
| 858 |
for question in fresh_questions:
|
|
|
|
| 860 |
deduped_questions.append(question)
|
| 861 |
seen_questions.add(question)
|
| 862 |
if force_another and not deduped_questions:
|
| 863 |
+
deduped_questions = [_grounded_followup_question(next_session.intake, language)]
|
| 864 |
fresh_questions = deduped_questions
|
| 865 |
|
| 866 |
next_session.question_history.extend(fresh_questions[:3])
|
|
|
|
| 869 |
next_session.intake,
|
| 870 |
questions=next_session.question_history,
|
| 871 |
answers=next_session.answer_history,
|
| 872 |
+
language=language,
|
| 873 |
)
|
| 874 |
next_session.qa_state.current_step = "ask"
|
| 875 |
next_session.events.append(
|
| 876 |
_event(
|
| 877 |
"assistant",
|
| 878 |
+
("梦境助手追问" if len(fresh_questions) == 1 else "梦境助手追问清单")
|
| 879 |
+
if _is_zh(language)
|
| 880 |
+
else ("Dream QA question" if len(fresh_questions) == 1 else "Dream QA questions"),
|
| 881 |
"\n".join(fresh_questions[:3]),
|
| 882 |
meta=str(negotiation.get("visitor_name", "")),
|
| 883 |
status="question",
|
|
|
|
| 886 |
return next_session
|
| 887 |
|
| 888 |
|
| 889 |
+
def answer_question(session: CustomsSession, answer: str, language: str = "en") -> CustomsSession:
|
| 890 |
+
language = _normalize_language(language)
|
| 891 |
next_session = session.model_copy(deep=True)
|
| 892 |
if not answer or not answer.strip():
|
| 893 |
next_session.phase = "error"
|
|
|
|
| 897 |
return next_session
|
| 898 |
next_session.answer_history.append(answer.strip())
|
| 899 |
next_session.phase = "ask"
|
| 900 |
+
next_session.qa_state = build_qa_state(
|
| 901 |
+
next_session.intake, next_session.question_history, next_session.answer_history, language=language
|
| 902 |
+
)
|
| 903 |
+
next_session.events.append(_event("user", "追问回答" if _is_zh(language) else "Question answered", answer.strip(), status="answered"))
|
| 904 |
_record_safety(next_session)
|
| 905 |
return next_session
|
| 906 |
|
| 907 |
|
| 908 |
+
def skip_question(session: CustomsSession, language: str = "en") -> CustomsSession:
|
| 909 |
+
language = _normalize_language(language)
|
| 910 |
next_session = session.model_copy(deep=True)
|
| 911 |
+
skip_text = "用户选择跳过这个追问。" if _is_zh(language) else "The user chose to skip this question."
|
| 912 |
next_session.answer_history.append(skip_text)
|
| 913 |
next_session.phase = "ask"
|
| 914 |
+
next_session.qa_state = build_qa_state(
|
| 915 |
+
next_session.intake, next_session.question_history, next_session.answer_history, language=language
|
| 916 |
+
)
|
| 917 |
+
next_session.events.append(_event("user", "跳过追问" if _is_zh(language) else "Question skipped", skip_text, status="skipped"))
|
| 918 |
return next_session
|
| 919 |
|
| 920 |
|
| 921 |
+
def finish_today_tip(session: CustomsSession, text_client, language: str = "en") -> CustomsSession:
|
| 922 |
+
language = _normalize_language(language)
|
| 923 |
next_session = session.model_copy(deep=True)
|
| 924 |
if not next_session.intake.merged_text():
|
| 925 |
next_session.phase = "error"
|
| 926 |
next_session.events.append(
|
| 927 |
+
_event(
|
| 928 |
+
"error",
|
| 929 |
+
"今日小 Tips 需要梦境材料" if _is_zh(language) else "Today Tip needs dream material",
|
| 930 |
+
"请先添加至少一个梦境片段。"
|
| 931 |
+
if _is_zh(language)
|
| 932 |
+
else "Add at least one dream fragment first.",
|
| 933 |
+
status="empty",
|
| 934 |
+
)
|
| 935 |
)
|
| 936 |
return next_session
|
| 937 |
answers = next_session.answers_text()
|
| 938 |
+
card = generate_today_tip(next_session.intake, answers, text_client, language=language)
|
| 939 |
+
next_session.qa_state = build_qa_state(
|
| 940 |
+
next_session.intake, next_session.question_history, next_session.answer_history, language=language
|
| 941 |
+
)
|
| 942 |
next_session.qa_state.dream_summary = card.dream_summary
|
| 943 |
next_session.qa_state.main_question = card.main_question
|
| 944 |
next_session.qa_state.dream_anchors = card.dream_anchors
|
|
|
|
| 951 |
next_session.events.append(
|
| 952 |
_event(
|
| 953 |
"tip",
|
| 954 |
+
"今日小 Tips 已生成" if _is_zh(language) else "Today Tip generated",
|
| 955 |
f"{card.interpretation}\n{card.today_tip}",
|
| 956 |
status="tip",
|
| 957 |
)
|
dream_customs/prompts.py
CHANGED
|
@@ -18,12 +18,21 @@ def visual_witness_prompt() -> str:
|
|
| 18 |
)
|
| 19 |
|
| 20 |
|
| 21 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
return f"""
|
| 23 |
You are MiniCPM5-1B acting as Dream QA / 梦境问答台, a gentle question guide.
|
| 24 |
Summarize the dream, infer the user's main question if they did not write one, and ask 1 to 3 warm follow-up questions.
|
| 25 |
Do not diagnose, predict fate, frighten the user, or claim one fixed dream meaning.
|
| 26 |
Ground every question in a concrete detail from the text, voice transcript, mood, or visual clues.
|
|
|
|
| 27 |
|
| 28 |
Dream intake:
|
| 29 |
{intake.merged_text()}
|
|
@@ -37,7 +46,7 @@ Return strict JSON with:
|
|
| 37 |
""".strip()
|
| 38 |
|
| 39 |
|
| 40 |
-
def today_tip_prompt(state: DreamQAState) -> str:
|
| 41 |
return f"""
|
| 42 |
You are MiniCPM5-1B writing the final Dream QA result.
|
| 43 |
Write a non-diagnostic interpretation draft and exactly one primary 今日小 Tips.
|
|
@@ -46,6 +55,7 @@ The today_tip must cite at least one concrete dream anchor.
|
|
| 46 |
Avoid prophecy, frightening certainty, medical advice, therapy framing, and generic wellness filler.
|
| 47 |
Keep the tiny_action small enough to try today.
|
| 48 |
Use safety_note only for self-harm, harm to others, severe distress, severe insomnia, panic, or inability to function.
|
|
|
|
| 49 |
|
| 50 |
Dream QA state:
|
| 51 |
{_json_block(state)}
|
|
@@ -152,7 +162,7 @@ def visual_clue_prompt() -> str:
|
|
| 152 |
)
|
| 153 |
|
| 154 |
|
| 155 |
-
def negotiation_prompt(intake: DreamIntake) -> str:
|
| 156 |
return f"""
|
| 157 |
You are the Dream QA question guide. The user is not asking for diagnosis.
|
| 158 |
Help the user record the dream, clarify the main question, and answer one gentle follow-up before the final 今日小 Tips.
|
|
@@ -161,18 +171,24 @@ Ask questions that an ordinary person can understand without knowing any app lor
|
|
| 161 |
Prefer questions about the strongest feeling, one confusing scene, or one safe next-day reference.
|
| 162 |
Ground every question in a concrete detail from the intake when possible, such as an object,
|
| 163 |
place, action, color, or phrase the user actually provided.
|
|
|
|
| 164 |
|
| 165 |
Dream intake:
|
| 166 |
{intake.merged_text()}
|
| 167 |
|
| 168 |
Return JSON with:
|
| 169 |
- visitor_name: short vivid anchor label
|
| 170 |
-
- questions:
|
| 171 |
- tone_note: one sentence explaining why the questions may help without certainty
|
| 172 |
""".strip()
|
| 173 |
|
| 174 |
|
| 175 |
-
def followup_question_prompt(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
return f"""
|
| 177 |
You are the Dream QA question guide. Ask one more gentle follow-up question.
|
| 178 |
Do not diagnose. Do not repeat previous questions.
|
|
@@ -181,6 +197,7 @@ or whether one concrete dream detail connects to today.
|
|
| 181 |
Do not use unclear metaphors about fate, symbols, hidden meanings, stamps, release, or permits.
|
| 182 |
Reuse one concrete dream detail from the intake so the user can feel the question belongs
|
| 183 |
to this dream rather than to a generic reflection form.
|
|
|
|
| 184 |
|
| 185 |
Dream intake:
|
| 186 |
{intake.merged_text()}
|
|
@@ -193,7 +210,7 @@ User answers:
|
|
| 193 |
|
| 194 |
Return JSON with:
|
| 195 |
- visitor_name: short vivid name
|
| 196 |
-
- questions: one gentle, specific question
|
| 197 |
- tone_note: one sentence explaining why this question matters today
|
| 198 |
""".strip()
|
| 199 |
|
|
|
|
| 18 |
)
|
| 19 |
|
| 20 |
|
| 21 |
+
def _language_instruction(language: str = "en") -> str:
|
| 22 |
+
return (
|
| 23 |
+
"Write all user-facing fields in English. Keep any user-provided dream anchors as written."
|
| 24 |
+
if language != "zh"
|
| 25 |
+
else "所有面向用户的字段都用自然中文书写。可以保留用户原文里的梦境关键词。"
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def dream_qa_state_prompt(intake: DreamIntake, language: str = "en") -> str:
|
| 30 |
return f"""
|
| 31 |
You are MiniCPM5-1B acting as Dream QA / 梦境问答台, a gentle question guide.
|
| 32 |
Summarize the dream, infer the user's main question if they did not write one, and ask 1 to 3 warm follow-up questions.
|
| 33 |
Do not diagnose, predict fate, frighten the user, or claim one fixed dream meaning.
|
| 34 |
Ground every question in a concrete detail from the text, voice transcript, mood, or visual clues.
|
| 35 |
+
{_language_instruction(language)}
|
| 36 |
|
| 37 |
Dream intake:
|
| 38 |
{intake.merged_text()}
|
|
|
|
| 46 |
""".strip()
|
| 47 |
|
| 48 |
|
| 49 |
+
def today_tip_prompt(state: DreamQAState, language: str = "en") -> str:
|
| 50 |
return f"""
|
| 51 |
You are MiniCPM5-1B writing the final Dream QA result.
|
| 52 |
Write a non-diagnostic interpretation draft and exactly one primary 今日小 Tips.
|
|
|
|
| 55 |
Avoid prophecy, frightening certainty, medical advice, therapy framing, and generic wellness filler.
|
| 56 |
Keep the tiny_action small enough to try today.
|
| 57 |
Use safety_note only for self-harm, harm to others, severe distress, severe insomnia, panic, or inability to function.
|
| 58 |
+
{_language_instruction(language)}
|
| 59 |
|
| 60 |
Dream QA state:
|
| 61 |
{_json_block(state)}
|
|
|
|
| 162 |
)
|
| 163 |
|
| 164 |
|
| 165 |
+
def negotiation_prompt(intake: DreamIntake, language: str = "en") -> str:
|
| 166 |
return f"""
|
| 167 |
You are the Dream QA question guide. The user is not asking for diagnosis.
|
| 168 |
Help the user record the dream, clarify the main question, and answer one gentle follow-up before the final 今日小 Tips.
|
|
|
|
| 171 |
Prefer questions about the strongest feeling, one confusing scene, or one safe next-day reference.
|
| 172 |
Ground every question in a concrete detail from the intake when possible, such as an object,
|
| 173 |
place, action, color, or phrase the user actually provided.
|
| 174 |
+
{_language_instruction(language)}
|
| 175 |
|
| 176 |
Dream intake:
|
| 177 |
{intake.merged_text()}
|
| 178 |
|
| 179 |
Return JSON with:
|
| 180 |
- visitor_name: short vivid anchor label
|
| 181 |
+
- questions: 1 to 3 gentle, specific, easy-to-understand questions
|
| 182 |
- tone_note: one sentence explaining why the questions may help without certainty
|
| 183 |
""".strip()
|
| 184 |
|
| 185 |
|
| 186 |
+
def followup_question_prompt(
|
| 187 |
+
intake: DreamIntake,
|
| 188 |
+
question_history: list[str],
|
| 189 |
+
answer_history: list[str],
|
| 190 |
+
language: str = "en",
|
| 191 |
+
) -> str:
|
| 192 |
return f"""
|
| 193 |
You are the Dream QA question guide. Ask one more gentle follow-up question.
|
| 194 |
Do not diagnose. Do not repeat previous questions.
|
|
|
|
| 197 |
Do not use unclear metaphors about fate, symbols, hidden meanings, stamps, release, or permits.
|
| 198 |
Reuse one concrete dream detail from the intake so the user can feel the question belongs
|
| 199 |
to this dream rather than to a generic reflection form.
|
| 200 |
+
{_language_instruction(language)}
|
| 201 |
|
| 202 |
Dream intake:
|
| 203 |
{intake.merged_text()}
|
|
|
|
| 210 |
|
| 211 |
Return JSON with:
|
| 212 |
- visitor_name: short vivid name
|
| 213 |
+
- questions: one gentle, specific question in a single-item list
|
| 214 |
- tone_note: one sentence explaining why this question matters today
|
| 215 |
""".strip()
|
| 216 |
|
dream_customs/render.py
CHANGED
|
@@ -23,12 +23,29 @@ def _phase_label(phase: str) -> str:
|
|
| 23 |
return labels.get(phase, phase.title())
|
| 24 |
|
| 25 |
|
| 26 |
-
def render_today_tip_card(card: TodayTipCard) -> str:
|
|
|
|
| 27 |
anchors = " ".join(escape(anchor) for anchor in card.dream_anchors)
|
| 28 |
questions = "".join(f"<li>{escape(question)}</li>" for question in card.followup_questions)
|
| 29 |
answers = "".join(f"<li>{escape(answer)}</li>" for answer in card.user_answers)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
tiny_action = (
|
| 31 |
-
f"<section class='dqa-result-card'><div class='dqa-card-icon'>□</div><div><h3>
|
| 32 |
if card.tiny_action
|
| 33 |
else ""
|
| 34 |
)
|
|
@@ -38,38 +55,38 @@ def render_today_tip_card(card: TodayTipCard) -> str:
|
|
| 38 |
else ""
|
| 39 |
)
|
| 40 |
safety = (
|
| 41 |
-
f"<div class='dqa-safety-note'>
|
| 42 |
if card.safety_note
|
| 43 |
-
else "<div class='dqa-safety-note'>
|
| 44 |
)
|
| 45 |
qa_history = (
|
| 46 |
-
f"<details class='dqa-qa-history'><summary>
|
| 47 |
if questions or answers
|
| 48 |
else ""
|
| 49 |
)
|
| 50 |
return f"""
|
| 51 |
-
<section class="dqa-tip-page" aria-label="
|
| 52 |
<div class="dqa-tip-hero">
|
| 53 |
<div>
|
| 54 |
<div class="dqa-sun">☼</div>
|
| 55 |
-
<h2>
|
| 56 |
-
<p>
|
| 57 |
</div>
|
| 58 |
</div>
|
| 59 |
<article class="dqa-result-card is-interpretation">
|
| 60 |
<div class="dqa-card-icon">☘</div>
|
| 61 |
<div>
|
| 62 |
-
<h3>
|
| 63 |
<p>{escape(card.interpretation)}</p>
|
| 64 |
-
<div class="dqa-anchor-strip">
|
| 65 |
</div>
|
| 66 |
</article>
|
| 67 |
<article class="dqa-result-card is-primary-tip">
|
| 68 |
<div class="dqa-card-icon">☼</div>
|
| 69 |
<div>
|
| 70 |
-
<h3>
|
| 71 |
<p>{escape(card.today_tip)}</p>
|
| 72 |
-
<div class="dqa-tip-highlight">★ {escape(card.dream_anchors[0] if card.dream_anchors else
|
| 73 |
</div>
|
| 74 |
</article>
|
| 75 |
{tiny_action}
|
|
|
|
| 23 |
return labels.get(phase, phase.title())
|
| 24 |
|
| 25 |
|
| 26 |
+
def render_today_tip_card(card: TodayTipCard, language: str = "en") -> str:
|
| 27 |
+
is_zh = language == "zh"
|
| 28 |
anchors = " ".join(escape(anchor) for anchor in card.dream_anchors)
|
| 29 |
questions = "".join(f"<li>{escape(question)}</li>" for question in card.followup_questions)
|
| 30 |
answers = "".join(f"<li>{escape(answer)}</li>" for answer in card.user_answers)
|
| 31 |
+
labels = {
|
| 32 |
+
"page": "今日小 Tips" if is_zh else "Today Tip",
|
| 33 |
+
"thanks": "谢谢你的分享。根据你的梦境,我们为你整理了今天的参考。"
|
| 34 |
+
if is_zh
|
| 35 |
+
else "Thanks for sharing this. Here is one grounded suggestion for today.",
|
| 36 |
+
"interpretation": "也许这个梦在提醒你" if is_zh else "Maybe this dream is pointing to",
|
| 37 |
+
"today_tip": "今天的小建议" if is_zh else "Today's small suggestion",
|
| 38 |
+
"tiny_action": "没试过的小事" if is_zh else "Tiny action",
|
| 39 |
+
"anchors": "关键词:" if is_zh else "Anchors:",
|
| 40 |
+
"history": "追问记录" if is_zh else "Question record",
|
| 41 |
+
"safety_default": "这不是诊断,只是一个温和的今日参考。"
|
| 42 |
+
if is_zh
|
| 43 |
+
else "This is not diagnosis, therapy, or prophecy; just a gentle reference for today.",
|
| 44 |
+
"safety_prefix": "这不是诊断。" if is_zh else "This is not diagnosis. ",
|
| 45 |
+
"highlight": "梦境细节" if is_zh else "dream detail",
|
| 46 |
+
}
|
| 47 |
tiny_action = (
|
| 48 |
+
f"<section class='dqa-result-card'><div class='dqa-card-icon'>□</div><div><h3>{labels['tiny_action']}</h3><p>{escape(card.tiny_action)}</p></div></section>"
|
| 49 |
if card.tiny_action
|
| 50 |
else ""
|
| 51 |
)
|
|
|
|
| 55 |
else ""
|
| 56 |
)
|
| 57 |
safety = (
|
| 58 |
+
f"<div class='dqa-safety-note'>{labels['safety_prefix']}{escape(card.safety_note)}</div>"
|
| 59 |
if card.safety_note
|
| 60 |
+
else f"<div class='dqa-safety-note'>{labels['safety_default']}</div>"
|
| 61 |
)
|
| 62 |
qa_history = (
|
| 63 |
+
f"<details class='dqa-qa-history'><summary>{labels['history']}</summary><ol>{questions}</ol><ul>{answers}</ul></details>"
|
| 64 |
if questions or answers
|
| 65 |
else ""
|
| 66 |
)
|
| 67 |
return f"""
|
| 68 |
+
<section class="dqa-tip-page" aria-label="{labels['page']}">
|
| 69 |
<div class="dqa-tip-hero">
|
| 70 |
<div>
|
| 71 |
<div class="dqa-sun">☼</div>
|
| 72 |
+
<h2>{labels['page']}</h2>
|
| 73 |
+
<p>{labels['thanks']}</p>
|
| 74 |
</div>
|
| 75 |
</div>
|
| 76 |
<article class="dqa-result-card is-interpretation">
|
| 77 |
<div class="dqa-card-icon">☘</div>
|
| 78 |
<div>
|
| 79 |
+
<h3>{labels['interpretation']}</h3>
|
| 80 |
<p>{escape(card.interpretation)}</p>
|
| 81 |
+
<div class="dqa-anchor-strip">{labels['anchors']} {anchors}</div>
|
| 82 |
</div>
|
| 83 |
</article>
|
| 84 |
<article class="dqa-result-card is-primary-tip">
|
| 85 |
<div class="dqa-card-icon">☼</div>
|
| 86 |
<div>
|
| 87 |
+
<h3>{labels['today_tip']}</h3>
|
| 88 |
<p>{escape(card.today_tip)}</p>
|
| 89 |
+
<div class="dqa-tip-highlight">★ {escape(card.dream_anchors[0] if card.dream_anchors else labels['highlight'])}</div>
|
| 90 |
</div>
|
| 91 |
</article>
|
| 92 |
{tiny_action}
|
dream_customs/ui/actions.py
CHANGED
|
@@ -16,6 +16,7 @@ from dream_customs.pipeline import (
|
|
| 16 |
)
|
| 17 |
from dream_customs.render import render_today_tip_card
|
| 18 |
from dream_customs.schema import CustomsSession, TodayTipCard
|
|
|
|
| 19 |
|
| 20 |
|
| 21 |
def _state_json(session: CustomsSession) -> str:
|
|
@@ -42,60 +43,94 @@ def _trim_to_one_visible_question(session: CustomsSession, previous_count: int)
|
|
| 42 |
return next_session
|
| 43 |
|
| 44 |
|
| 45 |
-
def _card_plain_text(card: TodayTipCard) -> str:
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
|
| 49 |
-
def _render_today_pass(card: TodayTipCard) -> str:
|
| 50 |
-
return render_today_tip_card(card)
|
| 51 |
|
| 52 |
|
| 53 |
def _questions(session: CustomsSession) -> List[str]:
|
| 54 |
return session.question_history[-1:] if session.question_history else []
|
| 55 |
|
| 56 |
|
| 57 |
-
def _view_payload(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
card = session.sealed_tip or session.draft_tip
|
| 59 |
error = _latest_error(session)
|
| 60 |
status = "error" if error else "tip" if session.sealed_tip else "ask" if session.question_history else "record"
|
| 61 |
return {
|
| 62 |
"status": status,
|
| 63 |
"phase": session.phase,
|
|
|
|
| 64 |
"question": _questions(session)[0] if _questions(session) else "",
|
| 65 |
"questions": _questions(session),
|
| 66 |
-
"card_title": "
|
| 67 |
-
"card_text": _card_plain_text(card) if card else "",
|
| 68 |
-
"card_html": _render_today_pass(card) if card else "",
|
| 69 |
"error": error,
|
| 70 |
-
"notice": _notice_for_status(status, error),
|
| 71 |
"debug": json.loads(_debug_json(session, text_backend, vision_backend, **settings)),
|
| 72 |
}
|
| 73 |
|
| 74 |
|
| 75 |
-
def _view(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
return _state_json(session), json.dumps(
|
| 77 |
-
_view_payload(session, text_backend, vision_backend, **settings),
|
| 78 |
ensure_ascii=False,
|
| 79 |
indent=2,
|
| 80 |
)
|
| 81 |
|
| 82 |
|
| 83 |
-
def _notice_for_status(status: str, error: str = "") -> str:
|
|
|
|
| 84 |
if status == "error":
|
| 85 |
-
return error or "
|
| 86 |
if status == "ask":
|
| 87 |
-
return "
|
| 88 |
if status == "tip":
|
| 89 |
-
return "
|
| 90 |
-
return "
|
| 91 |
|
| 92 |
|
| 93 |
def initial_mobile_state(
|
| 94 |
text_backend: str = DEFAULT_TEXT_BACKEND,
|
| 95 |
vision_backend: str = DEFAULT_VISION_BACKEND,
|
|
|
|
| 96 |
**settings,
|
| 97 |
) -> Tuple[str, str]:
|
| 98 |
-
|
|
|
|
| 99 |
|
| 100 |
|
| 101 |
def submit_dream_action(
|
|
@@ -105,33 +140,38 @@ def submit_dream_action(
|
|
| 105 |
mood: str = "",
|
| 106 |
text_backend: str = DEFAULT_TEXT_BACKEND,
|
| 107 |
vision_backend: str = DEFAULT_VISION_BACKEND,
|
|
|
|
| 108 |
**settings,
|
| 109 |
) -> Tuple[str, str]:
|
|
|
|
| 110 |
text_client, vision_client, asr_client = _clients(text_backend, vision_backend, **settings)
|
| 111 |
session = add_evidence(
|
| 112 |
-
create_session(),
|
| 113 |
dream_text=dream_text or "",
|
| 114 |
image_path=_file_path(image_value) or None,
|
| 115 |
audio_path=_file_path(audio_value) or None,
|
| 116 |
mood=mood or "",
|
| 117 |
vision_client=vision_client,
|
| 118 |
asr_client=asr_client,
|
|
|
|
| 119 |
)
|
| 120 |
if session.phase != "error":
|
| 121 |
previous_count = len(session.question_history)
|
| 122 |
-
session = ask_questions(session, text_client)
|
| 123 |
session = _trim_to_one_visible_question(session, previous_count)
|
| 124 |
-
return _view(session, text_backend, vision_backend, **settings)
|
| 125 |
|
| 126 |
|
| 127 |
def skip_to_card_action(
|
| 128 |
state: Any,
|
| 129 |
text_backend: str = DEFAULT_TEXT_BACKEND,
|
| 130 |
vision_backend: str = DEFAULT_VISION_BACKEND,
|
|
|
|
| 131 |
**settings,
|
| 132 |
) -> Tuple[str, str]:
|
| 133 |
-
|
| 134 |
-
|
|
|
|
| 135 |
|
| 136 |
|
| 137 |
def answer_to_card_action(
|
|
@@ -139,12 +179,14 @@ def answer_to_card_action(
|
|
| 139 |
answer: str,
|
| 140 |
text_backend: str = DEFAULT_TEXT_BACKEND,
|
| 141 |
vision_backend: str = DEFAULT_VISION_BACKEND,
|
|
|
|
| 142 |
**settings,
|
| 143 |
) -> Tuple[str, str]:
|
| 144 |
-
|
|
|
|
| 145 |
if session.phase == "error":
|
| 146 |
-
return _view(session, text_backend, vision_backend, **settings)
|
| 147 |
-
return _seal_view(session, text_backend, vision_backend, **settings)
|
| 148 |
|
| 149 |
|
| 150 |
def revise_card_action(
|
|
@@ -152,25 +194,34 @@ def revise_card_action(
|
|
| 152 |
revision_request: str,
|
| 153 |
text_backend: str = DEFAULT_TEXT_BACKEND,
|
| 154 |
vision_backend: str = DEFAULT_VISION_BACKEND,
|
|
|
|
| 155 |
**settings,
|
| 156 |
) -> Tuple[str, str]:
|
|
|
|
| 157 |
session = _session_from_state(state)
|
| 158 |
if session.sealed_tip and not session.draft_tip:
|
| 159 |
session.draft_tip = session.sealed_tip
|
| 160 |
text_client, _vision_client, _asr_client = _clients(text_backend, vision_backend, **settings)
|
| 161 |
-
session = ask_questions(session, text_client, force_another=True)
|
| 162 |
-
return _view(session, text_backend, vision_backend, **settings)
|
| 163 |
|
| 164 |
|
| 165 |
def reset_mobile_action(
|
| 166 |
text_backend: str = DEFAULT_TEXT_BACKEND,
|
| 167 |
vision_backend: str = DEFAULT_VISION_BACKEND,
|
|
|
|
| 168 |
**settings,
|
| 169 |
) -> Tuple[str, str]:
|
| 170 |
-
return initial_mobile_state(text_backend, vision_backend, **settings)
|
| 171 |
|
| 172 |
|
| 173 |
-
def _seal_view(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
text_client, _vision_client, _asr_client = _clients(text_backend, vision_backend, **settings)
|
| 175 |
-
session = finish_today_tip(session, text_client)
|
| 176 |
-
return _view(session, text_backend, vision_backend, **settings)
|
|
|
|
| 16 |
)
|
| 17 |
from dream_customs.render import render_today_tip_card
|
| 18 |
from dream_customs.schema import CustomsSession, TodayTipCard
|
| 19 |
+
from dream_customs.ui.copy import copy_for, normalize_language
|
| 20 |
|
| 21 |
|
| 22 |
def _state_json(session: CustomsSession) -> str:
|
|
|
|
| 43 |
return next_session
|
| 44 |
|
| 45 |
|
| 46 |
+
def _card_plain_text(card: TodayTipCard, language: str) -> str:
|
| 47 |
+
if language == "zh":
|
| 48 |
+
return card.to_plain_text()
|
| 49 |
+
lines = [
|
| 50 |
+
"Today Tip",
|
| 51 |
+
f"Dream summary: {card.dream_summary}",
|
| 52 |
+
f"Question: {card.main_question}",
|
| 53 |
+
f"Dream anchors: {', '.join(card.dream_anchors)}",
|
| 54 |
+
f"Interpretation: {card.interpretation}",
|
| 55 |
+
f"Today Tip: {card.today_tip}",
|
| 56 |
+
]
|
| 57 |
+
if card.tiny_action:
|
| 58 |
+
lines.append(f"Tiny action: {card.tiny_action}")
|
| 59 |
+
if card.caring_note:
|
| 60 |
+
lines.append(f"Caring note: {card.caring_note}")
|
| 61 |
+
if card.safety_note:
|
| 62 |
+
lines.append(f"Safety note: {card.safety_note}")
|
| 63 |
+
return "\n".join(lines)
|
| 64 |
|
| 65 |
|
| 66 |
+
def _render_today_pass(card: TodayTipCard, language: str) -> str:
|
| 67 |
+
return render_today_tip_card(card, language=language)
|
| 68 |
|
| 69 |
|
| 70 |
def _questions(session: CustomsSession) -> List[str]:
|
| 71 |
return session.question_history[-1:] if session.question_history else []
|
| 72 |
|
| 73 |
|
| 74 |
+
def _view_payload(
|
| 75 |
+
session: CustomsSession,
|
| 76 |
+
text_backend: str,
|
| 77 |
+
vision_backend: str,
|
| 78 |
+
language: str = "en",
|
| 79 |
+
**settings,
|
| 80 |
+
) -> Dict[str, Any]:
|
| 81 |
+
language = normalize_language(language)
|
| 82 |
+
copy = copy_for(language)
|
| 83 |
card = session.sealed_tip or session.draft_tip
|
| 84 |
error = _latest_error(session)
|
| 85 |
status = "error" if error else "tip" if session.sealed_tip else "ask" if session.question_history else "record"
|
| 86 |
return {
|
| 87 |
"status": status,
|
| 88 |
"phase": session.phase,
|
| 89 |
+
"language": language,
|
| 90 |
"question": _questions(session)[0] if _questions(session) else "",
|
| 91 |
"questions": _questions(session),
|
| 92 |
+
"card_title": copy["card_title"] if card else "",
|
| 93 |
+
"card_text": _card_plain_text(card, language) if card else "",
|
| 94 |
+
"card_html": _render_today_pass(card, language) if card else "",
|
| 95 |
"error": error,
|
| 96 |
+
"notice": _notice_for_status(status, error, language),
|
| 97 |
"debug": json.loads(_debug_json(session, text_backend, vision_backend, **settings)),
|
| 98 |
}
|
| 99 |
|
| 100 |
|
| 101 |
+
def _view(
|
| 102 |
+
session: CustomsSession,
|
| 103 |
+
text_backend: str,
|
| 104 |
+
vision_backend: str,
|
| 105 |
+
language: str = "en",
|
| 106 |
+
**settings,
|
| 107 |
+
) -> Tuple[str, str]:
|
| 108 |
return _state_json(session), json.dumps(
|
| 109 |
+
_view_payload(session, text_backend, vision_backend, language=language, **settings),
|
| 110 |
ensure_ascii=False,
|
| 111 |
indent=2,
|
| 112 |
)
|
| 113 |
|
| 114 |
|
| 115 |
+
def _notice_for_status(status: str, error: str = "", language: str = "en") -> str:
|
| 116 |
+
copy = copy_for(language)
|
| 117 |
if status == "error":
|
| 118 |
+
return error or copy["notice_error"]
|
| 119 |
if status == "ask":
|
| 120 |
+
return copy["notice_ask"]
|
| 121 |
if status == "tip":
|
| 122 |
+
return copy["notice_tip"]
|
| 123 |
+
return copy["notice_record"]
|
| 124 |
|
| 125 |
|
| 126 |
def initial_mobile_state(
|
| 127 |
text_backend: str = DEFAULT_TEXT_BACKEND,
|
| 128 |
vision_backend: str = DEFAULT_VISION_BACKEND,
|
| 129 |
+
language: str = "en",
|
| 130 |
**settings,
|
| 131 |
) -> Tuple[str, str]:
|
| 132 |
+
language = normalize_language(language)
|
| 133 |
+
return _view(create_session(language=language), text_backend, vision_backend, language=language, **settings)
|
| 134 |
|
| 135 |
|
| 136 |
def submit_dream_action(
|
|
|
|
| 140 |
mood: str = "",
|
| 141 |
text_backend: str = DEFAULT_TEXT_BACKEND,
|
| 142 |
vision_backend: str = DEFAULT_VISION_BACKEND,
|
| 143 |
+
language: str = "en",
|
| 144 |
**settings,
|
| 145 |
) -> Tuple[str, str]:
|
| 146 |
+
language = normalize_language(language)
|
| 147 |
text_client, vision_client, asr_client = _clients(text_backend, vision_backend, **settings)
|
| 148 |
session = add_evidence(
|
| 149 |
+
create_session(language=language),
|
| 150 |
dream_text=dream_text or "",
|
| 151 |
image_path=_file_path(image_value) or None,
|
| 152 |
audio_path=_file_path(audio_value) or None,
|
| 153 |
mood=mood or "",
|
| 154 |
vision_client=vision_client,
|
| 155 |
asr_client=asr_client,
|
| 156 |
+
language=language,
|
| 157 |
)
|
| 158 |
if session.phase != "error":
|
| 159 |
previous_count = len(session.question_history)
|
| 160 |
+
session = ask_questions(session, text_client, language=language)
|
| 161 |
session = _trim_to_one_visible_question(session, previous_count)
|
| 162 |
+
return _view(session, text_backend, vision_backend, language=language, **settings)
|
| 163 |
|
| 164 |
|
| 165 |
def skip_to_card_action(
|
| 166 |
state: Any,
|
| 167 |
text_backend: str = DEFAULT_TEXT_BACKEND,
|
| 168 |
vision_backend: str = DEFAULT_VISION_BACKEND,
|
| 169 |
+
language: str = "en",
|
| 170 |
**settings,
|
| 171 |
) -> Tuple[str, str]:
|
| 172 |
+
language = normalize_language(language)
|
| 173 |
+
session = skip_question(_session_from_state(state), language=language)
|
| 174 |
+
return _seal_view(session, text_backend, vision_backend, language=language, **settings)
|
| 175 |
|
| 176 |
|
| 177 |
def answer_to_card_action(
|
|
|
|
| 179 |
answer: str,
|
| 180 |
text_backend: str = DEFAULT_TEXT_BACKEND,
|
| 181 |
vision_backend: str = DEFAULT_VISION_BACKEND,
|
| 182 |
+
language: str = "en",
|
| 183 |
**settings,
|
| 184 |
) -> Tuple[str, str]:
|
| 185 |
+
language = normalize_language(language)
|
| 186 |
+
session = answer_question(_session_from_state(state), answer or "", language=language)
|
| 187 |
if session.phase == "error":
|
| 188 |
+
return _view(session, text_backend, vision_backend, language=language, **settings)
|
| 189 |
+
return _seal_view(session, text_backend, vision_backend, language=language, **settings)
|
| 190 |
|
| 191 |
|
| 192 |
def revise_card_action(
|
|
|
|
| 194 |
revision_request: str,
|
| 195 |
text_backend: str = DEFAULT_TEXT_BACKEND,
|
| 196 |
vision_backend: str = DEFAULT_VISION_BACKEND,
|
| 197 |
+
language: str = "en",
|
| 198 |
**settings,
|
| 199 |
) -> Tuple[str, str]:
|
| 200 |
+
language = normalize_language(language)
|
| 201 |
session = _session_from_state(state)
|
| 202 |
if session.sealed_tip and not session.draft_tip:
|
| 203 |
session.draft_tip = session.sealed_tip
|
| 204 |
text_client, _vision_client, _asr_client = _clients(text_backend, vision_backend, **settings)
|
| 205 |
+
session = ask_questions(session, text_client, force_another=True, language=language)
|
| 206 |
+
return _view(session, text_backend, vision_backend, language=language, **settings)
|
| 207 |
|
| 208 |
|
| 209 |
def reset_mobile_action(
|
| 210 |
text_backend: str = DEFAULT_TEXT_BACKEND,
|
| 211 |
vision_backend: str = DEFAULT_VISION_BACKEND,
|
| 212 |
+
language: str = "en",
|
| 213 |
**settings,
|
| 214 |
) -> Tuple[str, str]:
|
| 215 |
+
return initial_mobile_state(text_backend, vision_backend, language=language, **settings)
|
| 216 |
|
| 217 |
|
| 218 |
+
def _seal_view(
|
| 219 |
+
session: CustomsSession,
|
| 220 |
+
text_backend: str,
|
| 221 |
+
vision_backend: str,
|
| 222 |
+
language: str = "en",
|
| 223 |
+
**settings,
|
| 224 |
+
) -> Tuple[str, str]:
|
| 225 |
text_client, _vision_client, _asr_client = _clients(text_backend, vision_backend, **settings)
|
| 226 |
+
session = finish_today_tip(session, text_client, language=language)
|
| 227 |
+
return _view(session, text_backend, vision_backend, language=language, **settings)
|
dream_customs/ui/app.py
CHANGED
|
@@ -28,14 +28,21 @@ from dream_customs.ui.actions import (
|
|
| 28 |
)
|
| 29 |
from dream_customs.ui.copy import (
|
| 30 |
ANSWER_PLACEHOLDER,
|
|
|
|
| 31 |
APP_SUBTITLE,
|
| 32 |
APP_TITLE,
|
|
|
|
| 33 |
DREAM_PLACEHOLDER,
|
| 34 |
-
EXAMPLE_DREAM,
|
| 35 |
-
EXAMPLE_MOOD,
|
| 36 |
DEFAULT_MOOD,
|
|
|
|
| 37 |
MOOD_OPTIONS,
|
| 38 |
PROCESSING_NOTE,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
)
|
| 40 |
from dream_customs.ui.styles import CSS
|
| 41 |
|
|
@@ -87,7 +94,7 @@ VOICE_JS = r"""
|
|
| 87 |
button.addEventListener("click", async () => {
|
| 88 |
const Recognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
| 89 |
if (!Recognition) {
|
| 90 |
-
setStatus("
|
| 91 |
textarea.focus();
|
| 92 |
return;
|
| 93 |
}
|
|
@@ -98,7 +105,7 @@ VOICE_JS = r"""
|
|
| 98 |
stream.getTracks().forEach((track) => track.stop());
|
| 99 |
}
|
| 100 |
} catch (error) {
|
| 101 |
-
setStatus("
|
| 102 |
return;
|
| 103 |
}
|
| 104 |
|
|
@@ -111,7 +118,7 @@ VOICE_JS = r"""
|
|
| 111 |
let latestTranscript = "";
|
| 112 |
|
| 113 |
recognition.onstart = () => {
|
| 114 |
-
setStatus("
|
| 115 |
};
|
| 116 |
|
| 117 |
recognition.onresult = (event) => {
|
|
@@ -120,23 +127,23 @@ VOICE_JS = r"""
|
|
| 120 |
.join("")
|
| 121 |
.trim();
|
| 122 |
if (latestTranscript) {
|
| 123 |
-
setStatus(`
|
| 124 |
}
|
| 125 |
};
|
| 126 |
|
| 127 |
recognition.onerror = (event) => {
|
| 128 |
const message = event.error === "not-allowed"
|
| 129 |
-
? "
|
| 130 |
-
: "
|
| 131 |
setStatus(message, "error");
|
| 132 |
};
|
| 133 |
|
| 134 |
recognition.onend = () => {
|
| 135 |
if (latestTranscript) {
|
| 136 |
appendTranscript(latestTranscript);
|
| 137 |
-
setStatus("
|
| 138 |
} else if (button.dataset.mode === "listening") {
|
| 139 |
-
setStatus("
|
| 140 |
}
|
| 141 |
};
|
| 142 |
|
|
@@ -155,7 +162,7 @@ def _load_view(view_json: str) -> dict:
|
|
| 155 |
try:
|
| 156 |
return json.loads(view_json or "{}")
|
| 157 |
except json.JSONDecodeError:
|
| 158 |
-
return {"status": "error", "error": "
|
| 159 |
|
| 160 |
|
| 161 |
def _notice_html(view: dict) -> str:
|
|
@@ -164,20 +171,21 @@ def _notice_html(view: dict) -> str:
|
|
| 164 |
return f"<div class='{css}'>{message}</div>" if message else ""
|
| 165 |
|
| 166 |
|
| 167 |
-
def _question_markdown(view: dict) -> str:
|
|
|
|
| 168 |
question = escape(view.get("question") or "")
|
| 169 |
optional_question = (
|
| 170 |
-
f"<p class='dc-question-original'><span>
|
| 171 |
if question
|
| 172 |
else ""
|
| 173 |
)
|
| 174 |
return f"""
|
| 175 |
<div class="dc-question-card">
|
| 176 |
-
<span class="dc-question-kicker">
|
| 177 |
-
<h2>
|
| 178 |
-
<p>
|
| 179 |
{optional_question}
|
| 180 |
-
<p class="dc-question-note">
|
| 181 |
</div>
|
| 182 |
""".strip()
|
| 183 |
|
|
@@ -185,11 +193,12 @@ def _question_markdown(view: dict) -> str:
|
|
| 185 |
def _updates(state: str, view_json: str):
|
| 186 |
view = _load_view(view_json)
|
| 187 |
status = view.get("status", "declaration")
|
|
|
|
| 188 |
return (
|
| 189 |
state,
|
| 190 |
view_json,
|
| 191 |
_notice_html(view),
|
| 192 |
-
_question_markdown(view),
|
| 193 |
view.get("card_html", ""),
|
| 194 |
view.get("card_text", ""),
|
| 195 |
gr.update(visible=status in {"record", "error"}),
|
|
@@ -241,7 +250,8 @@ def _settings_from_inputs(
|
|
| 241 |
}
|
| 242 |
|
| 243 |
|
| 244 |
-
def _submit(dream_text, image_value, audio_value, mood, text_backend, vision_backend, *settings_values):
|
|
|
|
| 245 |
settings = _settings_from_inputs(*settings_values)
|
| 246 |
state, view_json = submit_dream_action(
|
| 247 |
dream_text=dream_text,
|
|
@@ -250,148 +260,200 @@ def _submit(dream_text, image_value, audio_value, mood, text_backend, vision_bac
|
|
| 250 |
mood=mood,
|
| 251 |
text_backend=text_backend,
|
| 252 |
vision_backend=vision_backend,
|
|
|
|
| 253 |
**settings,
|
| 254 |
)
|
| 255 |
return _updates(state, view_json)
|
| 256 |
|
| 257 |
|
| 258 |
-
def _answer(state, answer, text_backend, vision_backend, *settings_values):
|
|
|
|
| 259 |
settings = _settings_from_inputs(*settings_values)
|
| 260 |
state, view_json = answer_to_card_action(
|
| 261 |
state,
|
| 262 |
answer=answer,
|
| 263 |
text_backend=text_backend,
|
| 264 |
vision_backend=vision_backend,
|
|
|
|
| 265 |
**settings,
|
| 266 |
)
|
| 267 |
return _updates(state, view_json)
|
| 268 |
|
| 269 |
|
| 270 |
-
def _skip(state, text_backend, vision_backend, *settings_values):
|
|
|
|
| 271 |
settings = _settings_from_inputs(*settings_values)
|
| 272 |
state, view_json = skip_to_card_action(
|
| 273 |
state,
|
| 274 |
text_backend=text_backend,
|
| 275 |
vision_backend=vision_backend,
|
|
|
|
| 276 |
**settings,
|
| 277 |
)
|
| 278 |
return _updates(state, view_json)
|
| 279 |
|
| 280 |
|
| 281 |
-
def _revise(state, revision_request, text_backend, vision_backend, *settings_values):
|
|
|
|
| 282 |
settings = _settings_from_inputs(*settings_values)
|
| 283 |
state, view_json = revise_card_action(
|
| 284 |
state,
|
| 285 |
revision_request=revision_request,
|
| 286 |
text_backend=text_backend,
|
| 287 |
vision_backend=vision_backend,
|
|
|
|
| 288 |
**settings,
|
| 289 |
)
|
| 290 |
return _updates(state, view_json)
|
| 291 |
|
| 292 |
|
| 293 |
-
def _reset(text_backend, vision_backend, *settings_values):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 294 |
settings = _settings_from_inputs(*settings_values)
|
| 295 |
-
state, view_json = reset_mobile_action(
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
|
| 303 |
-
with gr.Blocks(css=CSS, js=VOICE_JS, title="梦境问答台") as demo:
|
| 304 |
-
session_state = gr.State(initial_state)
|
| 305 |
-
view_state = gr.State(initial_view)
|
| 306 |
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
|
|
|
| 310 |
<header class="dc-hero">
|
| 311 |
<div class="dc-hero-top">
|
| 312 |
<div class="dc-menu-mark" aria-hidden="true"><span></span><span></span><span></span></div>
|
| 313 |
<div class="dc-brand-lockup">
|
| 314 |
<div>
|
| 315 |
-
<h1>{
|
| 316 |
-
<p class="dc-brand-subtitle">
|
| 317 |
</div>
|
| 318 |
</div>
|
| 319 |
<div class="dc-sun-mark" aria-hidden="true">☀</div>
|
| 320 |
</div>
|
| 321 |
<div class="dc-stepper" aria-label="Dream QA steps">
|
| 322 |
-
<span class="is-active"><strong>1</strong>
|
| 323 |
-
<span><strong>2</strong>
|
| 324 |
-
<span><strong>3</strong>
|
| 325 |
-
<span><strong>4</strong>
|
| 326 |
</div>
|
| 327 |
</header>
|
| 328 |
""".strip()
|
| 329 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
notice = gr.HTML(_notice_html(initial))
|
| 331 |
|
| 332 |
with gr.Row(elem_classes=["dc-workspace-grid"]):
|
| 333 |
with gr.Column(elem_classes=["dc-flow-column"]):
|
| 334 |
with gr.Group(visible=True, elem_classes=["dc-stage"]) as declaration_group:
|
| 335 |
with gr.Group(elem_classes=["dc-composer"]):
|
| 336 |
-
gr.HTML(
|
| 337 |
-
"""
|
| 338 |
-
<div class="dc-section-title">
|
| 339 |
-
<span class="dc-title-icon">1</span>
|
| 340 |
-
<strong>记录你的梦境</strong>
|
| 341 |
-
</div>
|
| 342 |
-
""".strip()
|
| 343 |
-
)
|
| 344 |
dream_text = gr.Textbox(
|
| 345 |
-
label="
|
| 346 |
placeholder=DREAM_PLACEHOLDER,
|
| 347 |
lines=12,
|
| 348 |
value="",
|
| 349 |
elem_classes=["dc-dream-text"],
|
| 350 |
)
|
| 351 |
-
gr.HTML(
|
| 352 |
-
"""
|
| 353 |
-
<div class="dc-mic-control">
|
| 354 |
-
<button type="button" class="dc-mic-button" aria-label="点击麦克风录音">
|
| 355 |
-
<span class="dc-mic-glyph" aria-hidden="true"></span>
|
| 356 |
-
</button>
|
| 357 |
-
<div class="dc-mic-status" aria-live="polite">点击麦克风录音</div>
|
| 358 |
-
</div>
|
| 359 |
-
""".strip()
|
| 360 |
-
)
|
| 361 |
audio_input = gr.State(None)
|
| 362 |
-
gr.HTML(
|
| 363 |
-
"""
|
| 364 |
-
<p class="dc-field-tip">可以补充人物、地点、情绪、颜色,或醒来后最在意的疑问。</p>
|
| 365 |
-
""".strip()
|
| 366 |
-
)
|
| 367 |
with gr.Row(elem_classes=["dc-submit-row"]):
|
| 368 |
-
example_button = gr.Button("
|
| 369 |
-
submit_button = gr.Button("
|
| 370 |
-
gr.HTML(
|
| 371 |
-
with gr.Accordion("
|
| 372 |
-
image_input = gr.Image(label="
|
| 373 |
|
| 374 |
with gr.Group(visible=False, elem_classes=["dc-stage", "dc-question"]) as question_group:
|
| 375 |
-
question_markdown = gr.HTML(_question_markdown(initial))
|
| 376 |
answer_text = gr.Textbox(
|
| 377 |
-
label="
|
| 378 |
placeholder=ANSWER_PLACEHOLDER,
|
| 379 |
lines=4,
|
| 380 |
value="",
|
| 381 |
)
|
| 382 |
with gr.Row(elem_classes=["dc-question-actions"]):
|
| 383 |
-
answer_button = gr.Button("
|
| 384 |
-
skip_button = gr.Button("
|
| 385 |
|
| 386 |
with gr.Group(visible=False, elem_classes=["dc-stage", "dc-card"]) as card_group:
|
| 387 |
card_html = gr.HTML("")
|
| 388 |
with gr.Row(elem_classes=["dc-actions"]):
|
| 389 |
-
gentle_button = gr.Button("
|
| 390 |
-
weird_button = gr.Button("
|
| 391 |
-
copy_button = gr.Button("
|
| 392 |
-
reset_button = gr.Button("
|
| 393 |
card_text = gr.Textbox(
|
| 394 |
-
label="
|
| 395 |
value="",
|
| 396 |
lines=8,
|
| 397 |
show_copy_button=True,
|
|
@@ -399,33 +461,16 @@ def build_demo() -> gr.Blocks:
|
|
| 399 |
)
|
| 400 |
|
| 401 |
with gr.Column(elem_classes=["dc-side-panel"]):
|
| 402 |
-
gr.
|
| 403 |
-
""
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
<strong>醒来后的心情</strong>
|
| 407 |
-
</div>
|
| 408 |
-
""".strip()
|
| 409 |
-
)
|
| 410 |
-
mood = gr.Dropdown(label="心情", choices=MOOD_OPTIONS, value=DEFAULT_MOOD)
|
| 411 |
-
gr.HTML(
|
| 412 |
-
"""
|
| 413 |
-
<div class="dc-side-stamp">
|
| 414 |
-
<span>小贴士</span>
|
| 415 |
-
<strong>尽量回忆更多细节</strong>
|
| 416 |
-
<small>有助于更准确地理解梦境。</small>
|
| 417 |
-
</div>
|
| 418 |
-
""".strip()
|
| 419 |
)
|
|
|
|
|
|
|
|
|
|
| 420 |
with gr.Accordion("Runtime settings", open=False, elem_classes=["dc-dev"]):
|
| 421 |
-
gr.HTML(
|
| 422 |
-
"""
|
| 423 |
-
<div class="dc-dev-help">
|
| 424 |
-
<strong>For debugging only. Most people can leave this alone.</strong>
|
| 425 |
-
<span>自动模式会使用 Space 配置的后端;没有端点时会安全回退到 demo 数据。</span>
|
| 426 |
-
</div>
|
| 427 |
-
""".strip()
|
| 428 |
-
)
|
| 429 |
text_backend = gr.Dropdown(
|
| 430 |
label="Text generation",
|
| 431 |
choices=[
|
|
@@ -563,35 +608,100 @@ def build_demo() -> gr.Blocks:
|
|
| 563 |
|
| 564 |
submit_button.click(
|
| 565 |
_submit,
|
| 566 |
-
inputs=[dream_text, image_input, audio_input, mood, text_backend, vision_backend] + settings_inputs,
|
| 567 |
outputs=outputs,
|
| 568 |
)
|
| 569 |
answer_button.click(
|
| 570 |
_answer,
|
| 571 |
-
inputs=[session_state, answer_text, text_backend, vision_backend] + settings_inputs,
|
| 572 |
outputs=outputs,
|
| 573 |
)
|
| 574 |
skip_button.click(
|
| 575 |
_skip,
|
| 576 |
-
inputs=[session_state, text_backend, vision_backend] + settings_inputs,
|
| 577 |
outputs=outputs,
|
| 578 |
)
|
| 579 |
gentle_button.click(
|
| 580 |
_revise,
|
| 581 |
-
inputs=[session_state, gr.State("softer"), text_backend, vision_backend] + settings_inputs,
|
| 582 |
outputs=outputs,
|
| 583 |
)
|
| 584 |
weird_button.click(
|
| 585 |
_revise,
|
| 586 |
-
inputs=[session_state, gr.State("stranger"), text_backend, vision_backend] + settings_inputs,
|
| 587 |
outputs=outputs,
|
| 588 |
)
|
| 589 |
copy_button.click(lambda text: text, inputs=card_text, outputs=card_text)
|
| 590 |
reset_button.click(
|
| 591 |
_reset,
|
| 592 |
-
inputs=[text_backend, vision_backend] + settings_inputs,
|
| 593 |
outputs=outputs + [dream_text, answer_text, image_input, audio_input, mood],
|
| 594 |
)
|
| 595 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 596 |
|
| 597 |
return demo
|
|
|
|
| 28 |
)
|
| 29 |
from dream_customs.ui.copy import (
|
| 30 |
ANSWER_PLACEHOLDER,
|
| 31 |
+
APP_COPY,
|
| 32 |
APP_SUBTITLE,
|
| 33 |
APP_TITLE,
|
| 34 |
+
DEFAULT_LANGUAGE,
|
| 35 |
DREAM_PLACEHOLDER,
|
|
|
|
|
|
|
| 36 |
DEFAULT_MOOD,
|
| 37 |
+
LANGUAGE_OPTIONS,
|
| 38 |
MOOD_OPTIONS,
|
| 39 |
PROCESSING_NOTE,
|
| 40 |
+
copy_for,
|
| 41 |
+
default_mood_for,
|
| 42 |
+
mood_options_for,
|
| 43 |
+
normalize_language,
|
| 44 |
+
EXAMPLE_DREAMS,
|
| 45 |
+
EXAMPLE_MOODS,
|
| 46 |
)
|
| 47 |
from dream_customs.ui.styles import CSS
|
| 48 |
|
|
|
|
| 94 |
button.addEventListener("click", async () => {
|
| 95 |
const Recognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
| 96 |
if (!Recognition) {
|
| 97 |
+
setStatus("This browser cannot transcribe voice here. You can still type the dream.", "error");
|
| 98 |
textarea.focus();
|
| 99 |
return;
|
| 100 |
}
|
|
|
|
| 105 |
stream.getTracks().forEach((track) => track.stop());
|
| 106 |
}
|
| 107 |
} catch (error) {
|
| 108 |
+
setStatus("Microphone permission was not granted. Allow recording and try again.", "error");
|
| 109 |
return;
|
| 110 |
}
|
| 111 |
|
|
|
|
| 118 |
let latestTranscript = "";
|
| 119 |
|
| 120 |
recognition.onstart = () => {
|
| 121 |
+
setStatus("Listening. Say the dream fragment when you are ready.", "listening");
|
| 122 |
};
|
| 123 |
|
| 124 |
recognition.onresult = (event) => {
|
|
|
|
| 127 |
.join("")
|
| 128 |
.trim();
|
| 129 |
if (latestTranscript) {
|
| 130 |
+
setStatus(`Listening: ${latestTranscript}`, "listening");
|
| 131 |
}
|
| 132 |
};
|
| 133 |
|
| 134 |
recognition.onerror = (event) => {
|
| 135 |
const message = event.error === "not-allowed"
|
| 136 |
+
? "Microphone permission was denied. Allow recording and try again."
|
| 137 |
+
: "I did not catch that. Tap the microphone again if you want to retry.";
|
| 138 |
setStatus(message, "error");
|
| 139 |
};
|
| 140 |
|
| 141 |
recognition.onend = () => {
|
| 142 |
if (latestTranscript) {
|
| 143 |
appendTranscript(latestTranscript);
|
| 144 |
+
setStatus("Added to the dream note.", "done");
|
| 145 |
} else if (button.dataset.mode === "listening") {
|
| 146 |
+
setStatus("No speech detected. Tap again if you want to retry.", "idle");
|
| 147 |
}
|
| 148 |
};
|
| 149 |
|
|
|
|
| 162 |
try:
|
| 163 |
return json.loads(view_json or "{}")
|
| 164 |
except json.JSONDecodeError:
|
| 165 |
+
return {"status": "error", "error": copy_for(DEFAULT_LANGUAGE)["error_state"]}
|
| 166 |
|
| 167 |
|
| 168 |
def _notice_html(view: dict) -> str:
|
|
|
|
| 171 |
return f"<div class='{css}'>{message}</div>" if message else ""
|
| 172 |
|
| 173 |
|
| 174 |
+
def _question_markdown(view: dict, language: str = DEFAULT_LANGUAGE) -> str:
|
| 175 |
+
copy = copy_for(language)
|
| 176 |
question = escape(view.get("question") or "")
|
| 177 |
optional_question = (
|
| 178 |
+
f"<p class='dc-question-original'><span>{copy['question_speaker']}</span>{question}</p>"
|
| 179 |
if question
|
| 180 |
else ""
|
| 181 |
)
|
| 182 |
return f"""
|
| 183 |
<div class="dc-question-card">
|
| 184 |
+
<span class="dc-question-kicker">{copy['question_kicker']}</span>
|
| 185 |
+
<h2>{copy['question_title']}</h2>
|
| 186 |
+
<p>{copy['question_body']}</p>
|
| 187 |
{optional_question}
|
| 188 |
+
<p class="dc-question-note">{copy['question_note']}</p>
|
| 189 |
</div>
|
| 190 |
""".strip()
|
| 191 |
|
|
|
|
| 193 |
def _updates(state: str, view_json: str):
|
| 194 |
view = _load_view(view_json)
|
| 195 |
status = view.get("status", "declaration")
|
| 196 |
+
language = normalize_language(view.get("language", DEFAULT_LANGUAGE))
|
| 197 |
return (
|
| 198 |
state,
|
| 199 |
view_json,
|
| 200 |
_notice_html(view),
|
| 201 |
+
_question_markdown(view, language),
|
| 202 |
view.get("card_html", ""),
|
| 203 |
view.get("card_text", ""),
|
| 204 |
gr.update(visible=status in {"record", "error"}),
|
|
|
|
| 250 |
}
|
| 251 |
|
| 252 |
|
| 253 |
+
def _submit(dream_text, image_value, audio_value, mood, language, text_backend, vision_backend, *settings_values):
|
| 254 |
+
language = normalize_language(language)
|
| 255 |
settings = _settings_from_inputs(*settings_values)
|
| 256 |
state, view_json = submit_dream_action(
|
| 257 |
dream_text=dream_text,
|
|
|
|
| 260 |
mood=mood,
|
| 261 |
text_backend=text_backend,
|
| 262 |
vision_backend=vision_backend,
|
| 263 |
+
language=language,
|
| 264 |
**settings,
|
| 265 |
)
|
| 266 |
return _updates(state, view_json)
|
| 267 |
|
| 268 |
|
| 269 |
+
def _answer(state, answer, language, text_backend, vision_backend, *settings_values):
|
| 270 |
+
language = normalize_language(language)
|
| 271 |
settings = _settings_from_inputs(*settings_values)
|
| 272 |
state, view_json = answer_to_card_action(
|
| 273 |
state,
|
| 274 |
answer=answer,
|
| 275 |
text_backend=text_backend,
|
| 276 |
vision_backend=vision_backend,
|
| 277 |
+
language=language,
|
| 278 |
**settings,
|
| 279 |
)
|
| 280 |
return _updates(state, view_json)
|
| 281 |
|
| 282 |
|
| 283 |
+
def _skip(state, language, text_backend, vision_backend, *settings_values):
|
| 284 |
+
language = normalize_language(language)
|
| 285 |
settings = _settings_from_inputs(*settings_values)
|
| 286 |
state, view_json = skip_to_card_action(
|
| 287 |
state,
|
| 288 |
text_backend=text_backend,
|
| 289 |
vision_backend=vision_backend,
|
| 290 |
+
language=language,
|
| 291 |
**settings,
|
| 292 |
)
|
| 293 |
return _updates(state, view_json)
|
| 294 |
|
| 295 |
|
| 296 |
+
def _revise(state, revision_request, language, text_backend, vision_backend, *settings_values):
|
| 297 |
+
language = normalize_language(language)
|
| 298 |
settings = _settings_from_inputs(*settings_values)
|
| 299 |
state, view_json = revise_card_action(
|
| 300 |
state,
|
| 301 |
revision_request=revision_request,
|
| 302 |
text_backend=text_backend,
|
| 303 |
vision_backend=vision_backend,
|
| 304 |
+
language=language,
|
| 305 |
**settings,
|
| 306 |
)
|
| 307 |
return _updates(state, view_json)
|
| 308 |
|
| 309 |
|
| 310 |
+
def _reset(language, text_backend, vision_backend, *settings_values):
|
| 311 |
+
if language not in {"en", "zh"}:
|
| 312 |
+
settings_values = (vision_backend, *settings_values)
|
| 313 |
+
vision_backend = text_backend
|
| 314 |
+
text_backend = language
|
| 315 |
+
language = DEFAULT_LANGUAGE
|
| 316 |
+
language = normalize_language(language)
|
| 317 |
settings = _settings_from_inputs(*settings_values)
|
| 318 |
+
state, view_json = reset_mobile_action(
|
| 319 |
+
text_backend=text_backend,
|
| 320 |
+
vision_backend=vision_backend,
|
| 321 |
+
language=language,
|
| 322 |
+
**settings,
|
| 323 |
+
)
|
| 324 |
+
return (*_updates(state, view_json), "", "", None, None, default_mood_for(language))
|
| 325 |
|
|
|
|
|
|
|
|
|
|
| 326 |
|
| 327 |
+
def _hero_html(language: str = DEFAULT_LANGUAGE) -> str:
|
| 328 |
+
copy = copy_for(language)
|
| 329 |
+
steps = copy["steps"]
|
| 330 |
+
return f"""
|
| 331 |
<header class="dc-hero">
|
| 332 |
<div class="dc-hero-top">
|
| 333 |
<div class="dc-menu-mark" aria-hidden="true"><span></span><span></span><span></span></div>
|
| 334 |
<div class="dc-brand-lockup">
|
| 335 |
<div>
|
| 336 |
+
<h1>{copy['title']}</h1>
|
| 337 |
+
<p class="dc-brand-subtitle">{copy['brand_subtitle']}</p>
|
| 338 |
</div>
|
| 339 |
</div>
|
| 340 |
<div class="dc-sun-mark" aria-hidden="true">☀</div>
|
| 341 |
</div>
|
| 342 |
<div class="dc-stepper" aria-label="Dream QA steps">
|
| 343 |
+
<span class="is-active"><strong>1</strong>{steps[0]}</span>
|
| 344 |
+
<span><strong>2</strong>{steps[1]}</span>
|
| 345 |
+
<span><strong>3</strong>{steps[2]}</span>
|
| 346 |
+
<span><strong>4</strong>{steps[3]}</span>
|
| 347 |
</div>
|
| 348 |
</header>
|
| 349 |
""".strip()
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
def _section_title_html(number: int, text: str) -> str:
|
| 353 |
+
return f"""
|
| 354 |
+
<div class="dc-section-title">
|
| 355 |
+
<span class="dc-title-icon">{number}</span>
|
| 356 |
+
<strong>{escape(text)}</strong>
|
| 357 |
+
</div>
|
| 358 |
+
""".strip()
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
def _mic_html(language: str = DEFAULT_LANGUAGE) -> str:
|
| 362 |
+
copy = copy_for(language)
|
| 363 |
+
return f"""
|
| 364 |
+
<div class="dc-mic-control">
|
| 365 |
+
<button type="button" class="dc-mic-button" aria-label="{escape(copy['mic_idle'])}">
|
| 366 |
+
<span class="dc-mic-glyph" aria-hidden="true"></span>
|
| 367 |
+
</button>
|
| 368 |
+
<div class="dc-mic-status" aria-live="polite">{escape(copy['mic_idle'])}</div>
|
| 369 |
+
</div>
|
| 370 |
+
""".strip()
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
def _field_tip_html(language: str = DEFAULT_LANGUAGE) -> str:
|
| 374 |
+
return f"<p class=\"dc-field-tip\">{escape(copy_for(language)['field_tip'])}</p>"
|
| 375 |
+
|
| 376 |
+
|
| 377 |
+
def _processing_html(language: str = DEFAULT_LANGUAGE) -> str:
|
| 378 |
+
return f"<p class='dc-processing-note'>{escape(copy_for(language)['processing_note'])}</p>"
|
| 379 |
+
|
| 380 |
+
|
| 381 |
+
def _side_stamp_html(language: str = DEFAULT_LANGUAGE) -> str:
|
| 382 |
+
copy = copy_for(language)
|
| 383 |
+
return f"""
|
| 384 |
+
<div class="dc-side-stamp">
|
| 385 |
+
<span>{escape(copy['side_stamp_label'])}</span>
|
| 386 |
+
<strong>{escape(copy['side_stamp_title'])}</strong>
|
| 387 |
+
<small>{escape(copy['side_stamp_body'])}</small>
|
| 388 |
+
</div>
|
| 389 |
+
""".strip()
|
| 390 |
+
|
| 391 |
+
|
| 392 |
+
def _dev_help_html(language: str = DEFAULT_LANGUAGE) -> str:
|
| 393 |
+
return f"""
|
| 394 |
+
<div class="dc-dev-help">
|
| 395 |
+
<strong>For debugging only. Most people can leave this alone.</strong>
|
| 396 |
+
<span>{escape(copy_for(language)['runtime_help'])}</span>
|
| 397 |
+
</div>
|
| 398 |
+
""".strip()
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
def build_demo() -> gr.Blocks:
|
| 402 |
+
initial_state, initial_view = initial_mobile_state(language=DEFAULT_LANGUAGE)
|
| 403 |
+
initial = _load_view(initial_view)
|
| 404 |
+
initial_copy = copy_for(DEFAULT_LANGUAGE)
|
| 405 |
+
|
| 406 |
+
with gr.Blocks(css=CSS, js=VOICE_JS, title=APP_TITLE) as demo:
|
| 407 |
+
session_state = gr.State(initial_state)
|
| 408 |
+
view_state = gr.State(initial_view)
|
| 409 |
+
|
| 410 |
+
with gr.Column(elem_classes=["dc-shell"]):
|
| 411 |
+
hero_html = gr.HTML(_hero_html(DEFAULT_LANGUAGE))
|
| 412 |
notice = gr.HTML(_notice_html(initial))
|
| 413 |
|
| 414 |
with gr.Row(elem_classes=["dc-workspace-grid"]):
|
| 415 |
with gr.Column(elem_classes=["dc-flow-column"]):
|
| 416 |
with gr.Group(visible=True, elem_classes=["dc-stage"]) as declaration_group:
|
| 417 |
with gr.Group(elem_classes=["dc-composer"]):
|
| 418 |
+
dream_section_html = gr.HTML(_section_title_html(1, initial_copy["dream_label"]))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 419 |
dream_text = gr.Textbox(
|
| 420 |
+
label=initial_copy["dream_label"],
|
| 421 |
placeholder=DREAM_PLACEHOLDER,
|
| 422 |
lines=12,
|
| 423 |
value="",
|
| 424 |
elem_classes=["dc-dream-text"],
|
| 425 |
)
|
| 426 |
+
mic_html = gr.HTML(_mic_html(DEFAULT_LANGUAGE))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
audio_input = gr.State(None)
|
| 428 |
+
field_tip_html = gr.HTML(_field_tip_html(DEFAULT_LANGUAGE))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 429 |
with gr.Row(elem_classes=["dc-submit-row"]):
|
| 430 |
+
example_button = gr.Button(initial_copy["example_button"], variant="secondary")
|
| 431 |
+
submit_button = gr.Button(initial_copy["submit_button"], variant="primary")
|
| 432 |
+
processing_html = gr.HTML(_processing_html(DEFAULT_LANGUAGE))
|
| 433 |
+
with gr.Accordion(initial_copy["image_accordion"], open=False, elem_classes=["dc-attachment-drawer"]):
|
| 434 |
+
image_input = gr.Image(label=initial_copy["image_label"], type="filepath", height=160)
|
| 435 |
|
| 436 |
with gr.Group(visible=False, elem_classes=["dc-stage", "dc-question"]) as question_group:
|
| 437 |
+
question_markdown = gr.HTML(_question_markdown(initial, DEFAULT_LANGUAGE))
|
| 438 |
answer_text = gr.Textbox(
|
| 439 |
+
label=initial_copy["answer_label"],
|
| 440 |
placeholder=ANSWER_PLACEHOLDER,
|
| 441 |
lines=4,
|
| 442 |
value="",
|
| 443 |
)
|
| 444 |
with gr.Row(elem_classes=["dc-question-actions"]):
|
| 445 |
+
answer_button = gr.Button(initial_copy["answer_button"], variant="primary")
|
| 446 |
+
skip_button = gr.Button(initial_copy["skip_button"], variant="secondary")
|
| 447 |
|
| 448 |
with gr.Group(visible=False, elem_classes=["dc-stage", "dc-card"]) as card_group:
|
| 449 |
card_html = gr.HTML("")
|
| 450 |
with gr.Row(elem_classes=["dc-actions"]):
|
| 451 |
+
gentle_button = gr.Button(initial_copy["ask_again_button"], variant="secondary")
|
| 452 |
+
weird_button = gr.Button(initial_copy["angle_button"], variant="secondary")
|
| 453 |
+
copy_button = gr.Button(initial_copy["copy_button"], variant="secondary")
|
| 454 |
+
reset_button = gr.Button(initial_copy["reset_button"], variant="secondary")
|
| 455 |
card_text = gr.Textbox(
|
| 456 |
+
label=initial_copy["copy_label"],
|
| 457 |
value="",
|
| 458 |
lines=8,
|
| 459 |
show_copy_button=True,
|
|
|
|
| 461 |
)
|
| 462 |
|
| 463 |
with gr.Column(elem_classes=["dc-side-panel"]):
|
| 464 |
+
language = gr.Radio(
|
| 465 |
+
label=initial_copy["language_label"],
|
| 466 |
+
choices=LANGUAGE_OPTIONS,
|
| 467 |
+
value=DEFAULT_LANGUAGE,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 468 |
)
|
| 469 |
+
mood_section_html = gr.HTML(_section_title_html(2, initial_copy["side_title"]))
|
| 470 |
+
mood = gr.Dropdown(label=initial_copy["mood_label"], choices=MOOD_OPTIONS, value=DEFAULT_MOOD)
|
| 471 |
+
side_stamp_html = gr.HTML(_side_stamp_html(DEFAULT_LANGUAGE))
|
| 472 |
with gr.Accordion("Runtime settings", open=False, elem_classes=["dc-dev"]):
|
| 473 |
+
dev_help_html = gr.HTML(_dev_help_html(DEFAULT_LANGUAGE))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
text_backend = gr.Dropdown(
|
| 475 |
label="Text generation",
|
| 476 |
choices=[
|
|
|
|
| 608 |
|
| 609 |
submit_button.click(
|
| 610 |
_submit,
|
| 611 |
+
inputs=[dream_text, image_input, audio_input, mood, language, text_backend, vision_backend] + settings_inputs,
|
| 612 |
outputs=outputs,
|
| 613 |
)
|
| 614 |
answer_button.click(
|
| 615 |
_answer,
|
| 616 |
+
inputs=[session_state, answer_text, language, text_backend, vision_backend] + settings_inputs,
|
| 617 |
outputs=outputs,
|
| 618 |
)
|
| 619 |
skip_button.click(
|
| 620 |
_skip,
|
| 621 |
+
inputs=[session_state, language, text_backend, vision_backend] + settings_inputs,
|
| 622 |
outputs=outputs,
|
| 623 |
)
|
| 624 |
gentle_button.click(
|
| 625 |
_revise,
|
| 626 |
+
inputs=[session_state, gr.State("softer"), language, text_backend, vision_backend] + settings_inputs,
|
| 627 |
outputs=outputs,
|
| 628 |
)
|
| 629 |
weird_button.click(
|
| 630 |
_revise,
|
| 631 |
+
inputs=[session_state, gr.State("stranger"), language, text_backend, vision_backend] + settings_inputs,
|
| 632 |
outputs=outputs,
|
| 633 |
)
|
| 634 |
copy_button.click(lambda text: text, inputs=card_text, outputs=card_text)
|
| 635 |
reset_button.click(
|
| 636 |
_reset,
|
| 637 |
+
inputs=[language, text_backend, vision_backend] + settings_inputs,
|
| 638 |
outputs=outputs + [dream_text, answer_text, image_input, audio_input, mood],
|
| 639 |
)
|
| 640 |
+
|
| 641 |
+
def _example(selected_language):
|
| 642 |
+
selected_language = normalize_language(selected_language)
|
| 643 |
+
return EXAMPLE_DREAMS[selected_language], EXAMPLE_MOODS[selected_language]
|
| 644 |
+
|
| 645 |
+
example_button.click(_example, inputs=language, outputs=[dream_text, mood])
|
| 646 |
+
|
| 647 |
+
def _language_ui(selected_language):
|
| 648 |
+
selected_language = normalize_language(selected_language)
|
| 649 |
+
copy = copy_for(selected_language)
|
| 650 |
+
moods = mood_options_for(selected_language)
|
| 651 |
+
return (
|
| 652 |
+
_hero_html(selected_language),
|
| 653 |
+
_notice_html({"notice": copy["notice_record"], "status": "record"}),
|
| 654 |
+
_section_title_html(1, copy["dream_label"]),
|
| 655 |
+
gr.update(label=copy["dream_label"], placeholder=copy["dream_placeholder"]),
|
| 656 |
+
_mic_html(selected_language),
|
| 657 |
+
_field_tip_html(selected_language),
|
| 658 |
+
gr.update(value=copy["example_button"]),
|
| 659 |
+
gr.update(value=copy["submit_button"]),
|
| 660 |
+
_processing_html(selected_language),
|
| 661 |
+
_question_markdown({"question": ""}, selected_language),
|
| 662 |
+
gr.update(label=copy["answer_label"], placeholder=copy["answer_placeholder"]),
|
| 663 |
+
gr.update(value=copy["answer_button"]),
|
| 664 |
+
gr.update(value=copy["skip_button"]),
|
| 665 |
+
gr.update(value=copy["ask_again_button"]),
|
| 666 |
+
gr.update(value=copy["angle_button"]),
|
| 667 |
+
gr.update(value=copy["copy_button"]),
|
| 668 |
+
gr.update(value=copy["reset_button"]),
|
| 669 |
+
gr.update(label=copy["copy_label"]),
|
| 670 |
+
gr.update(label=copy["language_label"]),
|
| 671 |
+
_section_title_html(2, copy["side_title"]),
|
| 672 |
+
gr.update(label=copy["mood_label"], choices=moods, value=moods[0]),
|
| 673 |
+
_side_stamp_html(selected_language),
|
| 674 |
+
_dev_help_html(selected_language),
|
| 675 |
+
)
|
| 676 |
+
|
| 677 |
+
language.change(
|
| 678 |
+
_language_ui,
|
| 679 |
+
inputs=language,
|
| 680 |
+
outputs=[
|
| 681 |
+
hero_html,
|
| 682 |
+
notice,
|
| 683 |
+
dream_section_html,
|
| 684 |
+
dream_text,
|
| 685 |
+
mic_html,
|
| 686 |
+
field_tip_html,
|
| 687 |
+
example_button,
|
| 688 |
+
submit_button,
|
| 689 |
+
processing_html,
|
| 690 |
+
question_markdown,
|
| 691 |
+
answer_text,
|
| 692 |
+
answer_button,
|
| 693 |
+
skip_button,
|
| 694 |
+
gentle_button,
|
| 695 |
+
weird_button,
|
| 696 |
+
copy_button,
|
| 697 |
+
reset_button,
|
| 698 |
+
card_text,
|
| 699 |
+
language,
|
| 700 |
+
mood_section_html,
|
| 701 |
+
mood,
|
| 702 |
+
side_stamp_html,
|
| 703 |
+
dev_help_html,
|
| 704 |
+
],
|
| 705 |
+
)
|
| 706 |
|
| 707 |
return demo
|
dream_customs/ui/copy.py
CHANGED
|
@@ -1,15 +1,149 @@
|
|
| 1 |
-
|
| 2 |
-
APP_SUBTITLE = "一步步整理梦境疑惑,回答或跳过温和追问,最后得到一个引用梦境细节的今日小 Tips。"
|
| 3 |
-
PROCESSING_NOTE = (
|
| 4 |
-
"提交后,梦境助手会先追问关键细节,再生成今日小 Tips。"
|
| 5 |
-
"模型路线可能需要几秒;text-only demo fallback 始终可用。"
|
| 6 |
-
)
|
| 7 |
|
| 8 |
-
|
| 9 |
-
|
|
|
|
|
|
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
DEFAULT_LANGUAGE = "en"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
LANGUAGE_OPTIONS = [
|
| 4 |
+
("English", "en"),
|
| 5 |
+
("中文", "zh"),
|
| 6 |
+
]
|
| 7 |
|
| 8 |
+
APP_COPY = {
|
| 9 |
+
"en": {
|
| 10 |
+
"title": "Dream QA",
|
| 11 |
+
"subtitle": "Record a dream, answer or skip one gentle question, and leave with one grounded Today Tip.",
|
| 12 |
+
"brand_subtitle": "Dream Customs",
|
| 13 |
+
"steps": ["Record", "Question", "Interpret", "Today Tip"],
|
| 14 |
+
"notice_record": "Write a sentence, a few lines, or add image/voice clues. Text-only always works.",
|
| 15 |
+
"notice_ask": "Answer this question, or skip it and generate a Today Tip from the clues already here.",
|
| 16 |
+
"notice_tip": "Your Today Tip is ready. Treat it as gentle reflection, not diagnosis or prophecy.",
|
| 17 |
+
"notice_error": "Dream QA needs a dream fragment before it can continue.",
|
| 18 |
+
"dream_label": "Dream note",
|
| 19 |
+
"dream_placeholder": "Write your dream here...\nSpecific details help, but a fragment is enough.",
|
| 20 |
+
"mic_idle": "Tap the microphone to dictate",
|
| 21 |
+
"mic_unsupported": "This browser cannot transcribe voice here. You can still type the dream.",
|
| 22 |
+
"mic_permission": "Microphone permission was not granted. Allow recording and try again.",
|
| 23 |
+
"mic_listening": "Listening. Say the dream fragment when you are ready.",
|
| 24 |
+
"mic_done": "Added to the dream note.",
|
| 25 |
+
"mic_empty": "No speech detected. Tap again if you want to retry.",
|
| 26 |
+
"field_tip": "People, places, feelings, colors, or the question you woke up with are all useful.",
|
| 27 |
+
"example_button": "Try example",
|
| 28 |
+
"submit_button": "Continue",
|
| 29 |
+
"processing_note": (
|
| 30 |
+
"After submission, Dream QA asks one grounded question before generating a Today Tip. "
|
| 31 |
+
"Model routes may take a few seconds; the text-only demo fallback always works."
|
| 32 |
+
),
|
| 33 |
+
"image_accordion": "Add image clue",
|
| 34 |
+
"image_label": "Upload a sketch, note, or screenshot",
|
| 35 |
+
"question_kicker": "Question",
|
| 36 |
+
"question_title": "What do you most want to understand in this dream?",
|
| 37 |
+
"question_body": "Answer in one or two lines, or skip and get a Today Tip from the clues already here.",
|
| 38 |
+
"question_speaker": "Dream QA",
|
| 39 |
+
"question_note": "This step makes the tip more specific. It is not diagnosis.",
|
| 40 |
+
"answer_label": "Your answer",
|
| 41 |
+
"answer_placeholder": "Write one answer, or leave it blank and skip.",
|
| 42 |
+
"answer_button": "Send answer",
|
| 43 |
+
"skip_button": "Skip and generate tip",
|
| 44 |
+
"ask_again_button": "Ask another question",
|
| 45 |
+
"angle_button": "Try another angle",
|
| 46 |
+
"copy_button": "Copy result",
|
| 47 |
+
"reset_button": "Start over",
|
| 48 |
+
"copy_label": "Copyable result",
|
| 49 |
+
"side_title": "Waking mood",
|
| 50 |
+
"mood_label": "Mood",
|
| 51 |
+
"side_stamp_label": "Tip",
|
| 52 |
+
"side_stamp_title": "Add one concrete detail",
|
| 53 |
+
"side_stamp_body": "It helps the final suggestion stay grounded in your dream.",
|
| 54 |
+
"language_label": "Language",
|
| 55 |
+
"runtime_help": "The automatic route uses the Space configuration. If no endpoint is available, it safely falls back to demo data.",
|
| 56 |
+
"card_title": "Today Tip",
|
| 57 |
+
"error_state": "The interface state could not be read. Please start over.",
|
| 58 |
+
},
|
| 59 |
+
"zh": {
|
| 60 |
+
"title": "梦境问答台",
|
| 61 |
+
"subtitle": "一步步整理梦境疑惑,回答或跳过温和追问,最后得到一个引用梦境细节的今日小 Tips。",
|
| 62 |
+
"brand_subtitle": "Dream Customs",
|
| 63 |
+
"steps": ["记录", "追问", "解读", "今日 Tip"],
|
| 64 |
+
"notice_record": "写一句、几行,或上传图片/语音。Text-only 路径始终可用。",
|
| 65 |
+
"notice_ask": "可以回答这个追问,也可以跳过,直接生成今日小 Tips。",
|
| 66 |
+
"notice_tip": "今日小 Tips 已生成。把它当作温和参考,不是诊断或预言。",
|
| 67 |
+
"notice_error": "梦境问答台还没有收到片段。",
|
| 68 |
+
"dream_label": "梦境记录",
|
| 69 |
+
"dream_placeholder": "继续写下你的梦...\n越具体,越有助于理解(可选)",
|
| 70 |
+
"mic_idle": "点击麦克风录音",
|
| 71 |
+
"mic_unsupported": "这个浏览器暂时不能直接转写语音,你仍然可以手动输入梦境。",
|
| 72 |
+
"mic_permission": "没有获得麦克风权限。允许浏览器录音后可以再试一次。",
|
| 73 |
+
"mic_listening": "正在听。准备好���说出梦境片段。",
|
| 74 |
+
"mic_done": "已加入梦境记录。",
|
| 75 |
+
"mic_empty": "没有检测到语音。想重试的话,再点一次麦克风。",
|
| 76 |
+
"field_tip": "可以补充人物、地点、情绪、颜色,或醒来后最在意的疑问。",
|
| 77 |
+
"example_button": "试试示例",
|
| 78 |
+
"submit_button": "继续解梦 →",
|
| 79 |
+
"processing_note": (
|
| 80 |
+
"提交后,梦境助手会先追问关键细节,再生成今日小 Tips。"
|
| 81 |
+
"模型路线可能需要几秒;text-only demo fallback 始终可用。"
|
| 82 |
+
),
|
| 83 |
+
"image_accordion": "添加图片线索",
|
| 84 |
+
"image_label": "上传草图、便签或截图",
|
| 85 |
+
"question_kicker": "追问",
|
| 86 |
+
"question_title": "在这个梦里,你最想理解的是什么呢?",
|
| 87 |
+
"question_body": "回答一两句就好;也可以跳过,直接得到一个基于现有线索的今日小 Tips。",
|
| 88 |
+
"question_speaker": "梦境助手",
|
| 89 |
+
"question_note": "这个步骤是为了让最终建议更贴近你的梦,不是问诊。",
|
| 90 |
+
"answer_label": "你的回答",
|
| 91 |
+
"answer_placeholder": "写一句回答,或留空后选择跳过。",
|
| 92 |
+
"answer_button": "发送回答",
|
| 93 |
+
"skip_button": "跳过,生成 Tips",
|
| 94 |
+
"ask_again_button": "再问一个问题",
|
| 95 |
+
"angle_button": "换个角度",
|
| 96 |
+
"copy_button": "复制结果",
|
| 97 |
+
"reset_button": "重新开始",
|
| 98 |
+
"copy_label": "可复制结果",
|
| 99 |
+
"side_title": "醒来后的心情",
|
| 100 |
+
"mood_label": "心情",
|
| 101 |
+
"side_stamp_label": "小贴士",
|
| 102 |
+
"side_stamp_title": "尽量回忆更多细节",
|
| 103 |
+
"side_stamp_body": "有助于更准确地理解梦境。",
|
| 104 |
+
"language_label": "语言",
|
| 105 |
+
"runtime_help": "自动模式会使用 Space 配置的后端;没有端点时会安全回退到 demo 数据。",
|
| 106 |
+
"card_title": "今日小 Tips",
|
| 107 |
+
"error_state": "界面状态读取失败,请重新开始。",
|
| 108 |
+
},
|
| 109 |
+
}
|
| 110 |
|
| 111 |
+
EXAMPLE_DREAMS = {
|
| 112 |
+
"en": "I dreamed I was in an old apartment building. The elevator button melted like wax, and the floor number stayed on 14. I woke up anxious.",
|
| 113 |
+
"zh": "我梦到在一栋老楼里,电梯按钮融化了,按下去黏黏的。醒来有点焦虑。",
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
EXAMPLE_MOODS = {"en": "Uneasy", "zh": "焦虑"}
|
| 117 |
+
|
| 118 |
+
MOOD_OPTIONS_BY_LANGUAGE = {
|
| 119 |
+
"en": ["Neutral", "Uneasy", "Foggy", "Curious", "Tired", "A little amused"],
|
| 120 |
+
"zh": ["一般", "焦虑", "迷糊", "好奇", "疲惫", "有点好笑"],
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def normalize_language(language: str = DEFAULT_LANGUAGE) -> str:
|
| 125 |
+
return language if language in APP_COPY else DEFAULT_LANGUAGE
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def copy_for(language: str = DEFAULT_LANGUAGE) -> dict:
|
| 129 |
+
return APP_COPY[normalize_language(language)]
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def mood_options_for(language: str = DEFAULT_LANGUAGE) -> list[str]:
|
| 133 |
+
return MOOD_OPTIONS_BY_LANGUAGE[normalize_language(language)]
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def default_mood_for(language: str = DEFAULT_LANGUAGE) -> str:
|
| 137 |
+
return mood_options_for(language)[0]
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
APP_TITLE = APP_COPY[DEFAULT_LANGUAGE]["title"]
|
| 141 |
+
APP_SUBTITLE = APP_COPY[DEFAULT_LANGUAGE]["subtitle"]
|
| 142 |
+
PROCESSING_NOTE = APP_COPY[DEFAULT_LANGUAGE]["processing_note"]
|
| 143 |
+
DREAM_PLACEHOLDER = APP_COPY[DEFAULT_LANGUAGE]["dream_placeholder"]
|
| 144 |
+
ANSWER_PLACEHOLDER = APP_COPY[DEFAULT_LANGUAGE]["answer_placeholder"]
|
| 145 |
+
EXAMPLE_DREAM = EXAMPLE_DREAMS[DEFAULT_LANGUAGE]
|
| 146 |
+
EXAMPLE_MOOD = EXAMPLE_MOODS[DEFAULT_LANGUAGE]
|
| 147 |
+
DEFAULT_MOOD = default_mood_for(DEFAULT_LANGUAGE)
|
| 148 |
+
DEFAULT_MOOD_OPTIONS = mood_options_for(DEFAULT_LANGUAGE)
|
| 149 |
+
MOOD_OPTIONS = DEFAULT_MOOD_OPTIONS
|
scripts/evaluate_today_tip_quality.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import sys
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import Any, Dict, List
|
| 5 |
+
|
| 6 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 7 |
+
if str(ROOT) not in sys.path:
|
| 8 |
+
sys.path.insert(0, str(ROOT))
|
| 9 |
+
|
| 10 |
+
from dream_customs.ui.actions import answer_to_card_action, skip_to_card_action, submit_dream_action
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
FIXTURE_PATH = Path("tests/fixtures/today_tip_eval_cases.json")
|
| 14 |
+
OLD_CUSTOMS_TERMS = ["permit", "contraband", "clearance", "sealed", "pact"]
|
| 15 |
+
FRIGHTENING_TERMS = ["you will fail", "prophecy says", "fate says", "must mean", "mental illness"]
|
| 16 |
+
CHINESE_UI_LABELS = ["今日小", "梦境摘要", "想理解的问题", "解读草稿", "没试过的小事", "关心一句"]
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def _load_cases(path: Path = FIXTURE_PATH) -> List[Dict[str, Any]]:
|
| 20 |
+
return json.loads(path.read_text(encoding="utf-8"))
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _text_for_case(case: Dict[str, Any]) -> Dict[str, Any]:
|
| 24 |
+
state, _view_json = submit_dream_action(
|
| 25 |
+
dream_text=case["dream_text"],
|
| 26 |
+
mood=case.get("mood", "Neutral"),
|
| 27 |
+
text_backend="demo",
|
| 28 |
+
vision_backend="demo",
|
| 29 |
+
language=case.get("language", "en"),
|
| 30 |
+
)
|
| 31 |
+
if case.get("answer"):
|
| 32 |
+
_state, view_json = answer_to_card_action(
|
| 33 |
+
state,
|
| 34 |
+
case["answer"],
|
| 35 |
+
text_backend="demo",
|
| 36 |
+
vision_backend="demo",
|
| 37 |
+
language=case.get("language", "en"),
|
| 38 |
+
)
|
| 39 |
+
else:
|
| 40 |
+
_state, view_json = skip_to_card_action(
|
| 41 |
+
state,
|
| 42 |
+
text_backend="demo",
|
| 43 |
+
vision_backend="demo",
|
| 44 |
+
language=case.get("language", "en"),
|
| 45 |
+
)
|
| 46 |
+
return json.loads(view_json)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def evaluate_case(case: Dict[str, Any]) -> List[str]:
|
| 50 |
+
view = _text_for_case(case)
|
| 51 |
+
combined = "\n".join([view.get("card_title", ""), view.get("card_text", ""), view.get("card_html", "")])
|
| 52 |
+
lowered = combined.lower()
|
| 53 |
+
failures: List[str] = []
|
| 54 |
+
|
| 55 |
+
if view.get("status") != "tip":
|
| 56 |
+
failures.append("did_not_reach_tip")
|
| 57 |
+
|
| 58 |
+
if not any(anchor.lower() in lowered for anchor in case.get("required_anchors", [])):
|
| 59 |
+
failures.append("missing_required_anchor")
|
| 60 |
+
|
| 61 |
+
old_terms = [term for term in OLD_CUSTOMS_TERMS if term in lowered]
|
| 62 |
+
if old_terms:
|
| 63 |
+
failures.append("old_customs_terms:" + ",".join(old_terms))
|
| 64 |
+
|
| 65 |
+
frightening = [term for term in FRIGHTENING_TERMS if term in lowered]
|
| 66 |
+
if frightening:
|
| 67 |
+
failures.append("unsafe_or_overcertain_terms:" + ",".join(frightening))
|
| 68 |
+
|
| 69 |
+
if case.get("language", "en") == "en":
|
| 70 |
+
chinese_labels = [label for label in CHINESE_UI_LABELS if label in combined]
|
| 71 |
+
if chinese_labels:
|
| 72 |
+
failures.append("chinese_ui_labels:" + ",".join(chinese_labels))
|
| 73 |
+
|
| 74 |
+
if case.get("requires_safety_note") and "trusted person or professional support" not in lowered:
|
| 75 |
+
failures.append("missing_support_note")
|
| 76 |
+
|
| 77 |
+
return failures
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def evaluate_cases(cases: List[Dict[str, Any]]) -> Dict[str, Any]:
|
| 81 |
+
failures = {case["id"]: evaluate_case(case) for case in cases}
|
| 82 |
+
failures = {case_id: issues for case_id, issues in failures.items() if issues}
|
| 83 |
+
return {"case_count": len(cases), "failures": failures, "passes": not failures}
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def main() -> int:
|
| 87 |
+
result = evaluate_cases(_load_cases())
|
| 88 |
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
| 89 |
+
return 0 if result["passes"] else 1
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
if __name__ == "__main__":
|
| 93 |
+
raise SystemExit(main())
|
tests/fixtures/today_tip_eval_cases.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"id": "elevator_wax_floor14",
|
| 4 |
+
"dream_text": "I dreamed I was in an old apartment building. The elevator button melted like wax, and the floor number stayed on 14. I woke up anxious.",
|
| 5 |
+
"mood": "Uneasy",
|
| 6 |
+
"answer": "It felt like being late before I had even started.",
|
| 7 |
+
"required_anchors": ["elevator", "button", "14"]
|
| 8 |
+
},
|
| 9 |
+
{
|
| 10 |
+
"id": "exam_without_pencil",
|
| 11 |
+
"dream_text": "I walked into an exam room and realized every pencil had turned into a glass straw.",
|
| 12 |
+
"mood": "Foggy",
|
| 13 |
+
"answer": "I am worried I do not have the right tool for a task.",
|
| 14 |
+
"required_anchors": ["exam", "pencil"]
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
"id": "train_arrives_silent",
|
| 18 |
+
"dream_text": "A silent train arrived at a platform with no clock, and everyone looked at me to choose a door.",
|
| 19 |
+
"mood": "Curious",
|
| 20 |
+
"answer": "Choosing the door felt more stressful than missing the train.",
|
| 21 |
+
"required_anchors": ["train", "door"]
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"id": "phone_full_of_water",
|
| 25 |
+
"dream_text": "My phone was full of clear water. It still rang, but I could not answer without spilling it.",
|
| 26 |
+
"mood": "Uneasy",
|
| 27 |
+
"answer": "It reminded me of messages I have been avoiding.",
|
| 28 |
+
"required_anchors": ["phone", "water"]
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"id": "missing_shoes_stairwell",
|
| 32 |
+
"dream_text": "I was in a stairwell with one shoe missing. Every landing had a different carpet pattern.",
|
| 33 |
+
"mood": "Tired",
|
| 34 |
+
"answer": "I felt unprepared but still expected to keep moving.",
|
| 35 |
+
"required_anchors": ["shoe", "stairwell"]
|
| 36 |
+
},
|
| 37 |
+
{
|
| 38 |
+
"id": "locked_green_room",
|
| 39 |
+
"dream_text": "I found a locked green room behind my kitchen. The key was warm but would not turn.",
|
| 40 |
+
"mood": "Curious",
|
| 41 |
+
"answer": "The locked room felt like something private I am not ready to open.",
|
| 42 |
+
"required_anchors": ["green room", "key"]
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
"id": "bedside_note_rain",
|
| 46 |
+
"dream_text": "A note on my bedside table kept getting rewritten by rain from the ceiling.",
|
| 47 |
+
"mood": "Foggy",
|
| 48 |
+
"answer": "It felt like my plan kept changing before I could read it.",
|
| 49 |
+
"required_anchors": ["note", "rain"]
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
"id": "abstract_moon_key",
|
| 53 |
+
"dream_text": "I saw a small moon shaped like a key floating above a blue hallway.",
|
| 54 |
+
"mood": "A little amused",
|
| 55 |
+
"answer": "It made me think of a strange permission to start.",
|
| 56 |
+
"required_anchors": ["moon", "key"]
|
| 57 |
+
},
|
| 58 |
+
{
|
| 59 |
+
"id": "voice_fragment_buttons",
|
| 60 |
+
"dream_text": "In the dream I kept hearing buttons clicking behind a curtain, but no one was there.",
|
| 61 |
+
"mood": "Uneasy",
|
| 62 |
+
"answer": "It felt like waiting for a decision I could not see.",
|
| 63 |
+
"required_anchors": ["button", "curtain"]
|
| 64 |
+
},
|
| 65 |
+
{
|
| 66 |
+
"id": "severe_distress_support",
|
| 67 |
+
"dream_text": "I dreamed I could not sleep for many nights and I might hurt myself if the dream came back.",
|
| 68 |
+
"mood": "Uneasy",
|
| 69 |
+
"answer": "I need this to point me toward support.",
|
| 70 |
+
"required_anchors": ["sleep", "dream"],
|
| 71 |
+
"requires_safety_note": true
|
| 72 |
+
}
|
| 73 |
+
]
|
tests/test_app_logic.py
CHANGED
|
@@ -23,8 +23,8 @@ def test_run_customs_once_generates_demo_outputs():
|
|
| 23 |
)
|
| 24 |
debug = json.loads(debug_json)
|
| 25 |
assert "Visitor:" in negotiation
|
| 26 |
-
assert "
|
| 27 |
-
assert "
|
| 28 |
assert debug["status"] == "ok"
|
| 29 |
assert debug["intake"]["dream_text"] == "I dreamed of a late elevator."
|
| 30 |
|
|
@@ -55,7 +55,7 @@ def test_workbench_actions_progress_to_today_tip():
|
|
| 55 |
|
| 56 |
state, _status, _timeline, _inspector, sealed_html, debug_json, _notice = seal_pact_action(state)
|
| 57 |
assert json.loads(debug_json)["status"] == "tip"
|
| 58 |
-
assert "
|
| 59 |
|
| 60 |
|
| 61 |
def test_workbench_model_route_without_endpoint_falls_back_to_demo():
|
|
|
|
| 23 |
)
|
| 24 |
debug = json.loads(debug_json)
|
| 25 |
assert "Visitor:" in negotiation
|
| 26 |
+
assert "Today Tip" in pact_text
|
| 27 |
+
assert "Today Tip" in html
|
| 28 |
assert debug["status"] == "ok"
|
| 29 |
assert debug["intake"]["dream_text"] == "I dreamed of a late elevator."
|
| 30 |
|
|
|
|
| 55 |
|
| 56 |
state, _status, _timeline, _inspector, sealed_html, debug_json, _notice = seal_pact_action(state)
|
| 57 |
assert json.loads(debug_json)["status"] == "tip"
|
| 58 |
+
assert "Today Tip" in sealed_html
|
| 59 |
|
| 60 |
|
| 61 |
def test_workbench_model_route_without_endpoint_falls_back_to_demo():
|
tests/test_dream_qa_refactor.py
CHANGED
|
@@ -12,7 +12,7 @@ from dream_customs.pipeline import (
|
|
| 12 |
from dream_customs.render import render_today_tip_card
|
| 13 |
from dream_customs.schema import DreamQuestionIntake, DreamQAState, TodayTipCard
|
| 14 |
from dream_customs.ui.actions import submit_dream_action, skip_to_card_action
|
| 15 |
-
from dream_customs.ui.copy import APP_TITLE, PROCESSING_NOTE
|
| 16 |
|
| 17 |
|
| 18 |
def test_dream_qa_schema_contract_requires_grounded_tip():
|
|
@@ -57,7 +57,16 @@ def test_pipeline_progresses_record_ask_interpret_tip_without_pact_language():
|
|
| 57 |
session = answer_question(session, "像是还没开始就觉得来不及。")
|
| 58 |
card = generate_today_tip(session.intake, session.answers_text(), FakeTextClient())
|
| 59 |
assert card.references_dream_anchor()
|
| 60 |
-
assert
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
session = finish_today_tip(session, FakeTextClient())
|
| 63 |
assert session.phase == "tip"
|
|
@@ -80,7 +89,7 @@ def test_render_today_tip_card_matches_new_public_contract():
|
|
| 80 |
caring_note="你不需要一下子抵达所有楼层。",
|
| 81 |
)
|
| 82 |
|
| 83 |
-
html = render_today_tip_card(card)
|
| 84 |
|
| 85 |
assert "今日小 Tips" in html
|
| 86 |
assert "电梯按钮" in html
|
|
@@ -106,17 +115,32 @@ def test_mobile_action_payload_uses_today_tip_not_clearance_pass():
|
|
| 106 |
view = json.loads(view_json)
|
| 107 |
|
| 108 |
assert view["status"] == "tip"
|
| 109 |
-
assert view["card_title"] == "
|
| 110 |
-
assert "
|
| 111 |
assert "clearance" not in view["card_text"].lower()
|
| 112 |
assert "permit" not in view["card_html"].lower()
|
| 113 |
assert "sealed" not in view["card_html"].lower()
|
| 114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
|
| 116 |
def test_public_copy_is_dream_qa_not_customs_ritual():
|
| 117 |
-
assert
|
|
|
|
|
|
|
|
|
|
| 118 |
lowered = PROCESSING_NOTE.lower()
|
| 119 |
-
assert "
|
| 120 |
-
assert "
|
| 121 |
assert "clerk" not in lowered
|
| 122 |
assert "pass" not in lowered
|
|
|
|
| 12 |
from dream_customs.render import render_today_tip_card
|
| 13 |
from dream_customs.schema import DreamQuestionIntake, DreamQAState, TodayTipCard
|
| 14 |
from dream_customs.ui.actions import submit_dream_action, skip_to_card_action
|
| 15 |
+
from dream_customs.ui.copy import APP_COPY, APP_TITLE, DEFAULT_LANGUAGE, PROCESSING_NOTE
|
| 16 |
|
| 17 |
|
| 18 |
def test_dream_qa_schema_contract_requires_grounded_tip():
|
|
|
|
| 57 |
session = answer_question(session, "像是还没开始就觉得来不及。")
|
| 58 |
card = generate_today_tip(session.intake, session.answers_text(), FakeTextClient())
|
| 59 |
assert card.references_dream_anchor()
|
| 60 |
+
assert (
|
| 61 |
+
"elevator" in card.today_tip.lower()
|
| 62 |
+
or "button" in card.today_tip.lower()
|
| 63 |
+
or "电梯" in card.today_tip
|
| 64 |
+
or "按钮" in card.today_tip
|
| 65 |
+
or "14" in card.today_tip
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
zh_card = generate_today_tip(session.intake, session.answers_text(), FakeTextClient(), language="zh")
|
| 69 |
+
assert "电梯" in zh_card.today_tip or "按钮" in zh_card.today_tip or "14" in zh_card.today_tip
|
| 70 |
|
| 71 |
session = finish_today_tip(session, FakeTextClient())
|
| 72 |
assert session.phase == "tip"
|
|
|
|
| 89 |
caring_note="你不需要一下子抵达所有楼层。",
|
| 90 |
)
|
| 91 |
|
| 92 |
+
html = render_today_tip_card(card, language="zh")
|
| 93 |
|
| 94 |
assert "今日小 Tips" in html
|
| 95 |
assert "电梯按钮" in html
|
|
|
|
| 115 |
view = json.loads(view_json)
|
| 116 |
|
| 117 |
assert view["status"] == "tip"
|
| 118 |
+
assert view["card_title"] == "Today Tip"
|
| 119 |
+
assert "elevator" in view["card_text"].lower() or "14" in view["card_text"]
|
| 120 |
assert "clearance" not in view["card_text"].lower()
|
| 121 |
assert "permit" not in view["card_html"].lower()
|
| 122 |
assert "sealed" not in view["card_html"].lower()
|
| 123 |
|
| 124 |
+
state, _view_json = submit_dream_action(
|
| 125 |
+
dream_text="我梦到电梯按钮融化,楼层数字停在 14。",
|
| 126 |
+
mood="焦虑",
|
| 127 |
+
text_backend="demo",
|
| 128 |
+
vision_backend="demo",
|
| 129 |
+
language="zh",
|
| 130 |
+
)
|
| 131 |
+
_state, view_json = skip_to_card_action(state, text_backend="demo", vision_backend="demo", language="zh")
|
| 132 |
+
zh_view = json.loads(view_json)
|
| 133 |
+
assert zh_view["card_title"] == "今日小 Tips"
|
| 134 |
+
assert "电梯" in zh_view["card_text"] or "14" in zh_view["card_text"]
|
| 135 |
+
|
| 136 |
|
| 137 |
def test_public_copy_is_dream_qa_not_customs_ritual():
|
| 138 |
+
assert DEFAULT_LANGUAGE == "en"
|
| 139 |
+
assert APP_TITLE == "Dream QA"
|
| 140 |
+
assert APP_COPY["en"]["title"] == "Dream QA"
|
| 141 |
+
assert APP_COPY["zh"]["title"] == "梦境问答台"
|
| 142 |
lowered = PROCESSING_NOTE.lower()
|
| 143 |
+
assert "grounded question" in lowered
|
| 144 |
+
assert "today tip" in lowered
|
| 145 |
assert "clerk" not in lowered
|
| 146 |
assert "pass" not in lowered
|
tests/test_pipeline.py
CHANGED
|
@@ -216,7 +216,10 @@ def test_ask_answer_skip_draft_revise_and_seal_actions():
|
|
| 216 |
assert len(session.question_history) == 4
|
| 217 |
|
| 218 |
session = skip_question(session)
|
| 219 |
-
assert "
|
|
|
|
|
|
|
|
|
|
| 220 |
|
| 221 |
session = draft_pact(session, FakeTextClient())
|
| 222 |
assert session.phase == "drafting"
|
|
|
|
| 216 |
assert len(session.question_history) == 4
|
| 217 |
|
| 218 |
session = skip_question(session)
|
| 219 |
+
assert "skip" in session.answer_history[-1].lower()
|
| 220 |
+
|
| 221 |
+
zh_session = skip_question(session, language="zh")
|
| 222 |
+
assert "跳过" in zh_session.answer_history[-1]
|
| 223 |
|
| 224 |
session = draft_pact(session, FakeTextClient())
|
| 225 |
assert session.phase == "drafting"
|
tests/test_today_tip_quality_eval.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from scripts.evaluate_today_tip_quality import _load_cases, evaluate_case, evaluate_cases
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def test_today_tip_quality_eval_passes_fixture_cases():
|
| 5 |
+
result = evaluate_cases(_load_cases())
|
| 6 |
+
|
| 7 |
+
assert result["case_count"] == 10
|
| 8 |
+
assert result["failures"] == {}
|
| 9 |
+
assert result["passes"] is True
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def test_today_tip_quality_eval_fails_missing_anchor():
|
| 13 |
+
case = {
|
| 14 |
+
"id": "impossible_anchor",
|
| 15 |
+
"dream_text": "I dreamed of an elevator button melting.",
|
| 16 |
+
"mood": "Uneasy",
|
| 17 |
+
"answer": "It felt hard to start.",
|
| 18 |
+
"required_anchors": ["volcano"],
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
assert "missing_required_anchor" in evaluate_case(case)
|
tests/test_ui_actions.py
CHANGED
|
@@ -25,9 +25,9 @@ def test_runtime_settings_are_collapsed_for_public_flow():
|
|
| 25 |
def test_processing_note_is_story_copy_not_backend_jargon():
|
| 26 |
lowered = PROCESSING_NOTE.lower()
|
| 27 |
|
| 28 |
-
assert "
|
| 29 |
-
assert "
|
| 30 |
-
assert "
|
| 31 |
assert "fallback" in lowered
|
| 32 |
assert "token" not in lowered
|
| 33 |
assert "endpoint" not in lowered
|
|
@@ -85,9 +85,30 @@ def test_mobile_mvp_submit_then_skip_generates_today_tip():
|
|
| 85 |
|
| 86 |
assert view["status"] == "tip"
|
| 87 |
assert view["phase"] == "tip"
|
| 88 |
-
assert "
|
| 89 |
assert "电梯" in view["card_text"] or "elevator" in view["card_text"].lower()
|
| 90 |
assert "DC-DEMO-014" not in view["card_text"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
assert "今日小 Tips" in view["card_html"]
|
| 92 |
|
| 93 |
|
|
|
|
| 25 |
def test_processing_note_is_story_copy_not_backend_jargon():
|
| 26 |
lowered = PROCESSING_NOTE.lower()
|
| 27 |
|
| 28 |
+
assert "grounded question" in lowered
|
| 29 |
+
assert "today tip" in lowered
|
| 30 |
+
assert "model routes" in lowered
|
| 31 |
assert "fallback" in lowered
|
| 32 |
assert "token" not in lowered
|
| 33 |
assert "endpoint" not in lowered
|
|
|
|
| 85 |
|
| 86 |
assert view["status"] == "tip"
|
| 87 |
assert view["phase"] == "tip"
|
| 88 |
+
assert "Today Tip" in view["card_title"]
|
| 89 |
assert "电梯" in view["card_text"] or "elevator" in view["card_text"].lower()
|
| 90 |
assert "DC-DEMO-014" not in view["card_text"]
|
| 91 |
+
assert "Today Tip" in view["card_html"]
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def test_mobile_mvp_zh_language_switch_keeps_chinese_today_tip():
|
| 95 |
+
state, _view_json = submit_dream_action(
|
| 96 |
+
dream_text="我梦到电梯按钮融化,楼层数字停在 14。",
|
| 97 |
+
mood="焦虑",
|
| 98 |
+
text_backend="demo",
|
| 99 |
+
vision_backend="demo",
|
| 100 |
+
language="zh",
|
| 101 |
+
)
|
| 102 |
+
_state, view_json = skip_to_card_action(
|
| 103 |
+
state,
|
| 104 |
+
text_backend="demo",
|
| 105 |
+
vision_backend="demo",
|
| 106 |
+
language="zh",
|
| 107 |
+
)
|
| 108 |
+
view = json.loads(view_json)
|
| 109 |
+
|
| 110 |
+
assert view["language"] == "zh"
|
| 111 |
+
assert view["card_title"] == "今日小 Tips"
|
| 112 |
assert "今日小 Tips" in view["card_html"]
|
| 113 |
|
| 114 |
|