Improve Dream QA local flow robustness
#36
by ADJCJH - opened
- .gitattributes +3 -0
- app.py +2 -0
- docs/smoke/2026-06-12-modal-emotion-led-5round-evidence.json +101 -0
- docs/smoke/2026-06-12-modal-emotion-led-5round-evidence.md +183 -0
- docs/smoke/2026-06-12-modal-vision-sketch-evidence.json +76 -0
- docs/smoke/2026-06-12-modal-vision-sketch-evidence.md +77 -0
- docs/smoke/assets/2026-06-12-chrome-local-image-paste-fallback.png +0 -0
- docs/smoke/assets/2026-06-12-chrome-local-modal-sea-tip.png +0 -0
- docs/smoke/assets/dream_sketch_01_sea.png +0 -0
- docs/smoke/assets/dream_sketch_02_elevator.png +0 -0
- docs/smoke/assets/dream_sketch_03_subway_child.png +0 -0
- dream_customs/models.py +36 -0
- dream_customs/pipeline.py +149 -14
- dream_customs/runtime_env.py +61 -0
- dream_customs/ui/app.py +144 -8
- dream_customs/ui/copy.py +4 -4
- dream_customs/ui/styles.py +48 -0
- scripts/local_space_mirror.py +11 -41
- tests/test_local_space_mirror.py +63 -1
- tests/test_ollama_models.py +41 -0
- tests/test_pipeline.py +116 -0
- tests/test_ui_actions.py +72 -0
- videos/dreamqa_01_text_only.mp4 +3 -0
- videos/dreamqa_02_image_only.mp4 +3 -0
- videos/dreamqa_03_text_plus_image.mp4 +3 -0
.gitattributes
CHANGED
|
@@ -39,3 +39,6 @@ docs/design/assets/dream-customs-probe-night-desk.png filter=lfs diff=lfs merge=
|
|
| 39 |
docs/prototypes/2026-06-08-dream-qa-desktop-workbench.png filter=lfs diff=lfs merge=lfs -text
|
| 40 |
docs/prototypes/2026-06-08-dream-qa-mobile-flow.png filter=lfs diff=lfs merge=lfs -text
|
| 41 |
docs/prototypes/2026-06-08-dream-qa-tips-card.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
docs/prototypes/2026-06-08-dream-qa-desktop-workbench.png filter=lfs diff=lfs merge=lfs -text
|
| 40 |
docs/prototypes/2026-06-08-dream-qa-mobile-flow.png filter=lfs diff=lfs merge=lfs -text
|
| 41 |
docs/prototypes/2026-06-08-dream-qa-tips-card.png filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
videos/dreamqa_01_text_only.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
videos/dreamqa_02_image_only.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
videos/dreamqa_03_text_plus_image.mp4 filter=lfs diff=lfs merge=lfs -text
|
app.py
CHANGED
|
@@ -5,9 +5,11 @@ from fastapi import Request
|
|
| 5 |
from fastapi.responses import JSONResponse, RedirectResponse
|
| 6 |
|
| 7 |
from dream_customs import zerogpu # noqa: F401
|
|
|
|
| 8 |
from dream_customs.ui.app import build_demo
|
| 9 |
|
| 10 |
|
|
|
|
| 11 |
demo = build_demo()
|
| 12 |
|
| 13 |
|
|
|
|
| 5 |
from fastapi.responses import JSONResponse, RedirectResponse
|
| 6 |
|
| 7 |
from dream_customs import zerogpu # noqa: F401
|
| 8 |
+
from dream_customs.runtime_env import auto_load_runtime_env_json
|
| 9 |
from dream_customs.ui.app import build_demo
|
| 10 |
|
| 11 |
|
| 12 |
+
auto_load_runtime_env_json()
|
| 13 |
demo = build_demo()
|
| 14 |
|
| 15 |
|
docs/smoke/2026-06-12-modal-emotion-led-5round-evidence.json
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"generated_at": "2026-06-12T01:40:02.322834+00:00",
|
| 3 |
+
"cwd": "/Users/junhaocheng/working-dir/ai-competitions/build-small-hackthon",
|
| 4 |
+
"git_commit": "f51f11407c6fa7d41ba57c511b2cf683114e1442",
|
| 5 |
+
"git_branch": "bugfix/emotion-led-dream-qa",
|
| 6 |
+
"model": {
|
| 7 |
+
"modal_app": "dream-customs-minicpm-backend",
|
| 8 |
+
"endpoint_host": "cjh-12569--dream-customs-minicpm-backend-text.modal.run",
|
| 9 |
+
"text_model": "openbmb/MiniCPM5-1B",
|
| 10 |
+
"max_tokens_for_evidence": 220
|
| 11 |
+
},
|
| 12 |
+
"scope_note": "Direct Modal text endpoint evidence using the emotion-led prompt constraints from the local change. Full Gradio TodayTipCard JSON prompt was attempted but timed out/hung on the current Modal deployment, so this file preserves direct model replies instead of app-card schema replies.",
|
| 13 |
+
"secret_handling": "Modal secret was used inside the temporary Modal runner only; token value was not printed or written.",
|
| 14 |
+
"results": [
|
| 15 |
+
{
|
| 16 |
+
"round": 1,
|
| 17 |
+
"case_id": "sea-pressure-comfort",
|
| 18 |
+
"language": "zh",
|
| 19 |
+
"dream_text": "我梦到自己掉进海里,醒来很害怕。我想知道这是不是说明我快撑不住了?",
|
| 20 |
+
"user_answer": "我最近工作压力很大,真的很想被安慰一下。",
|
| 21 |
+
"modal_http_status": 200,
|
| 22 |
+
"modal_transport_ok": true,
|
| 23 |
+
"modal_elapsed_seconds": 26.44,
|
| 24 |
+
"modal_error": {
|
| 25 |
+
"type": "",
|
| 26 |
+
"message": ""
|
| 27 |
+
},
|
| 28 |
+
"modal_route_status": "ok",
|
| 29 |
+
"modal_route_error": "",
|
| 30 |
+
"modal_model_reply": "想理解的问题:用户最近工作压力很大,真的很想被安慰一下。\n\n一步步解读:用户提到“工作压力很大”和“很想被安慰”,这说明用户目前处于情绪低谷,需要关注自己的心理健康。梦到自己掉进海里,醒来后感到害怕,可能是因为现实中的压力和情绪影响睡眠或情绪状态。用户没有表现出害怕、难过、压力、自责或想被安慰的情绪,而是想被安慰,这可能是因为用户希望通过梦境来寻求情感支持。\n\n今日小 Tips:用户可以尝试在睡前写下当天的压力点,并思考如何调整心态,或者与信任的朋友倾诉。梦中的细节可能反映用户潜意识中的担忧或渴望,但不应过度解读。\n\n关心一句:用户需要关注自己的情绪和压力,及时寻求专业帮助或心理咨询。"
|
| 31 |
+
},
|
| 32 |
+
{
|
| 33 |
+
"round": 2,
|
| 34 |
+
"case_id": "ex-message-sadness",
|
| 35 |
+
"language": "zh",
|
| 36 |
+
"dream_text": "我梦到前任发消息又消失了,我醒来很难过,想知道是不是我还没走出来。",
|
| 37 |
+
"user_answer": "我不想要鸡汤,只想知道为什么这么难受。",
|
| 38 |
+
"modal_http_status": 200,
|
| 39 |
+
"modal_transport_ok": true,
|
| 40 |
+
"modal_elapsed_seconds": 5.82,
|
| 41 |
+
"modal_error": {
|
| 42 |
+
"type": "",
|
| 43 |
+
"message": ""
|
| 44 |
+
},
|
| 45 |
+
"modal_route_status": "ok",
|
| 46 |
+
"modal_route_error": "",
|
| 47 |
+
"modal_model_reply": "想理解的问题:用户梦到前任发消息又消失,醒来感到难过,想知道是否已走出过程。\n\n一步步解读:\n1. 梦到前任消失可能反映用户内心对这段关系的复杂感受。梦中的“消失”可能象征着情感上的断裂或未解决的矛盾。\n2. 用户感到“很难过”说明当前情绪状态可能与梦境中的经历有关,梦境往往承载着潜意识的信息。\n3. 用户想了解是否已走出过程,暗示用户可能正经历一段心理调整期,需要关注自己的情绪变化。\n\n今日小 Tips:梦中的具体细节是“前任发消息又消失”,这可能意味着用户在现实生活中遇到了沟通障碍或情感疏离,建议用户记录这些细节,以便更好地理解自己的情绪状态。\n\n关心一句:希望用户能耐心倾听自己的感受,并给予支持。"
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"round": 3,
|
| 51 |
+
"case_id": "black-wave-prophecy",
|
| 52 |
+
"language": "en",
|
| 53 |
+
"dream_text": "I dreamed of a black wave and woke thinking: is this a sign something bad will happen?",
|
| 54 |
+
"user_answer": "It left a tight feeling in my chest, and I want a grounded answer.",
|
| 55 |
+
"modal_http_status": 200,
|
| 56 |
+
"modal_transport_ok": true,
|
| 57 |
+
"modal_elapsed_seconds": 7.24,
|
| 58 |
+
"modal_error": {
|
| 59 |
+
"type": "",
|
| 60 |
+
"message": ""
|
| 61 |
+
},
|
| 62 |
+
"modal_route_status": "ok",
|
| 63 |
+
"modal_route_error": "",
|
| 64 |
+
"modal_model_reply": "It sounds like you're feeling a bit overwhelmed by the news of your black wave dream, and I understand why you want a grounded answer. Here is a step-by-step interpretation based on the details you provided.\n\n**Step 1: Analyze the dream elements**\nYou described a \"black wave,\" which is a very specific and intense dream symbol. In dream analysis, a black wave often represents a sudden, overwhelming surge of emotion, stress, or anxiety that feels like a tidal wave crashing against your mind. It is rarely a literal wave in the ocean; instead, it usually signifies a sudden, intense emotional event that has a high intensity and a strong impact on your psyche.\n\n**Step 2: Assess the emotional reaction**\nYou mentioned feeling a \"tight feeling in my chest\" after the dream. This is a classic sign of anxiety or panic. When a black wave occurs, the brain's amygdala (the fear center) is activated, triggering a fight-or-flight response. This physical sensation of tightness is a direct result of the fear of the sudden, violent nature of the event."
|
| 65 |
+
},
|
| 66 |
+
{
|
| 67 |
+
"round": 4,
|
| 68 |
+
"case_id": "lost-child-guilt",
|
| 69 |
+
"language": "zh",
|
| 70 |
+
"dream_text": "我梦到一个小孩在地铁站找不到家,我一直追不上他,醒来很自责。",
|
| 71 |
+
"user_answer": "我最近总觉得没有照顾好家人,想知道这种内疚该怎么放一放。",
|
| 72 |
+
"modal_http_status": 200,
|
| 73 |
+
"modal_transport_ok": true,
|
| 74 |
+
"modal_elapsed_seconds": 4.75,
|
| 75 |
+
"modal_error": {
|
| 76 |
+
"type": "",
|
| 77 |
+
"message": ""
|
| 78 |
+
},
|
| 79 |
+
"modal_route_status": "ok",
|
| 80 |
+
"modal_route_error": "",
|
| 81 |
+
"modal_model_reply": "想理解的问题:用户醒来后感到内疚和自责,这可能源于对未尽责任的担忧。\n\n一步步解读:用户梦到小孩在地铁站找不到家,这可能象征着生活中未解决的问题或未完成的任务。用户一直追不上小孩,可能意味着在现实生活中感到被忽视或无法跟上他人的步伐。醒来后感到内疚和自责,可能是因为对未能提供足够支持或帮助的担忧。\n\n今日小 Tips:用户可以尝试在日常生活中多关注家人,表达关心和支持。也可以尝试与家人沟通,分享自己的感受和需求。\n\n关心一句:希望用户能够找到适合自己的方式来处理这些情绪,并从中找到力量和成长。"
|
| 82 |
+
},
|
| 83 |
+
{
|
| 84 |
+
"round": 5,
|
| 85 |
+
"case_id": "drowning-not-coping",
|
| 86 |
+
"language": "en",
|
| 87 |
+
"dream_text": "I dreamed I was drowning in dark water and woke scared. Does this mean I am not coping?",
|
| 88 |
+
"user_answer": "I need comfort, not productivity advice.",
|
| 89 |
+
"modal_http_status": 200,
|
| 90 |
+
"modal_transport_ok": true,
|
| 91 |
+
"modal_elapsed_seconds": 7.54,
|
| 92 |
+
"modal_error": {
|
| 93 |
+
"type": "",
|
| 94 |
+
"message": ""
|
| 95 |
+
},
|
| 96 |
+
"modal_route_status": "ok",
|
| 97 |
+
"modal_route_error": "",
|
| 98 |
+
"modal_model_reply": "I need comfort, not productivity advice.\n\nHere is a step-by-step interpretation based on the dream details you provided:\n\n1. **Analyze the Dream Elements**: You were in \"dark water,\" which is a common symbol of confusion, danger, or emotional turmoil. Being \"drowning\" indicates a current crisis or fear of drowning, often linked to feelings of helplessness or anxiety. The fact that you were \"scared\" suggests the dreamer is currently feeling apprehension or panic about the situation.\n\n2. **Assess the Emotional State**: The combination of being in a dangerous environment (dark water) and feeling scared is a classic sign of anxiety or panic. This state is often a reaction to real-life stressors, such as a recent traumatic event, financial stress, or a difficult conversation.\n\n3. **Determine the Need for Support**: Since the dream occurred while you were \"scared,\" the dreamer is likely experiencing a heightened state of anxiety. This is a clear indication that they need immediate emotional comfort and reassurance rather than focusing on tasks or productivity.\n\n4."
|
| 99 |
+
}
|
| 100 |
+
]
|
| 101 |
+
}
|
docs/smoke/2026-06-12-modal-emotion-led-5round-evidence.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Modal Emotion-Led Reply Evidence - 2026-06-12
|
| 2 |
+
|
| 3 |
+
## Scope
|
| 4 |
+
|
| 5 |
+
- Local branch: `bugfix/emotion-led-dream-qa`
|
| 6 |
+
- Local commit: `f51f11407c6fa7d41ba57c511b2cf683114e1442`
|
| 7 |
+
- Modal app: `dream-customs-minicpm-backend`
|
| 8 |
+
- Text model: `openbmb/MiniCPM5-1B`
|
| 9 |
+
- Endpoint host: `cjh-12569--dream-customs-minicpm-backend-text.modal.run`
|
| 10 |
+
- Evidence mode: direct Modal text endpoint prompt using the emotion-led constraints from the local change.
|
| 11 |
+
- Secret handling: Modal secret was used only inside a temporary Modal runner; no token value was printed or written.
|
| 12 |
+
|
| 13 |
+
## Important Note
|
| 14 |
+
|
| 15 |
+
The full Gradio `TodayTipCard` JSON prompt path was attempted first, but the current Modal deployment did not return within the local evidence window. This evidence therefore records direct raw Modal model replies under the same product constraints, not the final app card schema. The raw replies show why local backend post-processing remains necessary.
|
| 16 |
+
|
| 17 |
+
## Summary
|
| 18 |
+
|
| 19 |
+
| Round | Case | HTTP | Elapsed | Reply Evidence |
|
| 20 |
+
| --- | --- | ---: | ---: | --- |
|
| 21 |
+
| 1 | `sea-pressure-comfort` | 200 | 26.44s | medicalized wording risk |
|
| 22 |
+
| 2 | `ex-message-sadness` | 200 | 5.82s | raw reply captured |
|
| 23 |
+
| 3 | `black-wave-prophecy` | 200 | 7.24s | medicalized wording risk |
|
| 24 |
+
| 4 | `lost-child-guilt` | 200 | 4.75s | raw reply captured |
|
| 25 |
+
| 5 | `drowning-not-coping` | 200 | 7.54s | medicalized wording risk |
|
| 26 |
+
|
| 27 |
+
## Round 1 - sea-pressure-comfort
|
| 28 |
+
|
| 29 |
+
**Dream**
|
| 30 |
+
|
| 31 |
+
我梦到自己掉进海里,醒来很害怕。我想知道这是不是说明我快撑不住了?
|
| 32 |
+
|
| 33 |
+
**User Answer**
|
| 34 |
+
|
| 35 |
+
我最近工作压力很大,真的很想被安慰一下。
|
| 36 |
+
|
| 37 |
+
**Modal Route**
|
| 38 |
+
|
| 39 |
+
- HTTP: `200`
|
| 40 |
+
- Transport OK: `True`
|
| 41 |
+
- Elapsed: `26.44s`
|
| 42 |
+
- Route status: `ok`
|
| 43 |
+
|
| 44 |
+
**Modal Model Reply**
|
| 45 |
+
|
| 46 |
+
```text
|
| 47 |
+
想理解的问题:用户最近工作压力很大,真的很想被安慰一下。
|
| 48 |
+
|
| 49 |
+
一步步解读:用户提到“工作压力很大”和“很想被安慰”,这说明用户目前处于情绪低谷,需要关注自己的心理健康。梦到自己掉进海里,醒来后感到害怕,可能是因为现实中的压力和情绪影响睡眠或情绪状态。用户没有表现出害怕、难过、压力、自责或想被安慰的情绪,而是想被安慰,这可能是因为用户希望通过梦境来寻求情感支持。
|
| 50 |
+
|
| 51 |
+
今日小 Tips:用户可以尝试在睡前写下当天的压力点,并思考如何调整心态,或者与信任的朋友倾诉。梦中的细节可能反映用户潜意识中的担忧或渴望,但不应过度解读。
|
| 52 |
+
|
| 53 |
+
关心一句:用户需要关注自己的情绪和压力,及时寻求专业帮助或心理咨询。
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
## Round 2 - ex-message-sadness
|
| 57 |
+
|
| 58 |
+
**Dream**
|
| 59 |
+
|
| 60 |
+
我梦到前任发消息又消失了,我醒来很难过,想知道是不是我还没走出来。
|
| 61 |
+
|
| 62 |
+
**User Answer**
|
| 63 |
+
|
| 64 |
+
我不想要鸡汤,只想知道为什么这么难受。
|
| 65 |
+
|
| 66 |
+
**Modal Route**
|
| 67 |
+
|
| 68 |
+
- HTTP: `200`
|
| 69 |
+
- Transport OK: `True`
|
| 70 |
+
- Elapsed: `5.82s`
|
| 71 |
+
- Route status: `ok`
|
| 72 |
+
|
| 73 |
+
**Modal Model Reply**
|
| 74 |
+
|
| 75 |
+
```text
|
| 76 |
+
想理解的问题:用户梦到前任发消息又消失,醒来感到难过,想知道是否已走出过程。
|
| 77 |
+
|
| 78 |
+
一步步解读:
|
| 79 |
+
1. 梦到前任消失可能反映用户内心对这段关系的复杂感受。梦中的“消失”可能象征着情感上的断裂或未解决的矛盾。
|
| 80 |
+
2. 用户感到“很难过”说明当前情绪状态可能与梦境中的经历有关,梦境往往承载着潜意识的信息。
|
| 81 |
+
3. 用户想了解是否已走出过程,暗示用户可能正经历一段心理调整期,需要关注自己的情绪变化。
|
| 82 |
+
|
| 83 |
+
今日小 Tips:梦中的具体细节是“前任发消息又消失”,这可能意味着用户在现实生活中遇到了沟通障碍或情感疏离,建议用户记录这些细节,以便更好地理解自己的情绪状态。
|
| 84 |
+
|
| 85 |
+
关心一句:希望用户能耐心倾听自己的感受,并给予支持。
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
## Round 3 - black-wave-prophecy
|
| 89 |
+
|
| 90 |
+
**Dream**
|
| 91 |
+
|
| 92 |
+
I dreamed of a black wave and woke thinking: is this a sign something bad will happen?
|
| 93 |
+
|
| 94 |
+
**User Answer**
|
| 95 |
+
|
| 96 |
+
It left a tight feeling in my chest, and I want a grounded answer.
|
| 97 |
+
|
| 98 |
+
**Modal Route**
|
| 99 |
+
|
| 100 |
+
- HTTP: `200`
|
| 101 |
+
- Transport OK: `True`
|
| 102 |
+
- Elapsed: `7.24s`
|
| 103 |
+
- Route status: `ok`
|
| 104 |
+
|
| 105 |
+
**Modal Model Reply**
|
| 106 |
+
|
| 107 |
+
```text
|
| 108 |
+
It sounds like you're feeling a bit overwhelmed by the news of your black wave dream, and I understand why you want a grounded answer. Here is a step-by-step interpretation based on the details you provided.
|
| 109 |
+
|
| 110 |
+
**Step 1: Analyze the dream elements**
|
| 111 |
+
You described a "black wave," which is a very specific and intense dream symbol. In dream analysis, a black wave often represents a sudden, overwhelming surge of emotion, stress, or anxiety that feels like a tidal wave crashing against your mind. It is rarely a literal wave in the ocean; instead, it usually signifies a sudden, intense emotional event that has a high intensity and a strong impact on your psyche.
|
| 112 |
+
|
| 113 |
+
**Step 2: Assess the emotional reaction**
|
| 114 |
+
You mentioned feeling a "tight feeling in my chest" after the dream. This is a classic sign of anxiety or panic. When a black wave occurs, the brain's amygdala (the fear center) is activated, triggering a fight-or-flight response. This physical sensation of tightness is a direct result of the fear of the sudden, violent nature of the event.
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
## Round 4 - lost-child-guilt
|
| 118 |
+
|
| 119 |
+
**Dream**
|
| 120 |
+
|
| 121 |
+
我梦到一个小孩在地铁站找不到家,我一直追不上他,醒来很自责。
|
| 122 |
+
|
| 123 |
+
**User Answer**
|
| 124 |
+
|
| 125 |
+
我最近总觉得没有照顾好家人,想知道这种内疚该怎么放一放。
|
| 126 |
+
|
| 127 |
+
**Modal Route**
|
| 128 |
+
|
| 129 |
+
- HTTP: `200`
|
| 130 |
+
- Transport OK: `True`
|
| 131 |
+
- Elapsed: `4.75s`
|
| 132 |
+
- Route status: `ok`
|
| 133 |
+
|
| 134 |
+
**Modal Model Reply**
|
| 135 |
+
|
| 136 |
+
```text
|
| 137 |
+
想理解的问题:用户醒来后感到内疚和自责,这可能源于对未尽责任的担忧。
|
| 138 |
+
|
| 139 |
+
一步步解读:用户梦到小孩在地铁站找不到家,这可能象征着生活中未解决的问题或未完成的任务。用户一直追不上小孩,可能意味着在现实生活中感到被忽视或无法跟上他人的步伐。醒来后感到内疚和自责,可能是因为对未能提供足够支持或帮助的担忧。
|
| 140 |
+
|
| 141 |
+
今日小 Tips:用户可以尝试在日常生活中多关注家人,表达关心和支持。也可以尝试与家人沟通,分享自己的感受和需求。
|
| 142 |
+
|
| 143 |
+
关心一句:希望用户能够找到适合自己的方式来处理这些情绪,并从中找到力量和成长。
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
## Round 5 - drowning-not-coping
|
| 147 |
+
|
| 148 |
+
**Dream**
|
| 149 |
+
|
| 150 |
+
I dreamed I was drowning in dark water and woke scared. Does this mean I am not coping?
|
| 151 |
+
|
| 152 |
+
**User Answer**
|
| 153 |
+
|
| 154 |
+
I need comfort, not productivity advice.
|
| 155 |
+
|
| 156 |
+
**Modal Route**
|
| 157 |
+
|
| 158 |
+
- HTTP: `200`
|
| 159 |
+
- Transport OK: `True`
|
| 160 |
+
- Elapsed: `7.54s`
|
| 161 |
+
- Route status: `ok`
|
| 162 |
+
|
| 163 |
+
**Modal Model Reply**
|
| 164 |
+
|
| 165 |
+
```text
|
| 166 |
+
I need comfort, not productivity advice.
|
| 167 |
+
|
| 168 |
+
Here is a step-by-step interpretation based on the dream details you provided:
|
| 169 |
+
|
| 170 |
+
1. **Analyze the Dream Elements**: You were in "dark water," which is a common symbol of confusion, danger, or emotional turmoil. Being "drowning" indicates a current crisis or fear of drowning, often linked to feelings of helplessness or anxiety. The fact that you were "scared" suggests the dreamer is currently feeling apprehension or panic about the situation.
|
| 171 |
+
|
| 172 |
+
2. **Assess the Emotional State**: The combination of being in a dangerous environment (dark water) and feeling scared is a classic sign of anxiety or panic. This state is often a reaction to real-life stressors, such as a recent traumatic event, financial stress, or a difficult conversation.
|
| 173 |
+
|
| 174 |
+
3. **Determine the Need for Support**: Since the dream occurred while you were "scared," the dreamer is likely experiencing a heightened state of anxiety. This is a clear indication that they need immediate emotional comfort and reassurance rather than focusing on tasks or productivity.
|
| 175 |
+
|
| 176 |
+
4.
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
+
## Takeaways
|
| 180 |
+
|
| 181 |
+
- Modal returned 5/5 HTTP 200 responses through the real MiniCPM text route.
|
| 182 |
+
- Raw Modal replies often follow the dream/emotion direction, but still drift into medicalized or generic counseling language.
|
| 183 |
+
- The local backend hardening should keep overriding/repairing final app output, especially for safety tone, direct question answering, and non-productivity comfort.
|
docs/smoke/2026-06-12-modal-vision-sketch-evidence.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"generated_at": "2026-06-12T01:53:21.674225+00:00",
|
| 3 |
+
"cwd": "/Users/junhaocheng/working-dir/ai-competitions/build-small-hackthon",
|
| 4 |
+
"git_commit": "ec0441abdecacf44918ca9eace7c2a57e6795d1b",
|
| 5 |
+
"git_branch": "bugfix/emotion-led-dream-qa",
|
| 6 |
+
"model": {
|
| 7 |
+
"modal_app": "dream-customs-minicpm-backend",
|
| 8 |
+
"vision_model": "openbmb/MiniCPM-V-4.6",
|
| 9 |
+
"endpoint_host": "cjh-12569--dream-customs-minicpm-backend-vision.modal.run"
|
| 10 |
+
},
|
| 11 |
+
"secret_handling": "Modal secret used only inside temporary Modal runner; no token printed or written.",
|
| 12 |
+
"results": [
|
| 13 |
+
{
|
| 14 |
+
"case_id": "sea",
|
| 15 |
+
"image_path": "/Users/junhaocheng/working-dir/ai-competitions/build-small-hackthon/tmp/dream-sketches/dream_sketch_01_sea.png",
|
| 16 |
+
"expected_markers": [
|
| 17 |
+
"sea",
|
| 18 |
+
"wave",
|
| 19 |
+
"moon",
|
| 20 |
+
"person"
|
| 21 |
+
],
|
| 22 |
+
"http_status": 200,
|
| 23 |
+
"transport_ok": true,
|
| 24 |
+
"elapsed_seconds": 25.48,
|
| 25 |
+
"route_error": "",
|
| 26 |
+
"raw_response": "{'input_text': [{'role': 'user', 'content': [{'type': 'image', 'path': '/tmp/tmpjyirmi6i.png'}, {'type': 'text', 'text': 'You are MiniCPM-V-4.6 acting as the visual witness for Dream QA. Describe only what is visible in this dream sketch. Return concise JSON with keys: scene_summary, objects, visible_text, spatial_relations, mood_cues, uncertain_details, surprising_detail. Do not diagnose the user.'}]}], 'generated_text': [{'role': 'user', 'content': [{'type': 'image', 'path': '/tmp/tmpjyirmi6i.png'}, {'type': 'text', 'text': 'You are MiniCPM-V-4.6 acting as the visual witness for Dream QA. Describe only what is visible in this dream sketch. Return concise JSON with keys: scene_summary, objects, visible_text, spatial_relations, mood_cues, uncertain_details, surprising_detail. Do not diagnose the user.'}]}, {'role': 'assistant', 'content': '\\n\\n{\\n \"scene_summary\": \"A dreamlike representation of a sea under night\",\\n \"objects\": [\"a stick figure\", \"wavy lines representing water\", \"a crescent moon\", \"a circular shape above the stick figure\"],\\n \"visible_text\": [\"dark sea dream\"],\\n \"spatial_relations\": \"the stick figure is on the water waves, with the moon in the upper right\",\\n \"mood_cues\": \"calm and dreamy\",\\n \"uncertain_details\": \"the exact emotions or states of the figure\",\\n \"surprising_detail\": \"the stick figure has a curved line above its head, suggesting a sense of wonder or awareness\"\\n}'}]}",
|
| 27 |
+
"marker_hits": [
|
| 28 |
+
"sea",
|
| 29 |
+
"wave",
|
| 30 |
+
"moon"
|
| 31 |
+
]
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"case_id": "elevator",
|
| 35 |
+
"image_path": "/Users/junhaocheng/working-dir/ai-competitions/build-small-hackthon/tmp/dream-sketches/dream_sketch_02_elevator.png",
|
| 36 |
+
"expected_markers": [
|
| 37 |
+
"elevator",
|
| 38 |
+
"button",
|
| 39 |
+
"14",
|
| 40 |
+
"melted"
|
| 41 |
+
],
|
| 42 |
+
"http_status": 200,
|
| 43 |
+
"transport_ok": true,
|
| 44 |
+
"elapsed_seconds": 5.55,
|
| 45 |
+
"route_error": "",
|
| 46 |
+
"raw_response": "{'input_text': [{'role': 'user', 'content': [{'type': 'image', 'path': '/tmp/tmpc8qf8m8w.png'}, {'type': 'text', 'text': 'You are MiniCPM-V-4.6 acting as the visual witness for Dream QA. Describe only what is visible in this dream sketch. Return concise JSON with keys: scene_summary, objects, visible_text, spatial_relations, mood_cues, uncertain_details, surprising_detail. Do not diagnose the user.'}]}], 'generated_text': [{'role': 'user', 'content': [{'type': 'image', 'path': '/tmp/tmpc8qf8m8w.png'}, {'type': 'text', 'text': 'You are MiniCPM-V-4.6 acting as the visual witness for Dream QA. Describe only what is visible in this dream sketch. Return concise JSON with keys: scene_summary, objects, visible_text, spatial_relations, mood_cues, uncertain_details, surprising_detail. Do not diagnose the user.'}]}, {'role': 'assistant', 'content': '\\n\\n{\\n \"scene_summary\": \"A conceptual visual representation of melted elevator buttons in a setting\",\\n \"objects\": [\"melted elevator buttons\", \"two tall gray doors\"],\\n \"visible_text\": [\"14\", \"melted elevator buttons\"],\\n \"spatial_relations\": \"The melted buttons are to the right of the elevator doors\",\\n \"mood_cues\": \"Suggests a malfunction or melting process\",\\n \"uncertain_details\": \"The exact state or effects of the melted buttons are implied\",\\n \"surprising_detail\": \"The buttons are depicted as colorful segments, indicating a melting or transition state\"\\n}'}]}",
|
| 47 |
+
"marker_hits": [
|
| 48 |
+
"elevator",
|
| 49 |
+
"button",
|
| 50 |
+
"14",
|
| 51 |
+
"melted"
|
| 52 |
+
]
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
"case_id": "subway_child",
|
| 56 |
+
"image_path": "/Users/junhaocheng/working-dir/ai-competitions/build-small-hackthon/tmp/dream-sketches/dream_sketch_03_subway_child.png",
|
| 57 |
+
"expected_markers": [
|
| 58 |
+
"subway",
|
| 59 |
+
"child",
|
| 60 |
+
"home",
|
| 61 |
+
"arrow"
|
| 62 |
+
],
|
| 63 |
+
"http_status": 200,
|
| 64 |
+
"transport_ok": true,
|
| 65 |
+
"elapsed_seconds": 5.62,
|
| 66 |
+
"route_error": "",
|
| 67 |
+
"raw_response": "{'input_text': [{'role': 'user', 'content': [{'type': 'image', 'path': '/tmp/tmp38m34fjv.png'}, {'type': 'text', 'text': 'You are MiniCPM-V-4.6 acting as the visual witness for Dream QA. Describe only what is visible in this dream sketch. Return concise JSON with keys: scene_summary, objects, visible_text, spatial_relations, mood_cues, uncertain_details, surprising_detail. Do not diagnose the user.'}]}], 'generated_text': [{'role': 'user', 'content': [{'type': 'image', 'path': '/tmp/tmp38m34fjv.png'}, {'type': 'text', 'text': 'You are MiniCPM-V-4.6 acting as the visual witness for Dream QA. Describe only what is visible in this dream sketch. Return concise JSON with keys: scene_summary, objects, visible_text, spatial_relations, mood_cues, uncertain_details, surprising_detail. Do not diagnose the user.'}]}, {'role': 'assistant', 'content': '\\n\\n{\\n \"scene_summary\": \"A simple sketch depicting a lost child at a subway station\",\\n \"objects\": [\"a child figure\", \"a subway sign\", \"an arrow\"],\\n \"visible_text\": [\"lost child at subway\", \"SUBWAY\", \"HOME ?\"],\\n \"spatial_relations\": \"the child is in front of the subway sign, which has an arrow pointing to the right\",\\n \"mood_cues\": \"suggests a situation of loss or search\",\\n \"uncertain_details\": \"what the child is looking for or where home is\",\\n \"surprising_detail\": \"the sign asks \\'HOME ?\\'\"\\n}'}]}",
|
| 68 |
+
"marker_hits": [
|
| 69 |
+
"subway",
|
| 70 |
+
"child",
|
| 71 |
+
"home",
|
| 72 |
+
"arrow"
|
| 73 |
+
]
|
| 74 |
+
}
|
| 75 |
+
]
|
| 76 |
+
}
|
docs/smoke/2026-06-12-modal-vision-sketch-evidence.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Modal Vision Sketch Evidence - 2026-06-12
|
| 2 |
+
|
| 3 |
+
Goal: verify whether Dream QA can use MiniCPM-V-4.6 to understand uploaded dream sketches, and clarify how text plus image should be merged.
|
| 4 |
+
|
| 5 |
+
## Test Assets
|
| 6 |
+
|
| 7 |
+
- Sea sketch: `docs/smoke/assets/dream_sketch_01_sea.png`
|
| 8 |
+
- Elevator sketch: `docs/smoke/assets/dream_sketch_02_elevator.png`
|
| 9 |
+
- Subway child sketch: `docs/smoke/assets/dream_sketch_03_subway_child.png`
|
| 10 |
+
- Raw Modal evidence: `docs/smoke/2026-06-12-modal-vision-sketch-evidence.json`
|
| 11 |
+
- Chrome local paste screenshot: `docs/smoke/assets/2026-06-12-chrome-local-image-paste-fallback.png`
|
| 12 |
+
- Chrome configured local Modal screenshot: `docs/smoke/assets/2026-06-12-chrome-local-modal-sea-tip.png`
|
| 13 |
+
|
| 14 |
+
## Chrome Local Upload Check
|
| 15 |
+
|
| 16 |
+
Local app URL: `http://127.0.0.1:7870`
|
| 17 |
+
|
| 18 |
+
Result:
|
| 19 |
+
|
| 20 |
+
- Native file chooser injection was blocked by the Chrome extension file-access permission.
|
| 21 |
+
- Clipboard paste into the Gradio image input worked. The page showed `Remove Image`, confirming the image slot received the sketch.
|
| 22 |
+
- Submitting the local page without Modal endpoint/token configured did not prove real MiniCPM-V behavior. The follow-up used fallback/demo visual clues (`melted elevator button`) instead of the pasted sea sketch.
|
| 23 |
+
|
| 24 |
+
Interpretation: the local Gradio image UI can receive images, but a local browser smoke without configured hosted endpoint/token only verifies upload plumbing. It must not be treated as evidence that MiniCPM-V understood the image.
|
| 25 |
+
|
| 26 |
+
Correction after local runtime config check:
|
| 27 |
+
|
| 28 |
+
- The machine already had `/tmp/dream-customs-runtime.json` configured with text, vision, ASR, and hosted token values.
|
| 29 |
+
- Starting the app with `python app.py` does not auto-load that file.
|
| 30 |
+
- Starting with `.venv/bin/python scripts/local_space_mirror.py --port 7871` does auto-load it and prints only configuration booleans.
|
| 31 |
+
- Chrome Debug confirmed `text_endpoint_configured: true`, `vision_endpoint_configured: true`, `asr_endpoint_configured: true`, and `hosted_token_configured: true`.
|
| 32 |
+
- Re-running the sea sketch through Chrome paste plus Chinese text produced real MiniCPM-V image clues: stick figure, wavy water, crescent moon, visible text `dark sea dream`.
|
| 33 |
+
- Final user-visible result preserved the user's question `为什么它让我这么慌?` and used Chinese anchors `小人`, `月牙`, `漆黑的海`, `草图`. The bad English anchor `a simple sketch of a person standing on wavy` no longer appeared after the follow-up fix.
|
| 34 |
+
|
| 35 |
+
## Real MiniCPM-V-4.6 Route
|
| 36 |
+
|
| 37 |
+
The three sketches were sent to the Modal vision endpoint through a temporary local runner. Secret handling: token was used only inside the Modal runner path; no token was printed, stored, or committed.
|
| 38 |
+
|
| 39 |
+
| Case | Expected visual markers | HTTP | Elapsed | Marker hits | Modal-V result |
|
| 40 |
+
| --- | --- | --- | ---: | --- | --- |
|
| 41 |
+
| `sea` | sea, wave, moon, person | 200 | 25.48s | sea, wave, moon | saw a sea under night, stick figure, waves, crescent moon, visible text `dark sea dream` |
|
| 42 |
+
| `elevator` | elevator, button, 14, melted | 200 | 5.55s | elevator, button, 14, melted | saw melted elevator buttons, two gray doors, visible text `14` |
|
| 43 |
+
| `subway_child` | subway, child, home, arrow | 200 | 5.62s | subway, child, home, arrow | saw a lost child at a subway station, subway sign, arrow, visible text `HOME ?` |
|
| 44 |
+
|
| 45 |
+
Conclusion: MiniCPM-V-4.6 did understand the low-resolution dream sketches and returned concrete visual evidence.
|
| 46 |
+
|
| 47 |
+
## Backend Fix Verified
|
| 48 |
+
|
| 49 |
+
Observed Modal response shape:
|
| 50 |
+
|
| 51 |
+
- The hosted endpoint returned `response` as a stringified Python dictionary.
|
| 52 |
+
- The useful assistant JSON was nested under `generated_text[-1].content`.
|
| 53 |
+
- Before this fix, Dream QA could treat that whole string as unparsed text and lose the structured witness report.
|
| 54 |
+
|
| 55 |
+
Fix:
|
| 56 |
+
|
| 57 |
+
- `HostedMiniCPMVisionClient` now extracts assistant content from stringified Modal pipeline payloads and message lists.
|
| 58 |
+
- Chinese Dream QA now localizes common English visual anchors from MiniCPM-V, so a Chinese user does not see awkward anchors like `dreamlike representation of a sea`.
|
| 59 |
+
|
| 60 |
+
Verification:
|
| 61 |
+
|
| 62 |
+
```bash
|
| 63 |
+
.venv/bin/python -m pytest -q tests/test_ollama_models.py tests/test_vision_witness.py tests/test_pipeline.py::test_visual_witness_clues_drive_questions_and_today_tip tests/test_pipeline.py::test_zh_text_and_image_keep_user_question_while_using_visual_anchors
|
| 64 |
+
.venv/bin/python -m py_compile dream_customs/models.py dream_customs/pipeline.py
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
Result: `21 passed in 0.12s`; `py_compile` passed.
|
| 68 |
+
|
| 69 |
+
## Text + Image Product Rule
|
| 70 |
+
|
| 71 |
+
When the user uploads both text and image:
|
| 72 |
+
|
| 73 |
+
1. User text owns the emotional question and intent. Example: `我想知道为什么它让我这么慌` becomes the main question.
|
| 74 |
+
2. Image contributes concrete anchors. Example: sea, waves, crescent moon, elevator button, subway sign, `HOME ?`.
|
| 75 |
+
3. The follow-up and Today Tip must combine them: answer the user's feeling first, then use the visual anchors as evidence.
|
| 76 |
+
4. If text and image conflict, do not force a single meaning. Ask one gentle clarification or phrase it as uncertainty.
|
| 77 |
+
5. Text-only remains the fallback; image failure must not block the user from getting a grounded response.
|
docs/smoke/assets/2026-06-12-chrome-local-image-paste-fallback.png
ADDED
|
docs/smoke/assets/2026-06-12-chrome-local-modal-sea-tip.png
ADDED
|
docs/smoke/assets/dream_sketch_01_sea.png
ADDED
|
docs/smoke/assets/dream_sketch_02_elevator.png
ADDED
|
docs/smoke/assets/dream_sketch_03_subway_child.png
ADDED
|
dream_customs/models.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
import base64
|
|
|
|
| 2 |
import json
|
| 3 |
import os
|
| 4 |
import re
|
|
@@ -549,7 +550,16 @@ def _hosted_text_from_response(payload: Dict[str, Any]) -> str:
|
|
| 549 |
for key in ("response", "text", "generated_text", "output"):
|
| 550 |
value = payload.get(key)
|
| 551 |
if isinstance(value, str):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 552 |
return value
|
|
|
|
|
|
|
|
|
|
|
|
|
| 553 |
choices = payload.get("choices")
|
| 554 |
if isinstance(choices, list) and choices:
|
| 555 |
first = choices[0]
|
|
@@ -565,6 +575,32 @@ def _hosted_text_from_response(payload: Dict[str, Any]) -> str:
|
|
| 565 |
return ""
|
| 566 |
|
| 567 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 568 |
class HostedMiniCPMTextClient:
|
| 569 |
def __init__(
|
| 570 |
self,
|
|
|
|
| 1 |
import base64
|
| 2 |
+
import ast
|
| 3 |
import json
|
| 4 |
import os
|
| 5 |
import re
|
|
|
|
| 550 |
for key in ("response", "text", "generated_text", "output"):
|
| 551 |
value = payload.get(key)
|
| 552 |
if isinstance(value, str):
|
| 553 |
+
parsed = _python_literal_dict(value)
|
| 554 |
+
if parsed:
|
| 555 |
+
nested = _hosted_text_from_response(parsed)
|
| 556 |
+
if nested:
|
| 557 |
+
return nested
|
| 558 |
return value
|
| 559 |
+
if isinstance(value, list):
|
| 560 |
+
nested = _content_from_messages(value)
|
| 561 |
+
if nested:
|
| 562 |
+
return nested
|
| 563 |
choices = payload.get("choices")
|
| 564 |
if isinstance(choices, list) and choices:
|
| 565 |
first = choices[0]
|
|
|
|
| 575 |
return ""
|
| 576 |
|
| 577 |
|
| 578 |
+
def _python_literal_dict(text: str) -> Optional[Dict[str, Any]]:
|
| 579 |
+
try:
|
| 580 |
+
parsed = ast.literal_eval(text)
|
| 581 |
+
except (SyntaxError, ValueError):
|
| 582 |
+
return None
|
| 583 |
+
return parsed if isinstance(parsed, dict) else None
|
| 584 |
+
|
| 585 |
+
|
| 586 |
+
def _content_from_messages(messages: List[Any]) -> str:
|
| 587 |
+
for item in reversed(messages):
|
| 588 |
+
if not isinstance(item, dict):
|
| 589 |
+
continue
|
| 590 |
+
content = item.get("content")
|
| 591 |
+
if isinstance(content, str) and content.strip():
|
| 592 |
+
return content.strip()
|
| 593 |
+
if isinstance(content, list):
|
| 594 |
+
text_parts = [
|
| 595 |
+
str(part.get("text", "")).strip()
|
| 596 |
+
for part in content
|
| 597 |
+
if isinstance(part, dict) and str(part.get("text", "")).strip()
|
| 598 |
+
]
|
| 599 |
+
if text_parts:
|
| 600 |
+
return "\n".join(text_parts)
|
| 601 |
+
return ""
|
| 602 |
+
|
| 603 |
+
|
| 604 |
class HostedMiniCPMTextClient:
|
| 605 |
def __init__(
|
| 606 |
self,
|
dream_customs/pipeline.py
CHANGED
|
@@ -175,10 +175,14 @@ _ZH_ANCHOR_MARKERS = [
|
|
| 175 |
"家门",
|
| 176 |
"旧家",
|
| 177 |
"楼顶",
|
|
|
|
|
|
|
| 178 |
"漆黑的走廊",
|
| 179 |
"漆黑走廊",
|
| 180 |
"黑暗走廊",
|
| 181 |
"走廊",
|
|
|
|
|
|
|
| 182 |
"教室",
|
| 183 |
"老师",
|
| 184 |
"交作业",
|
|
@@ -194,10 +198,15 @@ _ZH_ANCHOR_MARKERS = [
|
|
| 194 |
"安全帽",
|
| 195 |
"男生",
|
| 196 |
"电梯按钮",
|
|
|
|
|
|
|
|
|
|
| 197 |
"融化的按钮",
|
| 198 |
"按钮融化",
|
| 199 |
"数字 14",
|
| 200 |
"数字14",
|
|
|
|
|
|
|
| 201 |
"老楼",
|
| 202 |
"桥",
|
| 203 |
"水",
|
|
@@ -285,10 +294,14 @@ _ZH_TO_EN_PHRASES = {
|
|
| 285 |
"家门": "home door",
|
| 286 |
"旧家": "old home",
|
| 287 |
"楼顶": "rooftop",
|
|
|
|
|
|
|
| 288 |
"漆黑的走廊": "dark hallway",
|
| 289 |
"漆黑走廊": "dark hallway",
|
| 290 |
"黑暗走廊": "dark hallway",
|
| 291 |
"走廊": "hallway",
|
|
|
|
|
|
|
| 292 |
"教室": "classroom",
|
| 293 |
"老师": "teacher",
|
| 294 |
"交作业": "assignment",
|
|
@@ -304,10 +317,15 @@ _ZH_TO_EN_PHRASES = {
|
|
| 304 |
"安全帽": "hard hat",
|
| 305 |
"男生": "young man",
|
| 306 |
"电梯按钮": "elevator button",
|
|
|
|
|
|
|
|
|
|
| 307 |
"融化的按钮": "melted button",
|
| 308 |
"按钮融化": "melted button",
|
| 309 |
"数字 14": "floor 14",
|
| 310 |
"数字14": "floor 14",
|
|
|
|
|
|
|
| 311 |
"楼层数字": "floor number",
|
| 312 |
"楼层": "floor",
|
| 313 |
"老楼": "old apartment building",
|
|
@@ -567,6 +585,53 @@ def _clean_placeholder_phrase(text: str) -> str:
|
|
| 567 |
return clean.strip()
|
| 568 |
|
| 569 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 570 |
def _primary_anchor(intake: DreamIntake, language: str = "en") -> str:
|
| 571 |
anchors = _anchors_for_language(intake, language)
|
| 572 |
if anchors:
|
|
@@ -596,6 +661,25 @@ def _story_text(intake: DreamIntake, answers: str = "") -> str:
|
|
| 596 |
).lower()
|
| 597 |
|
| 598 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 599 |
def _contains_any(text: str, terms: List[str]) -> bool:
|
| 600 |
return any(term in text for term in terms)
|
| 601 |
|
|
@@ -632,13 +716,39 @@ def _join_anchors(anchors: List[str], language: str = "en", limit: int = 3) -> s
|
|
| 632 |
return ", ".join(visible[:-1]) + f", and {visible[-1]}"
|
| 633 |
|
| 634 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 635 |
def _story_anchor_phrase(intake: DreamIntake, anchors: List[str], language: str = "en", answers: str = "") -> str:
|
| 636 |
theme = _dream_theme(intake, answers)
|
|
|
|
|
|
|
| 637 |
if _is_zh(language):
|
| 638 |
themed = {
|
| 639 |
"lost_home": "地铁站里迷路的小孩和回家的方向",
|
| 640 |
"dark_water": "夜晚的海、海浪和月牙下的小人",
|
| 641 |
-
"stuck_elevator": "电梯、融化的按钮和数字 14",
|
| 642 |
"library_signal": "旧图书馆、红色楼梯和那张便签",
|
| 643 |
"message_loss": "那条消息、前任和突然消失",
|
| 644 |
"chased": "森林、白色猫和空白路牌",
|
|
@@ -649,7 +759,6 @@ def _story_anchor_phrase(intake: DreamIntake, anchors: List[str], language: str
|
|
| 649 |
themed = {
|
| 650 |
"lost_home": "the lost child, the subway, and the way home",
|
| 651 |
"dark_water": "the dark water, the waves, and the small figure under the moon",
|
| 652 |
-
"stuck_elevator": "the elevator, the melted button, and floor 14",
|
| 653 |
"library_signal": "the old library, the red staircase, and the note",
|
| 654 |
"message_loss": "the message, the former partner, and the disappearance",
|
| 655 |
"chased": "the chase, the call for help, and the missing route",
|
|
@@ -671,9 +780,16 @@ def _answer_snippet(answers: str, language: str = "en") -> str:
|
|
| 671 |
return ""
|
| 672 |
clean = re.sub(r"\s+", " ", lines[-1]).strip(" ::「」\"'()[]{}")
|
| 673 |
if _is_zh(language):
|
| 674 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 675 |
words = clean.split()
|
| 676 |
-
return " ".join(words[:
|
| 677 |
|
| 678 |
|
| 679 |
def _anchor_with_article(anchor: str) -> str:
|
|
@@ -1498,7 +1614,7 @@ def _decomposition_question(
|
|
| 1498 |
"最后只确认今天的需要:你更想要安定身体,还是想理解为什么会这么慌?",
|
| 1499 |
],
|
| 1500 |
"stuck_elevator": [
|
| 1501 |
-
"
|
| 1502 |
"这个卡住更像“来不及开始”,还是“已经按了按钮却没有回应”?",
|
| 1503 |
"最后只确认今天的需要:你想要一个小行动,还是先要一句能让压力降下来的话?",
|
| 1504 |
],
|
|
@@ -1531,8 +1647,8 @@ def _decomposition_question(
|
|
| 1531 |
"Last check before the tip: do you need help calming your body, or understanding why the image felt so intense?",
|
| 1532 |
],
|
| 1533 |
"stuck_elevator": [
|
| 1534 |
-
"Which detail feels closest to your current stuck point
|
| 1535 |
-
"Does the stuckness feel more like being late to start, or like
|
| 1536 |
"Last check before the tip: would one small action help most, or a sentence that lowers the pressure first?",
|
| 1537 |
],
|
| 1538 |
}
|
|
@@ -1685,16 +1801,22 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
|
|
| 1685 |
answer_lines = [line.strip() for line in (answers or "").splitlines() if line.strip()]
|
| 1686 |
if answer_lines:
|
| 1687 |
polished.user_answers = answer_lines
|
| 1688 |
-
intake_anchors =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1689 |
card_anchors = _remove_placeholder_anchors(
|
| 1690 |
polished.dream_anchors
|
| 1691 |
if _is_zh(language)
|
| 1692 |
else _dedupe_preserve_order([_english_anchor_text(anchor) for anchor in polished.dream_anchors])
|
| 1693 |
)
|
|
|
|
| 1694 |
if intake_anchors and not any(_text_uses_anchor(anchor, intake_anchors) for anchor in card_anchors):
|
| 1695 |
anchors = intake_anchors
|
| 1696 |
else:
|
| 1697 |
anchors = card_anchors or intake_anchors
|
|
|
|
| 1698 |
if not anchors:
|
| 1699 |
anchors = _remove_placeholder_anchors([_primary_anchor(intake, language)])
|
| 1700 |
if not anchors:
|
|
@@ -1709,7 +1831,13 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
|
|
| 1709 |
"caring_note",
|
| 1710 |
"safety_note",
|
| 1711 |
):
|
| 1712 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1713 |
if not polished.dream_summary.strip() or _is_placeholder_anchor(polished.dream_summary) or not _text_uses_anchor(polished.dream_summary, anchors):
|
| 1714 |
polished.dream_summary = _summary_from_intake(intake, language)
|
| 1715 |
explicit_question = _extract_explicit_user_question(intake, answers, language)
|
|
@@ -1723,7 +1851,12 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
|
|
| 1723 |
polished.main_question = _main_question_from_intake(intake, language)
|
| 1724 |
emotion_interpretation = _emotion_led_interpretation(intake, answers, anchors, language)
|
| 1725 |
answer_interpretation = _answer_based_interpretation(answers, _answer_bridge_anchor(anchors), language)
|
| 1726 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1727 |
polished.interpretation = emotion_interpretation
|
| 1728 |
elif answer_interpretation:
|
| 1729 |
polished.interpretation = answer_interpretation
|
|
@@ -1745,8 +1878,9 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
|
|
| 1745 |
polished.interpretation = _fallback_interpretation(intake, language)
|
| 1746 |
generic_tip_markers = ["drink water", "hydrate", "多休息", "保持积极", "take a walk"]
|
| 1747 |
emotion_tip = _emotion_led_today_tip(intake, answers, anchors, language)
|
| 1748 |
-
answer_tip
|
| 1749 |
-
|
|
|
|
| 1750 |
polished.today_tip = emotion_tip
|
| 1751 |
elif answer_tip:
|
| 1752 |
polished.today_tip = answer_tip
|
|
@@ -1773,8 +1907,9 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
|
|
| 1773 |
polished.today_tip = _grounded_today_tip(intake, language)
|
| 1774 |
hard_action_markers = ["address it immediately", "fix it immediately", "solve it immediately"]
|
| 1775 |
emotion_action = _emotion_led_tiny_action(intake, answers, anchors, language)
|
| 1776 |
-
answer_action
|
| 1777 |
-
|
|
|
|
| 1778 |
polished.tiny_action = emotion_action
|
| 1779 |
elif answer_action:
|
| 1780 |
polished.tiny_action = answer_action
|
|
|
|
| 175 |
"家门",
|
| 176 |
"旧家",
|
| 177 |
"楼顶",
|
| 178 |
+
"办公楼",
|
| 179 |
+
"高的办公楼",
|
| 180 |
"漆黑的走廊",
|
| 181 |
"漆黑走廊",
|
| 182 |
"黑暗走廊",
|
| 183 |
"走廊",
|
| 184 |
+
"邮件",
|
| 185 |
+
"迟迟没发出去的邮件",
|
| 186 |
"教室",
|
| 187 |
"老师",
|
| 188 |
"交作业",
|
|
|
|
| 198 |
"安全帽",
|
| 199 |
"男生",
|
| 200 |
"电梯按钮",
|
| 201 |
+
"电梯口",
|
| 202 |
+
"小台灯",
|
| 203 |
+
"台灯",
|
| 204 |
"融化的按钮",
|
| 205 |
"按钮融化",
|
| 206 |
"数字 14",
|
| 207 |
"数字14",
|
| 208 |
+
"14 层",
|
| 209 |
+
"14层",
|
| 210 |
"老楼",
|
| 211 |
"桥",
|
| 212 |
"水",
|
|
|
|
| 294 |
"家门": "home door",
|
| 295 |
"旧家": "old home",
|
| 296 |
"楼顶": "rooftop",
|
| 297 |
+
"办公楼": "office building",
|
| 298 |
+
"高的办公楼": "tall office building",
|
| 299 |
"漆黑的走廊": "dark hallway",
|
| 300 |
"漆黑走廊": "dark hallway",
|
| 301 |
"黑暗走廊": "dark hallway",
|
| 302 |
"走廊": "hallway",
|
| 303 |
+
"邮件": "email",
|
| 304 |
+
"迟迟没发出去的邮件": "unsent email",
|
| 305 |
"教室": "classroom",
|
| 306 |
"老师": "teacher",
|
| 307 |
"交作业": "assignment",
|
|
|
|
| 317 |
"安全帽": "hard hat",
|
| 318 |
"男生": "young man",
|
| 319 |
"电梯按钮": "elevator button",
|
| 320 |
+
"电梯口": "elevator doorway",
|
| 321 |
+
"小台灯": "small desk lamp",
|
| 322 |
+
"台灯": "desk lamp",
|
| 323 |
"融化的按钮": "melted button",
|
| 324 |
"按钮融化": "melted button",
|
| 325 |
"数字 14": "floor 14",
|
| 326 |
"数字14": "floor 14",
|
| 327 |
+
"14 层": "floor 14",
|
| 328 |
+
"14层": "floor 14",
|
| 329 |
"楼层数字": "floor number",
|
| 330 |
"楼层": "floor",
|
| 331 |
"老楼": "old apartment building",
|
|
|
|
| 585 |
return clean.strip()
|
| 586 |
|
| 587 |
|
| 588 |
+
def _clean_unsupported_melted_detail(
|
| 589 |
+
text: str,
|
| 590 |
+
intake: DreamIntake,
|
| 591 |
+
anchors: List[str],
|
| 592 |
+
language: str = "en",
|
| 593 |
+
answers: str = "",
|
| 594 |
+
) -> str:
|
| 595 |
+
if not text or _source_mentions_melted_detail(intake, answers):
|
| 596 |
+
return text
|
| 597 |
+
replacement = _story_anchor_phrase(intake, _without_unsupported_melted_anchors(anchors, intake, answers), language, answers)
|
| 598 |
+
if _is_zh(language):
|
| 599 |
+
clean = re.sub(r"电梯[、,, ]+融化的(?:电梯)?按钮[和、,, ]+数字\s*14", replacement, text)
|
| 600 |
+
clean = re.sub(r"电梯[、,, ]+融化的(?:电梯)?按钮[和、,, ]+14\s*层", replacement, clean)
|
| 601 |
+
clean = re.sub(r"电梯[、,, ]+融化的(?:电梯)?按钮[和、,, ]+楼层数字", replacement, clean)
|
| 602 |
+
clean = re.sub(r"(?:电梯)?按钮像蜡一样融化[,,、和 ]*", replacement, clean)
|
| 603 |
+
clean = re.sub(r"融化的(?:电梯)?按钮", replacement, clean)
|
| 604 |
+
clean = re.sub(r"(?:按钮)?像蜡一样融化", replacement, clean)
|
| 605 |
+
clean = clean.replace("按钮融化", replacement).replace("融化", replacement)
|
| 606 |
+
clean = re.sub(rf"(?:{re.escape(replacement)}[、,, ]*){{2,}}", replacement, clean)
|
| 607 |
+
return re.sub(r"\s+", " ", clean).strip()
|
| 608 |
+
clean = re.sub(
|
| 609 |
+
r"the elevator,\s*the melted button,\s*and floor 14",
|
| 610 |
+
replacement,
|
| 611 |
+
text,
|
| 612 |
+
flags=re.IGNORECASE,
|
| 613 |
+
)
|
| 614 |
+
clean = re.sub(
|
| 615 |
+
r"the elevator,\s*melted button,\s*and floor 14",
|
| 616 |
+
replacement,
|
| 617 |
+
clean,
|
| 618 |
+
flags=re.IGNORECASE,
|
| 619 |
+
)
|
| 620 |
+
clean = re.sub(
|
| 621 |
+
r"its button melted like wax,\s*and\s*",
|
| 622 |
+
"",
|
| 623 |
+
clean,
|
| 624 |
+
flags=re.IGNORECASE,
|
| 625 |
+
)
|
| 626 |
+
clean = re.sub(r"the melted (?:elevator )?button", replacement, clean, flags=re.IGNORECASE)
|
| 627 |
+
clean = re.sub(r"melted (?:elevator )?button", replacement, clean, flags=re.IGNORECASE)
|
| 628 |
+
clean = re.sub(r"(?:button|buttons) melted like wax", replacement, clean, flags=re.IGNORECASE)
|
| 629 |
+
clean = re.sub(r"\bmelted\b|\bwax\b|\bwaxy\b", replacement, clean, flags=re.IGNORECASE)
|
| 630 |
+
clean = re.sub(rf"(?:{re.escape(replacement)}[,\s]*){{2,}}", replacement, clean, flags=re.IGNORECASE)
|
| 631 |
+
clean = re.sub(r"\s+", " ", clean)
|
| 632 |
+
return clean.strip()
|
| 633 |
+
|
| 634 |
+
|
| 635 |
def _primary_anchor(intake: DreamIntake, language: str = "en") -> str:
|
| 636 |
anchors = _anchors_for_language(intake, language)
|
| 637 |
if anchors:
|
|
|
|
| 661 |
).lower()
|
| 662 |
|
| 663 |
|
| 664 |
+
def _source_mentions_melted_detail(intake: DreamIntake, answers: str = "") -> bool:
|
| 665 |
+
text = _story_text(intake, answers)
|
| 666 |
+
return any(term in text for term in ["融化", "像蜡", "蜡", "melt", "melted", "melting", "wax", "waxy"])
|
| 667 |
+
|
| 668 |
+
|
| 669 |
+
def _without_unsupported_melted_anchors(
|
| 670 |
+
anchors: List[str],
|
| 671 |
+
intake: DreamIntake,
|
| 672 |
+
answers: str = "",
|
| 673 |
+
) -> List[str]:
|
| 674 |
+
if _source_mentions_melted_detail(intake, answers):
|
| 675 |
+
return anchors
|
| 676 |
+
return [
|
| 677 |
+
anchor
|
| 678 |
+
for anchor in anchors
|
| 679 |
+
if not any(term in anchor.lower() for term in ["融化", "melt", "wax"])
|
| 680 |
+
]
|
| 681 |
+
|
| 682 |
+
|
| 683 |
def _contains_any(text: str, terms: List[str]) -> bool:
|
| 684 |
return any(term in text for term in terms)
|
| 685 |
|
|
|
|
| 716 |
return ", ".join(visible[:-1]) + f", and {visible[-1]}"
|
| 717 |
|
| 718 |
|
| 719 |
+
def _stuck_elevator_anchor_phrase(intake: DreamIntake, anchors: List[str], language: str = "en", answers: str = "") -> str:
|
| 720 |
+
clean_anchors = _without_unsupported_melted_anchors(anchors, intake, answers)
|
| 721 |
+
if not clean_anchors:
|
| 722 |
+
clean_anchors = anchors
|
| 723 |
+
priority_groups = (
|
| 724 |
+
("电梯", "elevator"),
|
| 725 |
+
("14", "floor 14"),
|
| 726 |
+
("走廊", "hallway", "corridor"),
|
| 727 |
+
("邮件", "email"),
|
| 728 |
+
("台灯", "lamp"),
|
| 729 |
+
("按钮", "button"),
|
| 730 |
+
)
|
| 731 |
+
ordered: List[str] = []
|
| 732 |
+
for group in priority_groups:
|
| 733 |
+
for anchor in clean_anchors:
|
| 734 |
+
lowered = anchor.lower()
|
| 735 |
+
if anchor not in ordered and any(term in lowered for term in group):
|
| 736 |
+
ordered.append(anchor)
|
| 737 |
+
for anchor in clean_anchors:
|
| 738 |
+
if anchor not in ordered:
|
| 739 |
+
ordered.append(anchor)
|
| 740 |
+
fallback = "电梯" if _is_zh(language) else "the elevator"
|
| 741 |
+
return _join_anchors(ordered or [fallback], language)
|
| 742 |
+
|
| 743 |
+
|
| 744 |
def _story_anchor_phrase(intake: DreamIntake, anchors: List[str], language: str = "en", answers: str = "") -> str:
|
| 745 |
theme = _dream_theme(intake, answers)
|
| 746 |
+
if theme == "stuck_elevator":
|
| 747 |
+
return _stuck_elevator_anchor_phrase(intake, anchors, language, answers)
|
| 748 |
if _is_zh(language):
|
| 749 |
themed = {
|
| 750 |
"lost_home": "地铁站里迷路的小孩和回家的方向",
|
| 751 |
"dark_water": "夜晚的海、海浪和月牙下的小人",
|
|
|
|
| 752 |
"library_signal": "旧图书馆、红色楼梯和那张便签",
|
| 753 |
"message_loss": "那条消息、前任和突然消失",
|
| 754 |
"chased": "森林、白色猫和空白路牌",
|
|
|
|
| 759 |
themed = {
|
| 760 |
"lost_home": "the lost child, the subway, and the way home",
|
| 761 |
"dark_water": "the dark water, the waves, and the small figure under the moon",
|
|
|
|
| 762 |
"library_signal": "the old library, the red staircase, and the note",
|
| 763 |
"message_loss": "the message, the former partner, and the disappearance",
|
| 764 |
"chased": "the chase, the call for help, and the missing route",
|
|
|
|
| 780 |
return ""
|
| 781 |
clean = re.sub(r"\s+", " ", lines[-1]).strip(" ::「」\"'()[]{}")
|
| 782 |
if _is_zh(language):
|
| 783 |
+
if len(clean) <= 72:
|
| 784 |
+
return clean.rstrip(",。;、 ")
|
| 785 |
+
prefix = clean[:72].rstrip(",;、 ")
|
| 786 |
+
sentence_breaks = [prefix.rfind(mark) for mark in ("。", "!", "?", ";")]
|
| 787 |
+
break_at = max(sentence_breaks)
|
| 788 |
+
if break_at >= 24:
|
| 789 |
+
return prefix[: break_at + 1].strip(",;、 ")
|
| 790 |
+
return prefix.rstrip(",;、 ") + "..."
|
| 791 |
words = clean.split()
|
| 792 |
+
return " ".join(words[:22])
|
| 793 |
|
| 794 |
|
| 795 |
def _anchor_with_article(anchor: str) -> str:
|
|
|
|
| 1614 |
"最后只确认今天的需要:你更想要安定身体,还是想理解为什么会这么慌?",
|
| 1615 |
],
|
| 1616 |
"stuck_elevator": [
|
| 1617 |
+
f"在「{story_anchor}」里,哪一个细节最像你最近卡住的感觉?",
|
| 1618 |
"这个卡住更像“来不及开始”,还是“已经按了按钮却没有回应”?",
|
| 1619 |
"最后只确认今天的需要:你想要一个小行动,还是先要一句能让压力降下来的话?",
|
| 1620 |
],
|
|
|
|
| 1647 |
"Last check before the tip: do you need help calming your body, or understanding why the image felt so intense?",
|
| 1648 |
],
|
| 1649 |
"stuck_elevator": [
|
| 1650 |
+
f"Which detail in {story_anchor} feels closest to your current stuck point?",
|
| 1651 |
+
"Does the stuckness feel more like being late to start, or like trying something and getting no response?",
|
| 1652 |
"Last check before the tip: would one small action help most, or a sentence that lowers the pressure first?",
|
| 1653 |
],
|
| 1654 |
}
|
|
|
|
| 1801 |
answer_lines = [line.strip() for line in (answers or "").splitlines() if line.strip()]
|
| 1802 |
if answer_lines:
|
| 1803 |
polished.user_answers = answer_lines
|
| 1804 |
+
intake_anchors = _without_unsupported_melted_anchors(
|
| 1805 |
+
_remove_placeholder_anchors(_anchors_for_language(intake, language)),
|
| 1806 |
+
intake,
|
| 1807 |
+
answers,
|
| 1808 |
+
)
|
| 1809 |
card_anchors = _remove_placeholder_anchors(
|
| 1810 |
polished.dream_anchors
|
| 1811 |
if _is_zh(language)
|
| 1812 |
else _dedupe_preserve_order([_english_anchor_text(anchor) for anchor in polished.dream_anchors])
|
| 1813 |
)
|
| 1814 |
+
card_anchors = _without_unsupported_melted_anchors(card_anchors, intake, answers)
|
| 1815 |
if intake_anchors and not any(_text_uses_anchor(anchor, intake_anchors) for anchor in card_anchors):
|
| 1816 |
anchors = intake_anchors
|
| 1817 |
else:
|
| 1818 |
anchors = card_anchors or intake_anchors
|
| 1819 |
+
anchors = _without_unsupported_melted_anchors(anchors, intake, answers)
|
| 1820 |
if not anchors:
|
| 1821 |
anchors = _remove_placeholder_anchors([_primary_anchor(intake, language)])
|
| 1822 |
if not anchors:
|
|
|
|
| 1831 |
"caring_note",
|
| 1832 |
"safety_note",
|
| 1833 |
):
|
| 1834 |
+
cleaned_field = _clean_placeholder_phrase(getattr(polished, field))
|
| 1835 |
+
cleaned_field = _clean_unsupported_melted_detail(cleaned_field, intake, anchors, language, answers)
|
| 1836 |
+
setattr(polished, field, cleaned_field)
|
| 1837 |
+
polished.followup_questions = [
|
| 1838 |
+
_clean_unsupported_melted_detail(_clean_placeholder_phrase(question), intake, anchors, language, answers)
|
| 1839 |
+
for question in polished.followup_questions
|
| 1840 |
+
]
|
| 1841 |
if not polished.dream_summary.strip() or _is_placeholder_anchor(polished.dream_summary) or not _text_uses_anchor(polished.dream_summary, anchors):
|
| 1842 |
polished.dream_summary = _summary_from_intake(intake, language)
|
| 1843 |
explicit_question = _extract_explicit_user_question(intake, answers, language)
|
|
|
|
| 1851 |
polished.main_question = _main_question_from_intake(intake, language)
|
| 1852 |
emotion_interpretation = _emotion_led_interpretation(intake, answers, anchors, language)
|
| 1853 |
answer_interpretation = _answer_based_interpretation(answers, _answer_bridge_anchor(anchors), language)
|
| 1854 |
+
answer_tip = _answer_based_today_tip(answers, anchors[0], language)
|
| 1855 |
+
answer_action = _answer_based_tiny_action(answers, language)
|
| 1856 |
+
answer_should_shape_visible_tip = bool((answer_tip or answer_action) and not _needs_comfort(answers, language))
|
| 1857 |
+
if answer_interpretation and answer_should_shape_visible_tip:
|
| 1858 |
+
polished.interpretation = answer_interpretation
|
| 1859 |
+
elif emotion_interpretation:
|
| 1860 |
polished.interpretation = emotion_interpretation
|
| 1861 |
elif answer_interpretation:
|
| 1862 |
polished.interpretation = answer_interpretation
|
|
|
|
| 1878 |
polished.interpretation = _fallback_interpretation(intake, language)
|
| 1879 |
generic_tip_markers = ["drink water", "hydrate", "多休息", "保持积极", "take a walk"]
|
| 1880 |
emotion_tip = _emotion_led_today_tip(intake, answers, anchors, language)
|
| 1881 |
+
if answer_tip and answer_should_shape_visible_tip:
|
| 1882 |
+
polished.today_tip = answer_tip
|
| 1883 |
+
elif emotion_tip:
|
| 1884 |
polished.today_tip = emotion_tip
|
| 1885 |
elif answer_tip:
|
| 1886 |
polished.today_tip = answer_tip
|
|
|
|
| 1907 |
polished.today_tip = _grounded_today_tip(intake, language)
|
| 1908 |
hard_action_markers = ["address it immediately", "fix it immediately", "solve it immediately"]
|
| 1909 |
emotion_action = _emotion_led_tiny_action(intake, answers, anchors, language)
|
| 1910 |
+
if answer_action and answer_should_shape_visible_tip:
|
| 1911 |
+
polished.tiny_action = answer_action
|
| 1912 |
+
elif emotion_action:
|
| 1913 |
polished.tiny_action = emotion_action
|
| 1914 |
elif answer_action:
|
| 1915 |
polished.tiny_action = answer_action
|
dream_customs/runtime_env.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
SPACE_ID = "build-small-hackathon/dream-customs"
|
| 7 |
+
DEFAULT_RUNTIME_ENV_JSON = Path("/tmp/dream-customs-runtime.json")
|
| 8 |
+
RUNTIME_ENV_KEYS = {
|
| 9 |
+
"DREAM_CUSTOMS_TEXT_ENDPOINT",
|
| 10 |
+
"DREAM_CUSTOMS_VISION_ENDPOINT",
|
| 11 |
+
"DREAM_CUSTOMS_ASR_ENDPOINT",
|
| 12 |
+
"DREAM_CUSTOMS_HOSTED_TOKEN",
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def runtime_env_json_path() -> Path:
|
| 17 |
+
return Path(os.getenv("DREAM_CUSTOMS_RUNTIME_ENV_JSON", str(DEFAULT_RUNTIME_ENV_JSON)))
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _runtime_env_autoload_enabled() -> bool:
|
| 21 |
+
value = os.getenv("DREAM_CUSTOMS_DISABLE_RUNTIME_ENV_JSON", "").strip().lower()
|
| 22 |
+
return value not in {"1", "true", "yes", "on"}
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def load_runtime_env_json(path: Path) -> dict:
|
| 26 |
+
if not path.exists():
|
| 27 |
+
return {"loaded": False, "path": str(path), "reason": "missing"}
|
| 28 |
+
try:
|
| 29 |
+
data = json.loads(path.read_text(encoding="utf-8"))
|
| 30 |
+
except (OSError, json.JSONDecodeError) as exc:
|
| 31 |
+
return {"loaded": False, "path": str(path), "reason": exc.__class__.__name__}
|
| 32 |
+
|
| 33 |
+
loaded_keys = []
|
| 34 |
+
for key in sorted(RUNTIME_ENV_KEYS):
|
| 35 |
+
value = str(data.get(key, "")).strip()
|
| 36 |
+
if value:
|
| 37 |
+
os.environ[key] = value
|
| 38 |
+
loaded_keys.append(key)
|
| 39 |
+
return {
|
| 40 |
+
"loaded": bool(loaded_keys),
|
| 41 |
+
"path": str(path),
|
| 42 |
+
"configured_keys": loaded_keys,
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def auto_load_runtime_env_json() -> dict:
|
| 47 |
+
if not _runtime_env_autoload_enabled():
|
| 48 |
+
return {"loaded": False, "reason": "disabled"}
|
| 49 |
+
if os.getenv("PYTEST_CURRENT_TEST") and not os.getenv("DREAM_CUSTOMS_RUNTIME_ENV_JSON"):
|
| 50 |
+
return {"loaded": False, "reason": "pytest"}
|
| 51 |
+
return load_runtime_env_json(runtime_env_json_path())
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def configured_env(space_id: str = SPACE_ID) -> dict:
|
| 55 |
+
return {
|
| 56 |
+
"space_id": os.getenv("SPACE_ID", space_id),
|
| 57 |
+
"text_endpoint_configured": bool(os.getenv("DREAM_CUSTOMS_TEXT_ENDPOINT", "").strip()),
|
| 58 |
+
"vision_endpoint_configured": bool(os.getenv("DREAM_CUSTOMS_VISION_ENDPOINT", "").strip()),
|
| 59 |
+
"asr_endpoint_configured": bool(os.getenv("DREAM_CUSTOMS_ASR_ENDPOINT", "").strip()),
|
| 60 |
+
"hosted_token_configured": bool(os.getenv("DREAM_CUSTOMS_HOSTED_TOKEN", "").strip()),
|
| 61 |
+
}
|
dream_customs/ui/app.py
CHANGED
|
@@ -309,6 +309,73 @@ VOICE_JS = r"""
|
|
| 309 |
const bindComposerControls = () => {
|
| 310 |
bindVoiceButton();
|
| 311 |
bindAttachmentButton();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
};
|
| 313 |
|
| 314 |
bindComposerControls();
|
|
@@ -318,6 +385,32 @@ VOICE_JS = r"""
|
|
| 318 |
"""
|
| 319 |
|
| 320 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
def _load_view(view_json: str) -> dict:
|
| 322 |
try:
|
| 323 |
return json.loads(view_json or "{}")
|
|
@@ -359,6 +452,7 @@ def _updates(state: str, view_json: str):
|
|
| 359 |
view_json,
|
| 360 |
_hero_html(language, status),
|
| 361 |
_notice_html(view),
|
|
|
|
| 362 |
_question_markdown(view, language),
|
| 363 |
view.get("card_html", ""),
|
| 364 |
view.get("card_text", ""),
|
|
@@ -649,8 +743,17 @@ def _attachment_html(language: str = DEFAULT_LANGUAGE) -> str:
|
|
| 649 |
""".strip()
|
| 650 |
|
| 651 |
|
| 652 |
-
def _field_tip_html(language: str = DEFAULT_LANGUAGE) -> str:
|
| 653 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 654 |
|
| 655 |
|
| 656 |
def _processing_html(language: str = DEFAULT_LANGUAGE) -> str:
|
|
@@ -733,7 +836,11 @@ def build_demo() -> gr.Blocks:
|
|
| 733 |
field_tip_html = gr.HTML(_field_tip_html(DEFAULT_LANGUAGE))
|
| 734 |
with gr.Row(elem_classes=["dc-submit-row"]):
|
| 735 |
example_button = gr.Button(initial_copy["example_button"], variant="secondary")
|
| 736 |
-
submit_button = gr.Button(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 737 |
processing_html = gr.HTML(_processing_html(DEFAULT_LANGUAGE))
|
| 738 |
|
| 739 |
with gr.Group(visible=False, elem_classes=["dc-stage", "dc-question"]) as question_group:
|
|
@@ -745,14 +852,30 @@ def build_demo() -> gr.Blocks:
|
|
| 745 |
value="",
|
| 746 |
)
|
| 747 |
with gr.Row(elem_classes=["dc-question-actions"]):
|
| 748 |
-
answer_button = gr.Button(
|
| 749 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 750 |
|
| 751 |
with gr.Group(visible=False, elem_classes=["dc-stage", "dc-card"]) as card_group:
|
| 752 |
card_html = gr.HTML("")
|
| 753 |
with gr.Row(elem_classes=["dc-actions"]):
|
| 754 |
-
gentle_button = gr.Button(
|
| 755 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 756 |
copy_button = gr.Button(initial_copy["copy_button"], variant="secondary")
|
| 757 |
reset_button = gr.Button(initial_copy["reset_button"], variant="secondary")
|
| 758 |
card_text = gr.Textbox(
|
|
@@ -883,6 +1006,7 @@ def build_demo() -> gr.Blocks:
|
|
| 883 |
view_state,
|
| 884 |
hero_html,
|
| 885 |
notice,
|
|
|
|
| 886 |
question_markdown,
|
| 887 |
card_html,
|
| 888 |
card_text,
|
|
@@ -917,6 +1041,7 @@ def build_demo() -> gr.Blocks:
|
|
| 917 |
inputs=[dream_text, image_input, audio_input, mood, language, text_backend, vision_backend] + settings_inputs,
|
| 918 |
outputs=outputs,
|
| 919 |
api_name=False,
|
|
|
|
| 920 |
show_api=False,
|
| 921 |
)
|
| 922 |
answer_button.click(
|
|
@@ -924,6 +1049,7 @@ def build_demo() -> gr.Blocks:
|
|
| 924 |
inputs=[session_state, answer_text, language, text_backend, vision_backend] + settings_inputs,
|
| 925 |
outputs=outputs,
|
| 926 |
api_name=False,
|
|
|
|
| 927 |
show_api=False,
|
| 928 |
)
|
| 929 |
skip_button.click(
|
|
@@ -931,6 +1057,7 @@ def build_demo() -> gr.Blocks:
|
|
| 931 |
inputs=[session_state, language, text_backend, vision_backend] + settings_inputs,
|
| 932 |
outputs=outputs,
|
| 933 |
api_name=False,
|
|
|
|
| 934 |
show_api=False,
|
| 935 |
)
|
| 936 |
gentle_button.click(
|
|
@@ -938,6 +1065,7 @@ def build_demo() -> gr.Blocks:
|
|
| 938 |
inputs=[session_state, gr.State("softer"), language, text_backend, vision_backend] + settings_inputs,
|
| 939 |
outputs=outputs,
|
| 940 |
api_name=False,
|
|
|
|
| 941 |
show_api=False,
|
| 942 |
)
|
| 943 |
weird_button.click(
|
|
@@ -945,9 +1073,17 @@ def build_demo() -> gr.Blocks:
|
|
| 945 |
inputs=[session_state, gr.State("stranger"), language, text_backend, vision_backend] + settings_inputs,
|
| 946 |
outputs=outputs,
|
| 947 |
api_name=False,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 948 |
show_api=False,
|
| 949 |
)
|
| 950 |
-
copy_button.click(lambda text: text, inputs=card_text, outputs=card_text, api_name=False, show_api=False)
|
| 951 |
reset_button.click(
|
| 952 |
_reset,
|
| 953 |
inputs=[language, text_backend, vision_backend] + settings_inputs,
|
|
|
|
| 309 |
const bindComposerControls = () => {
|
| 310 |
bindVoiceButton();
|
| 311 |
bindAttachmentButton();
|
| 312 |
+
bindProcessingButtons();
|
| 313 |
+
};
|
| 314 |
+
|
| 315 |
+
const currentLanguage = () => {
|
| 316 |
+
const micButton = document.querySelector(".dc-mic-button");
|
| 317 |
+
if (micButton?.dataset.language) {
|
| 318 |
+
return micButton.dataset.language;
|
| 319 |
+
}
|
| 320 |
+
return document.body.innerText.includes("梦境问答台") ? "zh" : "en";
|
| 321 |
+
};
|
| 322 |
+
|
| 323 |
+
const setProcessingCopy = (mode) => {
|
| 324 |
+
const isZh = currentLanguage() === "zh";
|
| 325 |
+
const copy = mode === "submit"
|
| 326 |
+
? {
|
| 327 |
+
notice: isZh
|
| 328 |
+
? "正在整理梦境线索。下一步会生成一个贴着细节的温和追问。"
|
| 329 |
+
: "Reading the dream details. Next, Dream QA will ask one grounded question.",
|
| 330 |
+
note: isZh
|
| 331 |
+
? "正在提取人物、地点、情绪和具体物件;如果生成较慢,通常需要十几秒。"
|
| 332 |
+
: "Extracting people, places, feelings, and concrete objects. A slower run can take several seconds.",
|
| 333 |
+
}
|
| 334 |
+
: mode === "question"
|
| 335 |
+
? {
|
| 336 |
+
notice: isZh
|
| 337 |
+
? "正在换一个追问角度。"
|
| 338 |
+
: "Asking from another angle.",
|
| 339 |
+
note: isZh
|
| 340 |
+
? "正在保留你的回答和梦境锚点,只重新生成一个更贴近当下的追问。"
|
| 341 |
+
: "Keeping your answer and dream anchors while writing one more grounded question.",
|
| 342 |
+
}
|
| 343 |
+
: {
|
| 344 |
+
notice: isZh
|
| 345 |
+
? "正在把追问回答整理进今日小 Tips。"
|
| 346 |
+
: "Folding the follow-up answer into the Today Tip.",
|
| 347 |
+
note: isZh
|
| 348 |
+
? "正在检查梦境锚点、你的回答和安全边界,然后生成一个可复制结果。"
|
| 349 |
+
: "Checking dream anchors, your answer, and safety boundaries before writing the copyable result.",
|
| 350 |
+
};
|
| 351 |
+
|
| 352 |
+
document.querySelectorAll(".dc-notice").forEach((el) => {
|
| 353 |
+
el.textContent = copy.notice;
|
| 354 |
+
el.classList.remove("is-error");
|
| 355 |
+
el.classList.add("is-processing");
|
| 356 |
+
});
|
| 357 |
+
document.querySelectorAll(".dc-processing-note").forEach((el) => {
|
| 358 |
+
el.textContent = copy.note;
|
| 359 |
+
el.classList.add("is-active");
|
| 360 |
+
});
|
| 361 |
+
};
|
| 362 |
+
|
| 363 |
+
const bindProcessingButtons = () => {
|
| 364 |
+
const bindButton = (selector, mode) => {
|
| 365 |
+
document.querySelectorAll(selector).forEach((root) => {
|
| 366 |
+
const button = root.matches("button") ? root : root.querySelector("button");
|
| 367 |
+
if (!button || button.dataset.processingBound === "true") {
|
| 368 |
+
return;
|
| 369 |
+
}
|
| 370 |
+
button.dataset.processingBound = "true";
|
| 371 |
+
button.addEventListener("click", () => setProcessingCopy(mode));
|
| 372 |
+
});
|
| 373 |
+
};
|
| 374 |
+
|
| 375 |
+
bindButton(".dc-submit-button", "submit");
|
| 376 |
+
bindButton(".dc-answer-button", "tip");
|
| 377 |
+
bindButton(".dc-tip-button", "tip");
|
| 378 |
+
bindButton(".dc-question-button", "question");
|
| 379 |
};
|
| 380 |
|
| 381 |
bindComposerControls();
|
|
|
|
| 385 |
"""
|
| 386 |
|
| 387 |
|
| 388 |
+
COPY_RESULT_JS = """
|
| 389 |
+
(text) => {
|
| 390 |
+
const value = text || "";
|
| 391 |
+
const showNotice = (message, isError = false) => {
|
| 392 |
+
document.querySelectorAll(".dc-notice").forEach((el) => {
|
| 393 |
+
el.textContent = message;
|
| 394 |
+
el.classList.toggle("is-error", isError);
|
| 395 |
+
el.classList.remove("is-processing");
|
| 396 |
+
});
|
| 397 |
+
};
|
| 398 |
+
if (!value.trim()) {
|
| 399 |
+
showNotice("Nothing to copy yet. Generate a Today Tip first. / 还没有可复制结果,请先生成今日小 Tips。", true);
|
| 400 |
+
return value;
|
| 401 |
+
}
|
| 402 |
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
| 403 |
+
navigator.clipboard.writeText(value)
|
| 404 |
+
.then(() => showNotice("Result copied. / 已复制结果。"))
|
| 405 |
+
.catch(() => showNotice("Copy was blocked by the browser. Use the Copy button inside the text box. / 浏览器阻止复制时,请使用文本框内置 Copy。", true));
|
| 406 |
+
} else {
|
| 407 |
+
showNotice("This browser blocked direct copy. Use the Copy button inside the text box. / 浏览器阻止复制时,请使用文本框内置 Copy。", true);
|
| 408 |
+
}
|
| 409 |
+
return value;
|
| 410 |
+
}
|
| 411 |
+
"""
|
| 412 |
+
|
| 413 |
+
|
| 414 |
def _load_view(view_json: str) -> dict:
|
| 415 |
try:
|
| 416 |
return json.loads(view_json or "{}")
|
|
|
|
| 452 |
view_json,
|
| 453 |
_hero_html(language, status),
|
| 454 |
_notice_html(view),
|
| 455 |
+
_field_tip_html(language, view),
|
| 456 |
_question_markdown(view, language),
|
| 457 |
view.get("card_html", ""),
|
| 458 |
view.get("card_text", ""),
|
|
|
|
| 743 |
""".strip()
|
| 744 |
|
| 745 |
|
| 746 |
+
def _field_tip_html(language: str = DEFAULT_LANGUAGE, view=None) -> str:
|
| 747 |
+
copy = copy_for(language)
|
| 748 |
+
view = view or {}
|
| 749 |
+
if view.get("status") == "error":
|
| 750 |
+
message = (
|
| 751 |
+
"Dream note needs at least one sentence, image, or voice clue before Continue."
|
| 752 |
+
if normalize_language(language) == "en"
|
| 753 |
+
else "梦境记录需要至少一句文字、图片或语音线索,然后再继续。"
|
| 754 |
+
)
|
| 755 |
+
return f'<p class="dc-field-tip is-error" role="alert">{escape(message)}</p>'
|
| 756 |
+
return f"<p class=\"dc-field-tip\">{escape(copy['field_tip'])}</p>"
|
| 757 |
|
| 758 |
|
| 759 |
def _processing_html(language: str = DEFAULT_LANGUAGE) -> str:
|
|
|
|
| 836 |
field_tip_html = gr.HTML(_field_tip_html(DEFAULT_LANGUAGE))
|
| 837 |
with gr.Row(elem_classes=["dc-submit-row"]):
|
| 838 |
example_button = gr.Button(initial_copy["example_button"], variant="secondary")
|
| 839 |
+
submit_button = gr.Button(
|
| 840 |
+
initial_copy["submit_button"],
|
| 841 |
+
variant="primary",
|
| 842 |
+
elem_classes=["dc-submit-button"],
|
| 843 |
+
)
|
| 844 |
processing_html = gr.HTML(_processing_html(DEFAULT_LANGUAGE))
|
| 845 |
|
| 846 |
with gr.Group(visible=False, elem_classes=["dc-stage", "dc-question"]) as question_group:
|
|
|
|
| 852 |
value="",
|
| 853 |
)
|
| 854 |
with gr.Row(elem_classes=["dc-question-actions"]):
|
| 855 |
+
answer_button = gr.Button(
|
| 856 |
+
initial_copy["answer_button"],
|
| 857 |
+
variant="primary",
|
| 858 |
+
elem_classes=["dc-answer-button"],
|
| 859 |
+
)
|
| 860 |
+
skip_button = gr.Button(
|
| 861 |
+
initial_copy["skip_button"],
|
| 862 |
+
variant="secondary",
|
| 863 |
+
elem_classes=["dc-tip-button"],
|
| 864 |
+
)
|
| 865 |
|
| 866 |
with gr.Group(visible=False, elem_classes=["dc-stage", "dc-card"]) as card_group:
|
| 867 |
card_html = gr.HTML("")
|
| 868 |
with gr.Row(elem_classes=["dc-actions"]):
|
| 869 |
+
gentle_button = gr.Button(
|
| 870 |
+
initial_copy["ask_again_button"],
|
| 871 |
+
variant="secondary",
|
| 872 |
+
elem_classes=["dc-question-button"],
|
| 873 |
+
)
|
| 874 |
+
weird_button = gr.Button(
|
| 875 |
+
initial_copy["angle_button"],
|
| 876 |
+
variant="secondary",
|
| 877 |
+
elem_classes=["dc-question-button"],
|
| 878 |
+
)
|
| 879 |
copy_button = gr.Button(initial_copy["copy_button"], variant="secondary")
|
| 880 |
reset_button = gr.Button(initial_copy["reset_button"], variant="secondary")
|
| 881 |
card_text = gr.Textbox(
|
|
|
|
| 1006 |
view_state,
|
| 1007 |
hero_html,
|
| 1008 |
notice,
|
| 1009 |
+
field_tip_html,
|
| 1010 |
question_markdown,
|
| 1011 |
card_html,
|
| 1012 |
card_text,
|
|
|
|
| 1041 |
inputs=[dream_text, image_input, audio_input, mood, language, text_backend, vision_backend] + settings_inputs,
|
| 1042 |
outputs=outputs,
|
| 1043 |
api_name=False,
|
| 1044 |
+
scroll_to_output=True,
|
| 1045 |
show_api=False,
|
| 1046 |
)
|
| 1047 |
answer_button.click(
|
|
|
|
| 1049 |
inputs=[session_state, answer_text, language, text_backend, vision_backend] + settings_inputs,
|
| 1050 |
outputs=outputs,
|
| 1051 |
api_name=False,
|
| 1052 |
+
scroll_to_output=True,
|
| 1053 |
show_api=False,
|
| 1054 |
)
|
| 1055 |
skip_button.click(
|
|
|
|
| 1057 |
inputs=[session_state, language, text_backend, vision_backend] + settings_inputs,
|
| 1058 |
outputs=outputs,
|
| 1059 |
api_name=False,
|
| 1060 |
+
scroll_to_output=True,
|
| 1061 |
show_api=False,
|
| 1062 |
)
|
| 1063 |
gentle_button.click(
|
|
|
|
| 1065 |
inputs=[session_state, gr.State("softer"), language, text_backend, vision_backend] + settings_inputs,
|
| 1066 |
outputs=outputs,
|
| 1067 |
api_name=False,
|
| 1068 |
+
scroll_to_output=True,
|
| 1069 |
show_api=False,
|
| 1070 |
)
|
| 1071 |
weird_button.click(
|
|
|
|
| 1073 |
inputs=[session_state, gr.State("stranger"), language, text_backend, vision_backend] + settings_inputs,
|
| 1074 |
outputs=outputs,
|
| 1075 |
api_name=False,
|
| 1076 |
+
scroll_to_output=True,
|
| 1077 |
+
show_api=False,
|
| 1078 |
+
)
|
| 1079 |
+
copy_button.click(
|
| 1080 |
+
lambda text: text,
|
| 1081 |
+
inputs=card_text,
|
| 1082 |
+
outputs=card_text,
|
| 1083 |
+
api_name=False,
|
| 1084 |
+
js=COPY_RESULT_JS,
|
| 1085 |
show_api=False,
|
| 1086 |
)
|
|
|
|
| 1087 |
reset_button.click(
|
| 1088 |
_reset,
|
| 1089 |
inputs=[language, text_backend, vision_backend] + settings_inputs,
|
dream_customs/ui/copy.py
CHANGED
|
@@ -44,8 +44,8 @@ APP_COPY = {
|
|
| 44 |
"answer_placeholder": "Write one answer, or leave it blank and skip.",
|
| 45 |
"answer_button": "Send answer",
|
| 46 |
"skip_button": "Skip and generate tip",
|
| 47 |
-
"ask_again_button": "Ask
|
| 48 |
-
"angle_button": "
|
| 49 |
"copy_button": "Copy result",
|
| 50 |
"reset_button": "Start over",
|
| 51 |
"copy_label": "Copyable result",
|
|
@@ -100,8 +100,8 @@ APP_COPY = {
|
|
| 100 |
"answer_placeholder": "写一句回答,或留空后选择跳过。",
|
| 101 |
"answer_button": "发送回答",
|
| 102 |
"skip_button": "跳过,生成 Tips",
|
| 103 |
-
"ask_again_button": "再问一
|
| 104 |
-
"angle_button": "换个角度",
|
| 105 |
"copy_button": "复制结果",
|
| 106 |
"reset_button": "重新开始",
|
| 107 |
"copy_label": "可复制结果",
|
|
|
|
| 44 |
"answer_placeholder": "Write one answer, or leave it blank and skip.",
|
| 45 |
"answer_button": "Send answer",
|
| 46 |
"skip_button": "Skip and generate tip",
|
| 47 |
+
"ask_again_button": "Ask one more question",
|
| 48 |
+
"angle_button": "Ask from another angle",
|
| 49 |
"copy_button": "Copy result",
|
| 50 |
"reset_button": "Start over",
|
| 51 |
"copy_label": "Copyable result",
|
|
|
|
| 100 |
"answer_placeholder": "写一句回答,或留空后选择跳过。",
|
| 101 |
"answer_button": "发送回答",
|
| 102 |
"skip_button": "跳过,生成 Tips",
|
| 103 |
+
"ask_again_button": "再追问一下",
|
| 104 |
+
"angle_button": "换个追问角度",
|
| 105 |
"copy_button": "复制结果",
|
| 106 |
"reset_button": "重新开始",
|
| 107 |
"copy_label": "可复制结果",
|
dream_customs/ui/styles.py
CHANGED
|
@@ -519,6 +519,15 @@ body,
|
|
| 519 |
margin: 12px 0 0;
|
| 520 |
}
|
| 521 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 522 |
.dc-side-panel .wrap,
|
| 523 |
.dc-side-panel .container,
|
| 524 |
.dc-side-panel select {
|
|
@@ -612,6 +621,39 @@ button.secondary {
|
|
| 612 |
max-width: 44rem;
|
| 613 |
}
|
| 614 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 615 |
.dc-processing-note::before {
|
| 616 |
color: var(--dc-coral-dark);
|
| 617 |
content: "While the stamp dries: ";
|
|
@@ -753,6 +795,12 @@ button.secondary {
|
|
| 753 |
color: #9f321c;
|
| 754 |
}
|
| 755 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 756 |
.dc-pass-card {
|
| 757 |
background:
|
| 758 |
linear-gradient(180deg, rgba(255, 252, 246, 0.98), rgba(246, 237, 222, 0.98));
|
|
|
|
| 519 |
margin: 12px 0 0;
|
| 520 |
}
|
| 521 |
|
| 522 |
+
.dc-field-tip.is-error {
|
| 523 |
+
background: rgba(255, 240, 235, 0.95);
|
| 524 |
+
border: 1px solid #efb19d;
|
| 525 |
+
border-radius: 10px;
|
| 526 |
+
color: #9f321c;
|
| 527 |
+
margin-top: 12px;
|
| 528 |
+
padding: 10px 12px;
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
.dc-side-panel .wrap,
|
| 532 |
.dc-side-panel .container,
|
| 533 |
.dc-side-panel select {
|
|
|
|
| 621 |
max-width: 44rem;
|
| 622 |
}
|
| 623 |
|
| 624 |
+
.dc-processing-note.is-active {
|
| 625 |
+
background: rgba(244, 201, 94, 0.16);
|
| 626 |
+
border: 1px solid rgba(191, 133, 35, 0.28);
|
| 627 |
+
border-radius: 10px;
|
| 628 |
+
color: var(--dc-ink);
|
| 629 |
+
padding: 10px 12px 10px 36px;
|
| 630 |
+
position: relative;
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
.dc-processing-note.is-active::before {
|
| 634 |
+
animation: dc-processing-pulse 1.1s ease-in-out infinite;
|
| 635 |
+
background: var(--dc-sage);
|
| 636 |
+
border-radius: 999px;
|
| 637 |
+
content: "";
|
| 638 |
+
height: 10px;
|
| 639 |
+
left: 14px;
|
| 640 |
+
position: absolute;
|
| 641 |
+
top: 16px;
|
| 642 |
+
width: 10px;
|
| 643 |
+
}
|
| 644 |
+
|
| 645 |
+
@keyframes dc-processing-pulse {
|
| 646 |
+
0%,
|
| 647 |
+
100% {
|
| 648 |
+
opacity: 0.45;
|
| 649 |
+
transform: scale(0.82);
|
| 650 |
+
}
|
| 651 |
+
50% {
|
| 652 |
+
opacity: 1;
|
| 653 |
+
transform: scale(1);
|
| 654 |
+
}
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
.dc-processing-note::before {
|
| 658 |
color: var(--dc-coral-dark);
|
| 659 |
content: "While the stamp dries: ";
|
|
|
|
| 795 |
color: #9f321c;
|
| 796 |
}
|
| 797 |
|
| 798 |
+
.dc-notice.is-processing {
|
| 799 |
+
background: rgba(244, 201, 94, 0.18);
|
| 800 |
+
border-color: rgba(191, 133, 35, 0.32);
|
| 801 |
+
color: var(--dc-ink);
|
| 802 |
+
}
|
| 803 |
+
|
| 804 |
.dc-pass-card {
|
| 805 |
background:
|
| 806 |
linear-gradient(180deg, rgba(255, 252, 246, 0.98), rgba(246, 237, 222, 0.98));
|
scripts/local_space_mirror.py
CHANGED
|
@@ -9,16 +9,17 @@ import os
|
|
| 9 |
import sys
|
| 10 |
from pathlib import Path
|
| 11 |
|
| 12 |
-
|
| 13 |
ROOT = Path(__file__).resolve().parents[1]
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
| 22 |
|
| 23 |
|
| 24 |
def _repo_root_on_path() -> None:
|
|
@@ -27,37 +28,6 @@ def _repo_root_on_path() -> None:
|
|
| 27 |
sys.path.insert(0, root)
|
| 28 |
|
| 29 |
|
| 30 |
-
def _configured_env() -> dict:
|
| 31 |
-
return {
|
| 32 |
-
"space_id": os.getenv("SPACE_ID", SPACE_ID),
|
| 33 |
-
"text_endpoint_configured": bool(os.getenv("DREAM_CUSTOMS_TEXT_ENDPOINT", "").strip()),
|
| 34 |
-
"vision_endpoint_configured": bool(os.getenv("DREAM_CUSTOMS_VISION_ENDPOINT", "").strip()),
|
| 35 |
-
"asr_endpoint_configured": bool(os.getenv("DREAM_CUSTOMS_ASR_ENDPOINT", "").strip()),
|
| 36 |
-
"hosted_token_configured": bool(os.getenv("DREAM_CUSTOMS_HOSTED_TOKEN", "").strip()),
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
def load_runtime_env_json(path: Path) -> dict:
|
| 41 |
-
if not path.exists():
|
| 42 |
-
return {"loaded": False, "path": str(path), "reason": "missing"}
|
| 43 |
-
try:
|
| 44 |
-
data = json.loads(path.read_text(encoding="utf-8"))
|
| 45 |
-
except (OSError, json.JSONDecodeError) as exc:
|
| 46 |
-
return {"loaded": False, "path": str(path), "reason": exc.__class__.__name__}
|
| 47 |
-
|
| 48 |
-
loaded_keys = []
|
| 49 |
-
for key in sorted(RUNTIME_ENV_KEYS):
|
| 50 |
-
value = str(data.get(key, "")).strip()
|
| 51 |
-
if value:
|
| 52 |
-
os.environ[key] = value
|
| 53 |
-
loaded_keys.append(key)
|
| 54 |
-
return {
|
| 55 |
-
"loaded": bool(loaded_keys),
|
| 56 |
-
"path": str(path),
|
| 57 |
-
"configured_keys": loaded_keys,
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
|
| 61 |
def mirror_manifest(host: str, port: int) -> dict:
|
| 62 |
_repo_root_on_path()
|
| 63 |
from dream_customs.defaults import DEFAULT_ASR_BACKEND, DEFAULT_TEXT_BACKEND, DEFAULT_VISION_BACKEND
|
|
@@ -72,7 +42,7 @@ def mirror_manifest(host: str, port: int) -> dict:
|
|
| 72 |
"vision": DEFAULT_VISION_BACKEND,
|
| 73 |
"asr": DEFAULT_ASR_BACKEND,
|
| 74 |
},
|
| 75 |
-
"env":
|
| 76 |
"notes": [
|
| 77 |
"Uses the same app.py/build_demo path as the Hugging Face Space.",
|
| 78 |
"Missing hosted endpoint secrets fall back to deterministic demo behavior.",
|
|
|
|
| 9 |
import sys
|
| 10 |
from pathlib import Path
|
| 11 |
|
|
|
|
| 12 |
ROOT = Path(__file__).resolve().parents[1]
|
| 13 |
+
root = str(ROOT)
|
| 14 |
+
if root not in sys.path:
|
| 15 |
+
sys.path.insert(0, root)
|
| 16 |
+
|
| 17 |
+
from dream_customs.runtime_env import (
|
| 18 |
+
DEFAULT_RUNTIME_ENV_JSON,
|
| 19 |
+
SPACE_ID,
|
| 20 |
+
configured_env,
|
| 21 |
+
load_runtime_env_json,
|
| 22 |
+
)
|
| 23 |
|
| 24 |
|
| 25 |
def _repo_root_on_path() -> None:
|
|
|
|
| 28 |
sys.path.insert(0, root)
|
| 29 |
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
def mirror_manifest(host: str, port: int) -> dict:
|
| 32 |
_repo_root_on_path()
|
| 33 |
from dream_customs.defaults import DEFAULT_ASR_BACKEND, DEFAULT_TEXT_BACKEND, DEFAULT_VISION_BACKEND
|
|
|
|
| 42 |
"vision": DEFAULT_VISION_BACKEND,
|
| 43 |
"asr": DEFAULT_ASR_BACKEND,
|
| 44 |
},
|
| 45 |
+
"env": configured_env(SPACE_ID),
|
| 46 |
"notes": [
|
| 47 |
"Uses the same app.py/build_demo path as the Hugging Face Space.",
|
| 48 |
"Missing hosted endpoint secrets fall back to deterministic demo behavior.",
|
tests/test_local_space_mirror.py
CHANGED
|
@@ -1,10 +1,20 @@
|
|
| 1 |
import json
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
from scripts.local_space_mirror import load_runtime_env_json, mirror_manifest
|
| 4 |
from scripts.smoke_local_space_mirror import inspect_config
|
| 5 |
|
| 6 |
|
| 7 |
-
def test_local_space_mirror_manifest_matches_space_entrypoint():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
manifest = mirror_manifest("127.0.0.1", 7862)
|
| 9 |
|
| 10 |
assert manifest["mode"] == "local-space-mirror"
|
|
@@ -61,6 +71,58 @@ def test_local_space_mirror_loads_runtime_env_json_without_printing_values(tmp_p
|
|
| 61 |
assert "super-secret-token" not in serialized
|
| 62 |
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
def test_local_space_mirror_config_smoke_requires_composer_debug_markers():
|
| 65 |
config = {
|
| 66 |
"title": "Dream QA",
|
|
|
|
| 1 |
import json
|
| 2 |
+
import os
|
| 3 |
+
import subprocess
|
| 4 |
+
import sys
|
| 5 |
|
| 6 |
from scripts.local_space_mirror import load_runtime_env_json, mirror_manifest
|
| 7 |
from scripts.smoke_local_space_mirror import inspect_config
|
| 8 |
|
| 9 |
|
| 10 |
+
def test_local_space_mirror_manifest_matches_space_entrypoint(monkeypatch):
|
| 11 |
+
for key in [
|
| 12 |
+
"DREAM_CUSTOMS_TEXT_ENDPOINT",
|
| 13 |
+
"DREAM_CUSTOMS_VISION_ENDPOINT",
|
| 14 |
+
"DREAM_CUSTOMS_ASR_ENDPOINT",
|
| 15 |
+
"DREAM_CUSTOMS_HOSTED_TOKEN",
|
| 16 |
+
]:
|
| 17 |
+
monkeypatch.delenv(key, raising=False)
|
| 18 |
manifest = mirror_manifest("127.0.0.1", 7862)
|
| 19 |
|
| 20 |
assert manifest["mode"] == "local-space-mirror"
|
|
|
|
| 71 |
assert "super-secret-token" not in serialized
|
| 72 |
|
| 73 |
|
| 74 |
+
def test_app_import_auto_loads_local_runtime_env_json(tmp_path):
|
| 75 |
+
runtime_json = tmp_path / "runtime.json"
|
| 76 |
+
runtime_json.write_text(
|
| 77 |
+
json.dumps(
|
| 78 |
+
{
|
| 79 |
+
"DREAM_CUSTOMS_TEXT_ENDPOINT": "https://example.test/text",
|
| 80 |
+
"DREAM_CUSTOMS_VISION_ENDPOINT": "https://example.test/vision",
|
| 81 |
+
"DREAM_CUSTOMS_ASR_ENDPOINT": "https://example.test/asr",
|
| 82 |
+
"DREAM_CUSTOMS_HOSTED_TOKEN": "super-secret-token",
|
| 83 |
+
}
|
| 84 |
+
),
|
| 85 |
+
encoding="utf-8",
|
| 86 |
+
)
|
| 87 |
+
env = os.environ.copy()
|
| 88 |
+
env.pop("PYTEST_CURRENT_TEST", None)
|
| 89 |
+
for key in [
|
| 90 |
+
"DREAM_CUSTOMS_TEXT_ENDPOINT",
|
| 91 |
+
"DREAM_CUSTOMS_VISION_ENDPOINT",
|
| 92 |
+
"DREAM_CUSTOMS_ASR_ENDPOINT",
|
| 93 |
+
"DREAM_CUSTOMS_HOSTED_TOKEN",
|
| 94 |
+
]:
|
| 95 |
+
env.pop(key, None)
|
| 96 |
+
env["DREAM_CUSTOMS_RUNTIME_ENV_JSON"] = str(runtime_json)
|
| 97 |
+
result = subprocess.run(
|
| 98 |
+
[
|
| 99 |
+
sys.executable,
|
| 100 |
+
"-c",
|
| 101 |
+
(
|
| 102 |
+
"import json, os; import app; "
|
| 103 |
+
"print(json.dumps({key: bool(os.getenv(key)) for key in "
|
| 104 |
+
"['DREAM_CUSTOMS_TEXT_ENDPOINT','DREAM_CUSTOMS_VISION_ENDPOINT',"
|
| 105 |
+
"'DREAM_CUSTOMS_ASR_ENDPOINT','DREAM_CUSTOMS_HOSTED_TOKEN']}))"
|
| 106 |
+
),
|
| 107 |
+
],
|
| 108 |
+
check=True,
|
| 109 |
+
capture_output=True,
|
| 110 |
+
text=True,
|
| 111 |
+
env=env,
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
loaded = json.loads(result.stdout.strip())
|
| 115 |
+
|
| 116 |
+
assert loaded == {
|
| 117 |
+
"DREAM_CUSTOMS_TEXT_ENDPOINT": True,
|
| 118 |
+
"DREAM_CUSTOMS_VISION_ENDPOINT": True,
|
| 119 |
+
"DREAM_CUSTOMS_ASR_ENDPOINT": True,
|
| 120 |
+
"DREAM_CUSTOMS_HOSTED_TOKEN": True,
|
| 121 |
+
}
|
| 122 |
+
assert "https://example.test" not in result.stdout
|
| 123 |
+
assert "super-secret-token" not in result.stdout
|
| 124 |
+
|
| 125 |
+
|
| 126 |
def test_local_space_mirror_config_smoke_requires_composer_debug_markers():
|
| 127 |
config = {
|
| 128 |
"title": "Dream QA",
|
tests/test_ollama_models.py
CHANGED
|
@@ -242,3 +242,44 @@ def test_hosted_vision_client_accepts_partial_witness_json():
|
|
| 242 |
assert "Object: elevator button" in clues
|
| 243 |
assert "Visible text: 14" in clues
|
| 244 |
assert "Mood cue: cold" in clues
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
assert "Object: elevator button" in clues
|
| 243 |
assert "Visible text: 14" in clues
|
| 244 |
assert "Mood cue: cold" in clues
|
| 245 |
+
|
| 246 |
+
|
| 247 |
+
def test_hosted_vision_client_extracts_modal_pipeline_assistant_content():
|
| 248 |
+
class StubHostedNestedVisionClient(HostedMiniCPMVisionClient):
|
| 249 |
+
def _post_image(self, image_path):
|
| 250 |
+
return {
|
| 251 |
+
"response": str(
|
| 252 |
+
{
|
| 253 |
+
"input_text": [
|
| 254 |
+
{
|
| 255 |
+
"role": "user",
|
| 256 |
+
"content": [
|
| 257 |
+
{"type": "image", "path": "/tmp/demo.png"},
|
| 258 |
+
{"type": "text", "text": "Describe the sketch."},
|
| 259 |
+
],
|
| 260 |
+
}
|
| 261 |
+
],
|
| 262 |
+
"generated_text": [
|
| 263 |
+
{"role": "user", "content": [{"type": "text", "text": "Describe the sketch."}]},
|
| 264 |
+
{
|
| 265 |
+
"role": "assistant",
|
| 266 |
+
"content": (
|
| 267 |
+
'{"scene_summary":"A child is lost at a subway station.",'
|
| 268 |
+
'"objects":["child figure","subway sign","arrow"],'
|
| 269 |
+
'"visible_text":["SUBWAY","HOME ?"],'
|
| 270 |
+
'"spatial_relations":"child in front of sign",'
|
| 271 |
+
'"mood_cues":"lost or searching",'
|
| 272 |
+
'"uncertain_details":"where home is",'
|
| 273 |
+
'"surprising_detail":"the sign asks HOME ?"}'
|
| 274 |
+
),
|
| 275 |
+
},
|
| 276 |
+
],
|
| 277 |
+
}
|
| 278 |
+
)
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
clues = StubHostedNestedVisionClient(endpoint="https://example.test").extract_witness("demo.png").to_visual_clues()
|
| 282 |
+
|
| 283 |
+
assert "Scene: A child is lost at a subway station." in clues
|
| 284 |
+
assert "Object: child figure" in clues
|
| 285 |
+
assert "Visible text: HOME ?" in clues
|
tests/test_pipeline.py
CHANGED
|
@@ -178,6 +178,99 @@ def test_generate_today_tip_keeps_answer_history_and_removes_placeholder_text():
|
|
| 178 |
assert "does not indicate any real-life concerns" not in combined
|
| 179 |
|
| 180 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
def test_generate_today_tip_follows_user_question_and_comfort_need_in_chinese():
|
| 182 |
intake = build_intake(
|
| 183 |
dream_text="我梦到自己掉进海里,醒来很害怕。我想知道这是不是说明我快撑不住了?",
|
|
@@ -449,6 +542,29 @@ def test_ask_answer_skip_draft_revise_and_seal_actions():
|
|
| 449 |
assert session.sealed_pact == session.draft_pact
|
| 450 |
|
| 451 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 452 |
def test_draft_pact_falls_back_to_legacy_generate_pact_client():
|
| 453 |
class LegacyPactOnlyClient:
|
| 454 |
def generate_pact(self, prompt):
|
|
|
|
| 178 |
assert "does not indicate any real-life concerns" not in combined
|
| 179 |
|
| 180 |
|
| 181 |
+
def test_elevator_floor_14_without_melted_button_does_not_invent_example_detail():
|
| 182 |
+
class ContaminatedElevatorClient:
|
| 183 |
+
def generate_today_tip(self, prompt):
|
| 184 |
+
return TodayTipCard(
|
| 185 |
+
dream_summary="你梦见电梯按钮像蜡一样融化,楼层数字停在 14。",
|
| 186 |
+
main_question="为什么梦里总是卡在电梯口?",
|
| 187 |
+
dream_anchors=["电梯", "融化的按钮", "数字 14"],
|
| 188 |
+
followup_questions=["电梯、融化的按钮和数字 14 里,哪一个最像你最近卡住的感觉?"],
|
| 189 |
+
user_answers=[],
|
| 190 |
+
interpretation="第二层,梦里的「电梯、融化的按钮和数字 14」也许把这种感受变成了画面。",
|
| 191 |
+
today_tip="今天不要逼自己抵达所有楼层;先把「电梯、融化的按钮和数字 14」变成一个只按一次的按钮。",
|
| 192 |
+
tiny_action="用 5 分钟写一个只按这一层的按钮。",
|
| 193 |
+
caring_note="不用解决所有楼层。",
|
| 194 |
+
safety_note="",
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
dream_text = (
|
| 198 |
+
"昨晚梦见我在一栋很高的办公楼14层找一封迟迟没发出去的邮件。"
|
| 199 |
+
"电梯一直停在黑暗的走廊,我手里拿着一盏小台灯,感觉有点焦急。"
|
| 200 |
+
"醒来最想知道为什么梦里总是卡在电梯口。"
|
| 201 |
+
)
|
| 202 |
+
session = add_evidence(
|
| 203 |
+
create_session(language="zh"),
|
| 204 |
+
dream_text=dream_text,
|
| 205 |
+
mood="焦急",
|
| 206 |
+
vision_client=FakeVisionClient(),
|
| 207 |
+
asr_client=FakeASRClient(),
|
| 208 |
+
language="zh",
|
| 209 |
+
)
|
| 210 |
+
session = ask_questions(session, FakeTextClient(), language="zh")
|
| 211 |
+
|
| 212 |
+
card = generate_today_tip(
|
| 213 |
+
session.intake,
|
| 214 |
+
"我觉得那封邮件像是我一直拖着的一次工作沟通。",
|
| 215 |
+
ContaminatedElevatorClient(),
|
| 216 |
+
language="zh",
|
| 217 |
+
followup_questions=session.question_history,
|
| 218 |
+
)
|
| 219 |
+
combined = "\n".join(
|
| 220 |
+
[
|
| 221 |
+
session.question_history[0],
|
| 222 |
+
card.dream_summary,
|
| 223 |
+
card.main_question,
|
| 224 |
+
",".join(card.dream_anchors),
|
| 225 |
+
"\n".join(card.followup_questions),
|
| 226 |
+
card.interpretation,
|
| 227 |
+
card.today_tip,
|
| 228 |
+
card.tiny_action,
|
| 229 |
+
card.caring_note,
|
| 230 |
+
]
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
assert "融化" not in combined
|
| 234 |
+
assert "像蜡" not in combined
|
| 235 |
+
assert "电梯" in combined
|
| 236 |
+
assert "14" in combined
|
| 237 |
+
assert "邮件" in combined or "办公楼" in combined or "台灯" in combined
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
def test_elevator_melted_button_stays_when_user_supplies_it():
|
| 241 |
+
intake = build_intake(dream_text="我梦见电梯按钮融化,数字停在 14。", mood="焦虑")
|
| 242 |
+
|
| 243 |
+
card = generate_today_tip(intake, "用户选择跳过这个追问。", FakeTextClient(), language="zh")
|
| 244 |
+
combined = "\n".join([card.dream_summary, ",".join(card.dream_anchors), card.interpretation, card.today_tip])
|
| 245 |
+
|
| 246 |
+
assert "融化" in combined
|
| 247 |
+
assert "电梯" in combined
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
def test_generate_today_tip_uses_chinese_concrete_answer_in_visible_tip():
|
| 251 |
+
intake = build_intake(
|
| 252 |
+
dream_text=(
|
| 253 |
+
"昨晚梦见我在一栋很高的办公楼14层找一封迟迟没发出去的邮件。"
|
| 254 |
+
"电梯一直停在黑暗的走廊,我手里拿着一盏小台灯,感觉有点焦急。"
|
| 255 |
+
"醒来最想知道为什么梦里总是卡在电梯口。"
|
| 256 |
+
),
|
| 257 |
+
mood="焦急",
|
| 258 |
+
)
|
| 259 |
+
|
| 260 |
+
card = generate_today_tip(
|
| 261 |
+
intake,
|
| 262 |
+
"我觉得那封邮件像是我一直拖着的一次工作沟通,也可能有一点道歉。今天可以先写第一句,不一定马上发出去。",
|
| 263 |
+
FakeTextClient(),
|
| 264 |
+
language="zh",
|
| 265 |
+
)
|
| 266 |
+
visible = "\n".join([card.interpretation, card.today_tip, card.tiny_action])
|
| 267 |
+
|
| 268 |
+
assert "邮件" in visible
|
| 269 |
+
assert "第一句" in visible or "第一句话" in visible
|
| 270 |
+
assert "不要求立刻发出" in visible or "不一定马上发" in visible
|
| 271 |
+
assert "融化" not in visible
|
| 272 |
+
|
| 273 |
+
|
| 274 |
def test_generate_today_tip_follows_user_question_and_comfort_need_in_chinese():
|
| 275 |
intake = build_intake(
|
| 276 |
dream_text="我梦到自己掉进海里,醒来很害怕。我想知道这是不是说明我快撑不住了?",
|
|
|
|
| 542 |
assert session.sealed_pact == session.draft_pact
|
| 543 |
|
| 544 |
|
| 545 |
+
def test_force_another_question_keeps_chinese_answer_snippet_readable():
|
| 546 |
+
session = add_evidence(
|
| 547 |
+
create_session(language="zh"),
|
| 548 |
+
dream_text="我梦见自己卡在电梯口,14 层的灯一直亮着。",
|
| 549 |
+
mood="焦急",
|
| 550 |
+
vision_client=FakeVisionClient(),
|
| 551 |
+
asr_client=FakeASRClient(),
|
| 552 |
+
language="zh",
|
| 553 |
+
)
|
| 554 |
+
session = ask_questions(session, FakeTextClient(), language="zh")
|
| 555 |
+
session = answer_question(
|
| 556 |
+
session,
|
| 557 |
+
"我觉得那封���件像是我一直拖着的一次工作沟通,也可能有一点道歉。今天可以先写第一句,不一定马上发出去。",
|
| 558 |
+
language="zh",
|
| 559 |
+
)
|
| 560 |
+
session = ask_questions(session, FakeTextClient(), force_another=True, language="zh")
|
| 561 |
+
|
| 562 |
+
latest = session.question_history[-1]
|
| 563 |
+
assert "今天可。" not in latest
|
| 564 |
+
assert "第一句" in latest or "工作沟通" in latest
|
| 565 |
+
assert "我把你的回答也放进来了" in latest
|
| 566 |
+
|
| 567 |
+
|
| 568 |
def test_draft_pact_falls_back_to_legacy_generate_pact_client():
|
| 569 |
class LegacyPactOnlyClient:
|
| 570 |
def generate_pact(self, prompt):
|
tests/test_ui_actions.py
CHANGED
|
@@ -159,6 +159,49 @@ def test_processing_note_is_story_copy_not_backend_jargon():
|
|
| 159 |
assert "debug" not in lowered
|
| 160 |
|
| 161 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
def test_mobile_reset_restores_calm_mood():
|
| 163 |
settings_values = [
|
| 164 |
"",
|
|
@@ -233,6 +276,35 @@ def test_mobile_mvp_empty_submit_stays_on_record_with_clear_error():
|
|
| 233 |
assert json.loads(state)["phase"] == "error"
|
| 234 |
|
| 235 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
def test_mobile_mvp_auto_switches_chinese_input_to_chinese_result():
|
| 237 |
state, view_json = submit_dream_action(
|
| 238 |
dream_text="梦里我在一片雾很大的森林里追着一只白色猫跑,周围有很多空白路牌。",
|
|
|
|
| 159 |
assert "debug" not in lowered
|
| 160 |
|
| 161 |
|
| 162 |
+
def test_long_running_buttons_show_processing_feedback():
|
| 163 |
+
source = inspect.getsource(ui_app.build_demo)
|
| 164 |
+
app_source = inspect.getsource(ui_app)
|
| 165 |
+
|
| 166 |
+
assert "elem_classes=[\"dc-submit-button\"]" in source
|
| 167 |
+
assert "elem_classes=[\"dc-answer-button\"]" in source
|
| 168 |
+
assert source.count("elem_classes=[\"dc-tip-button\"]") >= 1
|
| 169 |
+
assert source.count("elem_classes=[\"dc-question-button\"]") >= 2
|
| 170 |
+
assert "bindProcessingButtons" in ui_app.VOICE_JS
|
| 171 |
+
assert "setProcessingCopy" in ui_app.VOICE_JS
|
| 172 |
+
assert "processingBound" in ui_app.VOICE_JS
|
| 173 |
+
assert "Asking from another angle" in ui_app.VOICE_JS
|
| 174 |
+
assert "one more grounded question" in ui_app.VOICE_JS
|
| 175 |
+
assert "js=SUBMIT_PROCESSING_JS" not in source
|
| 176 |
+
assert "js=TIP_PROCESSING_JS" not in source
|
| 177 |
+
assert source.count("scroll_to_output=True") >= 5
|
| 178 |
+
assert "Reading the dream details" in app_source
|
| 179 |
+
assert "Folding the follow-up answer" in app_source
|
| 180 |
+
assert ".dc-notice.is-processing" in CSS
|
| 181 |
+
assert ".dc-processing-note.is-active" in CSS
|
| 182 |
+
assert "endpoint" not in ui_app.VOICE_JS.lower()
|
| 183 |
+
assert "debug" not in ui_app.VOICE_JS.lower()
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def test_copy_result_button_writes_to_clipboard_with_fallback():
|
| 187 |
+
source = inspect.getsource(ui_app.build_demo)
|
| 188 |
+
|
| 189 |
+
assert "js=COPY_RESULT_JS" in source
|
| 190 |
+
assert "navigator.clipboard.writeText" in ui_app.COPY_RESULT_JS
|
| 191 |
+
assert "Result copied" in ui_app.COPY_RESULT_JS
|
| 192 |
+
assert "text box" in ui_app.COPY_RESULT_JS
|
| 193 |
+
assert "return value" in ui_app.COPY_RESULT_JS
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def test_another_angle_copy_is_explicitly_question_focused():
|
| 197 |
+
from dream_customs.ui.copy import copy_for
|
| 198 |
+
|
| 199 |
+
assert copy_for("en")["ask_again_button"] == "Ask one more question"
|
| 200 |
+
assert copy_for("en")["angle_button"] == "Ask from another angle"
|
| 201 |
+
assert copy_for("zh")["ask_again_button"] == "再追问一下"
|
| 202 |
+
assert copy_for("zh")["angle_button"] == "换个追问角度"
|
| 203 |
+
|
| 204 |
+
|
| 205 |
def test_mobile_reset_restores_calm_mood():
|
| 206 |
settings_values = [
|
| 207 |
"",
|
|
|
|
| 276 |
assert json.loads(state)["phase"] == "error"
|
| 277 |
|
| 278 |
|
| 279 |
+
def test_empty_submit_updates_inline_field_error_near_composer():
|
| 280 |
+
state, view_json = submit_dream_action(
|
| 281 |
+
dream_text="",
|
| 282 |
+
mood="Uneasy",
|
| 283 |
+
text_backend="demo",
|
| 284 |
+
vision_backend="demo",
|
| 285 |
+
language="en",
|
| 286 |
+
)
|
| 287 |
+
view = json.loads(view_json)
|
| 288 |
+
updates = ui_app._updates(state, view_json)
|
| 289 |
+
field_tip_html = updates[4]
|
| 290 |
+
|
| 291 |
+
assert view["status"] == "error"
|
| 292 |
+
assert "dc-field-tip is-error" in field_tip_html
|
| 293 |
+
assert 'role="alert"' in field_tip_html
|
| 294 |
+
assert "Dream note needs" in field_tip_html
|
| 295 |
+
|
| 296 |
+
ready_state, ready_view_json = submit_dream_action(
|
| 297 |
+
dream_text="I dreamed of a late elevator.",
|
| 298 |
+
mood="Uneasy",
|
| 299 |
+
text_backend="demo",
|
| 300 |
+
vision_backend="demo",
|
| 301 |
+
language="en",
|
| 302 |
+
)
|
| 303 |
+
ready_updates = ui_app._updates(ready_state, ready_view_json)
|
| 304 |
+
|
| 305 |
+
assert "dc-field-tip is-error" not in ready_updates[4]
|
| 306 |
+
|
| 307 |
+
|
| 308 |
def test_mobile_mvp_auto_switches_chinese_input_to_chinese_result():
|
| 309 |
state, view_json = submit_dream_action(
|
| 310 |
dream_text="梦里我在一片雾很大的森林里追着一只白色猫跑,周围有很多空白路牌。",
|
videos/dreamqa_01_text_only.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:deaa14482d904b3084d4533dacfbff04e7689c15f1212331aebc9649da8c2cf6
|
| 3 |
+
size 957336
|
videos/dreamqa_02_image_only.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:eca1043f4ca313d5b2fe6823f23ae2ddc23f38a617a5587fea88b7cbddff5e9f
|
| 3 |
+
size 930264
|
videos/dreamqa_03_text_plus_image.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3722ac1e78fdb69a86154abe9d7d22b1437e2f0bc7d0f66239110b197d160f55
|
| 3 |
+
size 680603
|