Refine Dream QA waking tips and weird actions

#37
by ADJCJH - opened
dream_customs/app_logic.py CHANGED
@@ -257,7 +257,7 @@ def _today_tip_plain_text(card) -> str:
257
  f"Today Tip: {card.today_tip}",
258
  ]
259
  if card.tiny_action:
260
- lines.append(f"Tiny action: {card.tiny_action}")
261
  if card.caring_note:
262
  lines.append(f"Caring note: {card.caring_note}")
263
  if card.safety_note:
 
257
  f"Today Tip: {card.today_tip}",
258
  ]
259
  if card.tiny_action:
260
+ lines.append(f"Weird little thing: {card.tiny_action}")
261
  if card.caring_note:
262
  lines.append(f"Caring note: {card.caring_note}")
263
  if card.safety_note:
dream_customs/models.py CHANGED
@@ -100,8 +100,15 @@ class FakeTextClient:
100
  "Maybe this dream is not predicting lateness. It turns the feeling of being stuck before "
101
  "starting into an elevator paused at floor 14."
102
  ),
103
- today_tip="For today, press one very small elevator button: open the task and write only the first line.",
104
- tiny_action="Set a five-minute timer, open the task, and stop after the first line.",
 
 
 
 
 
 
 
105
  caring_note="You can move one floor at a time; the whole building does not need to be solved this morning.",
106
  )
107
  return TodayTipCard(
@@ -117,8 +124,12 @@ class FakeTextClient:
117
  "也许这个梦不是在预言你会迟到,而是在把“还没开始就担心来不及”的感觉,"
118
  "演成了一部停在 14 层的电梯。"
119
  ),
120
- today_tip="今天先按下一个很小的电梯按钮:只打开那件事,不要求自己马上抵达。",
121
- tiny_action=" 5 分钟写下:如果今天只停一层,我先停层?",
 
 
 
 
122
  caring_note="你可以慢慢前进,不需要一醒来就解决所有楼层。",
123
  )
124
 
 
100
  "Maybe this dream is not predicting lateness. It turns the feeling of being stuck before "
101
  "starting into an elevator paused at floor 14."
102
  ),
103
+ today_tip=(
104
+ "1. Translate the elevator into the real-life doorway where you feel stuck. "
105
+ "2. Open only the draft or email connected to that doorway. "
106
+ "3. Add the first sentence and save it without sending yet."
107
+ ),
108
+ tiny_action=(
109
+ "Draw an elevator button labeled Draft Floor on a sticky note, press it once with your finger, "
110
+ "then write only the first sentence."
111
+ ),
112
  caring_note="You can move one floor at a time; the whole building does not need to be solved this morning.",
113
  )
114
  return TodayTipCard(
 
124
  "也许这个梦不是在预言你会迟到,而是在把“还没开始就担心来不及”的感觉,"
125
  "演成了一部停在 14 层的电梯。"
126
  ),
127
+ today_tip=(
128
+ "1. 把「电梯」翻译成现实里最像卡入口的件事。"
129
+ "2. 只打开和它有关的草稿或邮件。"
130
+ "3. 写下第一句话后先存起来,不要求马上发出。"
131
+ ),
132
+ tiny_action="找一张便利贴,画一个只到“草稿层”的电梯按钮,按一下,再只写第一句话。",
133
  caring_note="你可以慢慢前进,不需要一醒来就解决所有楼层。",
134
  )
135
 
dream_customs/pipeline.py CHANGED
@@ -1023,94 +1023,106 @@ def _emotion_led_today_tip(intake: DreamIntake, answers: str, anchors: List[str]
1023
  if _is_zh(language):
1024
  if theme == "lost_home":
1025
  extra = f"如果刚才的回答里「{answer_snippet}」最重,就从它开始。" if answer_snippet else ""
1026
- return (
1027
- f"今天先顺着「{anchor}」照顾那种需要被带路的感觉,而不是急着解释梦的含义。"
1028
- f"{extra}给今天补一个很小的“标”:问个人、查一个入口或写下下一站在哪里。"
 
 
 
1029
  )
1030
  if theme == "dark_water":
1031
- return (
1032
- f"今天先把「{anchor}」当成醒来后还在身体里的浪,不当成危险证明。"
1033
- "先选一个让自己回到岸边的动作:开灯、洗脸、发一句“我醒来有点慌先缓一下”。"
 
 
 
1034
  )
1035
  if theme == "stuck_elevator":
1036
- return (
1037
- f"今天不要逼自己抵达所有楼层;先把「{anchor}」变成一个只按一次的按钮。"
1038
- "只确认当前最卡的一什么再允许自己停在那里做一个很小动作。"
 
 
 
1039
  )
1040
  if theme == "library_signal":
1041
- return (
1042
- f"今天把「{anchor}」当成一张提醒你回到安定处的便签。"
1043
- "不用立刻整理完整答案,只挑一个让你有“回家感”人、物或角落,靠近它五分钟。"
 
 
 
1044
  )
1045
  if theme == "message_loss":
1046
- return (
1047
- f"今天先把「{anchor}」看成一段还没被好好收起的联系。"
1048
- "不用真的发出去,先写封不发送的回信,让那句说完话有地方停靠。"
 
 
 
1049
  )
1050
- return (
1051
- f"今天的小建议是:先别把「{anchor}」解释成结论;把它当成梦递过来的线索,"
1052
- "问问在替你保护哪种感受,再给那种感受一很小的照顾动作。"
 
 
 
1053
  )
1054
  if theme == "lost_home":
1055
  extra = f" If your answer points to \"{answer_snippet}\", start there." if answer_snippet else ""
1056
- return (
1057
- f"For today, treat {anchor} as a need for guidance, not as a verdict about you.{extra} "
1058
- "Add one tiny wayfinding marker: ask one person, check one entrance, or name the next stop."
 
 
 
1059
  )
1060
  if theme == "dark_water":
1061
- return (
1062
- f"For today, treat {anchor} as a wave still in your body, not proof of danger. "
1063
- "Choose one shore-like action: turn on a light, wash your face, or tell someone you woke up unsettled."
 
 
 
1064
  )
1065
  if theme == "stuck_elevator":
1066
- return (
1067
- f"For today, do not make yourself reach every floor. Let {anchor} become one button: "
1068
- "name the stuck floor, then do only one small action there."
 
 
 
1069
  )
1070
  if theme == "library_signal":
1071
- return (
1072
- f"For today, treat {anchor} like a note pointing you back to steadiness. "
1073
- "Pick one person, place, or object that feels like home and spend five minutes near it."
 
 
 
1074
  )
1075
  if theme == "message_loss":
1076
- return (
1077
- f"For today, treat {anchor} as an unfinished contact, not a demand to reopen everything. "
1078
- "Write one unsent reply so the unsaid sentence has somewhere to rest."
 
 
 
1079
  )
1080
- return (
1081
- f"For today, do not turn {anchor} into a fixed conclusion. Treat it as a clue about one feeling "
1082
- "that needs care, then give that feeling one small caring action."
 
 
 
1083
  )
1084
 
1085
 
1086
  def _emotion_led_tiny_action(intake: DreamIntake, answers: str, anchors: List[str], language: str = "en") -> str:
1087
  if not _should_use_emotion_led_response(intake, answers, language):
1088
  return ""
1089
- theme = _dream_theme(intake, answers)
1090
- anchor = _story_anchor_phrase(intake, anchors, language, answers)
1091
- if _is_zh(language):
1092
- if theme == "lost_home":
1093
- return f"用 5 分钟给「{anchor}」画一张三格小路线:我在找什么、我能问谁或查哪里、今天只走到哪一站。"
1094
- if theme == "dark_water":
1095
- return f"用 5 分钟做一个“上岸”动作:看着「{anchor}」,说出三个房间里的真实物件,再喝一口水或开一盏灯。"
1096
- if theme == "stuck_elevator":
1097
- return f"用 5 分钟写一个“只按这一层”的按钮:今天卡在哪一层、我能做的最小动作、做完就停。"
1098
- if theme == "library_signal":
1099
- return f"用 5 分钟把「{anchor}」改写成一张现实便签:我现在可以回到哪里,或联系谁,让自己稳一点。"
1100
- if theme == "message_loss":
1101
- return f"用 5 分钟写一封不发送的短信草稿,开头只写:梦里的「{anchor}」让我想承认的是……"
1102
- return f"用 5 分钟做一个三格便签:梦里的「{anchor}」最亮的细节、它带出的感受、今天能照顾它的一小步。"
1103
- if theme == "lost_home":
1104
- return f"Spend five minutes making a three-box route for {anchor}: what I am looking for, who or where I can ask, and the next stop only."
1105
- if theme == "dark_water":
1106
- return f"Spend five minutes doing one shore action for {anchor}: name three real objects in the room, then drink water or turn on a light."
1107
- if theme == "stuck_elevator":
1108
- return f"Spend five minutes making a one-floor button for {anchor}: the stuck floor, the smallest action, and permission to stop after it."
1109
- if theme == "library_signal":
1110
- return f"Spend five minutes turning {anchor} into a real note: where can I return, or who can I contact, to feel steadier?"
1111
- if theme == "message_loss":
1112
- return f"Spend five minutes drafting one unsent message that begins: what {anchor} makes me want to admit is..."
1113
- return f"Spend five minutes making three boxes for {anchor}: brightest detail, feeling it carries, and one small care step for today."
1114
 
1115
 
1116
  def _emotion_led_caring_note(intake: DreamIntake, answers: str, language: str = "en") -> str:
@@ -1119,6 +1131,7 @@ def _emotion_led_caring_note(intake: DreamIntake, answers: str, language: str =
1119
  labels = _emotion_labels_from_text(_user_supplied_text(intake, answers), language)
1120
  emotion = _emotion_phrase(labels, language)
1121
  if _is_zh(language):
 
1122
  return f"你不是太脆弱,也不是需要被催着立刻想通;这份{emotion}可以先被轻轻接住。"
1123
  return f"You are not weak for feeling {emotion}; you do not have to turn it into a lesson before you are comforted."
1124
 
@@ -1158,6 +1171,97 @@ def _fallback_interpretation(intake: DreamIntake, language: str = "en") -> str:
1158
  )
1159
 
1160
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1161
  def _grounded_today_tip(intake: DreamIntake, language: str = "en") -> str:
1162
  anchors = _anchors_for_language(intake, language)
1163
  primary = anchors[0] if anchors else _primary_anchor(intake, language)
@@ -1165,48 +1269,130 @@ def _grounded_today_tip(intake: DreamIntake, language: str = "en") -> str:
1165
  theme = _dream_theme(intake)
1166
  if not _is_zh(language):
1167
  if theme == "lost_home":
1168
- return f"For today, let {anchor} become a small wayfinding cue: choose one place to ask, one entrance to check, or one next stop to name."
 
 
 
 
 
 
1169
  if theme == "dark_water":
1170
- return f"For today, let {anchor} become a shore cue: name what felt too large, then choose one ordinary thing that helps your body feel here again."
 
 
 
 
 
 
1171
  if theme == "library_signal":
1172
- return f"For today, let {anchor} become a note back to steadiness: choose one person, place, or object that feels like home and spend five minutes near it."
 
 
 
 
 
 
1173
  if theme == "message_loss":
1174
- return f"For today, let {anchor} become one unsent sentence, not a full conversation: write what is still tender and let it rest."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1175
  return (
1176
- f"For today, borrow one action from {_anchor_with_article(primary)}: open the task, write only the first line, "
1177
- "and let that be enough for now."
 
 
 
 
 
1178
  )
1179
  if theme == "lost_home":
1180
- return f"今天先让「{anchor}」变成一个小路标:只确认一个可以问的人、一个可以查的入口,或一个下一站。"
 
 
 
 
 
 
1181
  if theme == "dark_water":
1182
- return f"今天先让「{anchor}」变成一个上岸提示:说出那个太大的感受,再选一件能让身体回到当下的小事。"
 
 
 
 
 
 
1183
  if theme == "library_signal":
1184
- return f"今天先让「{anchor}」变成一张回到安定处的便签:靠近一个有回家感的人、地方或物件五分钟。"
 
 
 
 
 
 
1185
  if theme == "message_loss":
1186
- return f"今天先让「{anchor}」变成一句不发送的话,而不是一整场对话:写下还柔软的那句,然后让它停在那里。"
1187
- return f"今天先从「{primary}」借一个动作:只做最小的第一步,不急着把整件事完成。"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1188
 
1189
 
1190
- def _answer_based_tiny_action(answers: str, language: str = "en") -> str:
 
 
 
 
 
1191
  lowered = (answers or "").lower()
1192
- if _is_zh(language):
1193
- if "邮件" in lowered or "email" in lowered:
1194
- return "给自己 5 分钟,只打开那封邮件,写下第一句话;今天不要求立刻发出。"
1195
- if "消息" in lowered or "发消息" in lowered:
1196
- return "给自己 5 分钟,只写一条诚实的进度消息草稿;今天先不要求把整件事做完。"
1197
- if "作业" in lowered or "草稿" in lowered:
1198
- return "给自己 5 分钟,只打开草稿,写下下一小段或一个待补的标题。"
1199
- return ""
1200
- if "email" in lowered or "message" in lowered:
1201
- return "Set a five-minute timer, open the email, and write only the first sentence. You do not have to send it yet."
1202
- if "assignment" in lowered or "homework" in lowered or "draft" in lowered:
1203
- return "Set a five-minute timer, open the draft, and write only the next small heading or first sentence."
1204
- if "presentation" in lowered or "speech" in lowered or "rehearse" in lowered:
1205
- return "Set a five-minute timer and rehearse only the first minute, then stop and write one note for tomorrow."
1206
- if "deadline" in lowered or "application" in lowered:
1207
- return "Set a five-minute timer, open the application checklist, and mark only the next missing item."
1208
- if "apolog" in lowered:
1209
- return "Set a five-minute timer and draft one honest apology sentence without sending it yet."
 
 
1210
  return ""
1211
 
1212
 
@@ -1214,28 +1400,75 @@ def _answer_based_today_tip(answers: str, anchor: str, language: str = "en") ->
1214
  lowered = (answers or "").lower()
1215
  if _is_zh(language):
1216
  if "邮件" in lowered or "email" in lowered:
1217
- return f"今天把「{anchor}」当成允许慢慢开始的按钮:只打开那封邮件,先写第一句话。"
 
 
 
 
 
 
 
1218
  if "消息" in lowered or "发消息" in lowered:
1219
- return f"今天把「{anchor}」当成一个小门牌:先给对方发一条进度消息,不要求马上完成全部。"
 
 
 
 
 
 
 
1220
  if "作业" in lowered or "草稿" in lowered:
1221
- return f"今天把「{anchor}」当成一个可以靠近的教室门口:只打开草稿,补上下一小段。"
 
 
 
 
 
 
 
1222
  return ""
1223
  if "email" in lowered or "message" in lowered:
1224
- return (
1225
- f"For today, treat the {anchor} as permission to start gently: "
1226
- "open the overdue email and write only the first sentence."
 
 
 
 
1227
  )
1228
  if "assignment" in lowered or "homework" in lowered or "draft" in lowered:
1229
- return (
1230
- f"For today, treat the {anchor} as a smaller doorway: open the draft "
1231
- "and write only the next tiny piece."
 
 
 
 
1232
  )
1233
  if "presentation" in lowered or "speech" in lowered or "rehearse" in lowered:
1234
- return f"For today, let the {anchor} narrow the work: rehearse only the first minute of the presentation."
 
 
 
 
 
 
1235
  if "deadline" in lowered or "application" in lowered:
1236
- return f"For today, let the {anchor} become a checklist: open the application and choose one missing item."
 
 
 
 
 
 
1237
  if "apolog" in lowered:
1238
- return f"For today, let the {anchor} become one repair step: draft a single apology sentence."
 
 
 
 
 
 
1239
  return ""
1240
 
1241
 
@@ -1852,7 +2085,7 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
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
@@ -1876,7 +2109,18 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
1876
  )
1877
  elif not polished.interpretation.strip() or not _anchor_in_text(polished.interpretation, anchors):
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
@@ -1905,7 +2149,16 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
1905
  or not _anchor_in_text(polished.today_tip, anchors)
1906
  ):
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
@@ -1914,47 +2167,16 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
1914
  elif answer_action:
1915
  polished.tiny_action = answer_action
1916
  elif _has_prophecy_frame(intake.merged_text()):
1917
- polished.tiny_action = (
1918
- "Spend five minutes writing: what did I feel, what ordinary worry could it echo, and what can I do safely today?"
1919
- if not _is_zh(language)
1920
- else "用 5 分钟写下:我醒来时感觉到了什么?它像哪种普通担心?今天我能安全做哪一小步?"
1921
- )
1922
  elif _is_low_context_intake(intake) and _is_skip_answer(answers, language):
1923
- polished.tiny_action = (
1924
- "Add one missing detail: color, body feeling, repeated object, or what happened just before waking."
1925
- if not _is_zh(language)
1926
- else "补一个缺失细节:颜色、身体感受、重复物件,或醒来前最后发生的事。"
1927
- )
1928
  elif (
1929
  not polished.tiny_action.strip()
1930
  or _is_placeholder_anchor(polished.tiny_action)
1931
  or not _anchor_in_text(polished.tiny_action, anchors)
1932
  or any(marker in polished.tiny_action.lower() for marker in hard_action_markers)
1933
  ):
1934
- anchor_phrase = _story_anchor_phrase(intake, anchors, language, answers)
1935
- theme = _dream_theme(intake, answers)
1936
- if _is_zh(language):
1937
- if theme == "lost_home":
1938
- polished.tiny_action = f"用 5 分钟给「{anchor_phrase}」补一张小路标:我在找什么、能问哪里、下一站是什么。"
1939
- elif theme == "dark_water":
1940
- polished.tiny_action = f"用 5 分钟给「{anchor_phrase}」找一个上岸动作:看见三个真实物件,再做一件让身体安定的小事。"
1941
- elif theme == "library_signal":
1942
- polished.tiny_action = f"用 5 分钟把「{anchor_phrase}」变成现实便签:今天我可以靠近哪一个安定来源?"
1943
- elif theme == "message_loss":
1944
- polished.tiny_action = f"用 5 分钟给「{anchor_phrase}」写一句不发送的话,然后把手机放下。"
1945
- else:
1946
- polished.tiny_action = f"用 5 分钟写下:今天和「{anchors[0]}」有关的第一小步是什么?"
1947
- else:
1948
- if theme == "lost_home":
1949
- polished.tiny_action = f"Spend five minutes adding a small route marker for {anchor_phrase}: what I seek, where I can ask, and the next stop."
1950
- elif theme == "dark_water":
1951
- polished.tiny_action = f"Spend five minutes finding a shore action for {anchor_phrase}: name three real objects, then do one grounding thing."
1952
- elif theme == "library_signal":
1953
- polished.tiny_action = f"Spend five minutes turning {anchor_phrase} into a real note: which steady source can I move closer to today?"
1954
- elif theme == "message_loss":
1955
- polished.tiny_action = f"Spend five minutes writing one unsent sentence for {anchor_phrase}, then put the phone down."
1956
- else:
1957
- polished.tiny_action = f"Spend five minutes writing the first small step connected to the {anchors[0]}."
1958
  emotion_caring_note = _emotion_led_caring_note(intake, answers, language)
1959
  if emotion_caring_note:
1960
  polished.caring_note = emotion_caring_note
 
1023
  if _is_zh(language):
1024
  if theme == "lost_home":
1025
  extra = f"如果刚才的回答里「{answer_snippet}」最重,就从它开始。" if answer_snippet else ""
1026
+ return _numbered_suggestions(
1027
+ [
1028
+ f"把「{anchor}」翻译成现实里需要带件事先不急着解释梦的含义。",
1029
+ f"{extra}今天只补一个现实路标:问一个人、查一个入口,或写下下一站在哪里。",
1030
+ ],
1031
+ language,
1032
  )
1033
  if theme == "dark_water":
1034
+ return _numbered_suggestions(
1035
+ [
1036
+ f"把「{anchor}」当成醒来后还留在身体里的感觉不当成危险证明。",
1037
+ "先选一个现实里的上岸动作:开灯、洗脸,或发一句“我醒来有点慌,先缓一下”。",
1038
+ ],
1039
+ language,
1040
  )
1041
  if theme == "stuck_elevator":
1042
+ return _numbered_suggestions(
1043
+ [
1044
+ f"从「{anchor}」找出现实里在入口的一件事:开始、等待回应还是怕来不及。",
1045
+ "把它拆成两步:先确认卡点,再只做一个今天真的能完成的入口动作。",
1046
+ ],
1047
+ language,
1048
  )
1049
  if theme == "library_signal":
1050
+ return _numbered_suggestions(
1051
+ [
1052
+ f"让「{anchor}」指向现实里能让稳定一点来源。",
1053
+ "不用立刻整理完整答案,只挑一个有“回家感”的人、物或角落,靠近它五分钟。",
1054
+ ],
1055
+ language,
1056
  )
1057
  if theme == "message_loss":
1058
+ return _numbered_suggestions(
1059
+ [
1060
+ f"把「{anchor}」看成现实里段还被好好收起联系。",
1061
+ "不用真的联系对方,先写一封不发送的回信,让那句没说完的话有地方停靠。",
1062
+ ],
1063
+ language,
1064
  )
1065
+ return _numbered_suggestions(
1066
+ [
1067
+ f"先别把「{anchor}」解释成结论,把当成现实生活里一个需要照顾的线索。",
1068
+ "问问它像今天哪一种感受,再给那种感受一个真的能做的小照顾。",
1069
+ ],
1070
+ language,
1071
  )
1072
  if theme == "lost_home":
1073
  extra = f" If your answer points to \"{answer_snippet}\", start there." if answer_snippet else ""
1074
+ return _numbered_suggestions(
1075
+ [
1076
+ f"Translate {anchor} into a waking-life need for guidance, not a verdict about you.",
1077
+ f"{extra}Add one real wayfinding marker: ask one person, check one entrance, or name the next stop.",
1078
+ ],
1079
+ language,
1080
  )
1081
  if theme == "dark_water":
1082
+ return _numbered_suggestions(
1083
+ [
1084
+ f"Treat {anchor} as a body feeling after waking, not proof of danger.",
1085
+ "Choose one real shore-like action: turn on a light, wash your face, or tell someone you woke unsettled.",
1086
+ ],
1087
+ language,
1088
  )
1089
  if theme == "stuck_elevator":
1090
+ return _numbered_suggestions(
1091
+ [
1092
+ f"Use {anchor} to locate the real-life entrance where you feel stuck: starting, waiting, or being late.",
1093
+ "Split it in two: name the stuck point, then choose one waking-life doorway action for today.",
1094
+ ],
1095
+ language,
1096
  )
1097
  if theme == "library_signal":
1098
+ return _numbered_suggestions(
1099
+ [
1100
+ f"Let {anchor} point to a real source of steadiness.",
1101
+ "Pick one person, place, or object that feels like home and spend five minutes near it.",
1102
+ ],
1103
+ language,
1104
  )
1105
  if theme == "message_loss":
1106
+ return _numbered_suggestions(
1107
+ [
1108
+ f"Treat {anchor} as an unfinished contact in real life, not a demand to reopen everything.",
1109
+ "Write one private unsent reply so the unsaid sentence has somewhere to rest.",
1110
+ ],
1111
+ language,
1112
  )
1113
+ return _numbered_suggestions(
1114
+ [
1115
+ f"Do not turn {anchor} into a fixed conclusion; translate it into one waking-life feeling.",
1116
+ "Give that feeling one small caring action you can actually do today.",
1117
+ ],
1118
+ language,
1119
  )
1120
 
1121
 
1122
  def _emotion_led_tiny_action(intake: DreamIntake, answers: str, anchors: List[str], language: str = "en") -> str:
1123
  if not _should_use_emotion_led_response(intake, answers, language):
1124
  return ""
1125
+ return _weird_little_action(intake, answers, anchors, language)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1126
 
1127
 
1128
  def _emotion_led_caring_note(intake: DreamIntake, answers: str, language: str = "en") -> str:
 
1131
  labels = _emotion_labels_from_text(_user_supplied_text(intake, answers), language)
1132
  emotion = _emotion_phrase(labels, language)
1133
  if _is_zh(language):
1134
+ emotion = "感受" if emotion == "这个感受" else emotion
1135
  return f"你不是太脆弱,也不是需要被催着立刻想通;这份{emotion}可以先被轻轻接住。"
1136
  return f"You are not weak for feeling {emotion}; you do not have to turn it into a lesson before you are comforted."
1137
 
 
1171
  )
1172
 
1173
 
1174
+ def _numbered_suggestions(items: List[str], language: str = "en") -> str:
1175
+ cleaned = [item.strip() for item in items if item and item.strip()]
1176
+ return " ".join(f"{index}. {item}" for index, item in enumerate(cleaned[:3], start=1))
1177
+
1178
+
1179
+ def _seeded_option(options: List[str], intake: DreamIntake, answers: str = "") -> str:
1180
+ if not options:
1181
+ return ""
1182
+ seed_text = _story_text(intake, answers)
1183
+ seed = sum(ord(char) for char in seed_text)
1184
+ return options[seed % len(options)]
1185
+
1186
+
1187
+ def _weird_little_action(intake: DreamIntake, answers: str, anchors: List[str], language: str = "en") -> str:
1188
+ anchor = _story_anchor_phrase(intake, anchors, language, answers)
1189
+ theme = _dream_theme(intake, answers)
1190
+ if _is_zh(language):
1191
+ options_by_theme = {
1192
+ "lost_home": [
1193
+ f"拿一张纸画一个写着「{anchor}」的小站牌,把钥匙或一枚硬币从站牌左边挪到右边,像真的给它过一站。",
1194
+ f"在门边放一张写着「下一站」的便签,再把鞋尖对准它三秒;只做这一下,提醒身体已经回到现实地面。",
1195
+ ],
1196
+ "dark_water": [
1197
+ f"把一杯水放在桌边当「{anchor}」的海,再把一小片纸从杯子旁挪到“岸上”;挪完说出房间里三个真实物件。",
1198
+ f"用手指在桌面画一条看不见的海岸线,把一个小物件从「{anchor}」那侧推到自己这侧,像给醒来的身体靠岸。",
1199
+ ],
1200
+ "stuck_elevator": [
1201
+ "找一张便利贴,画一个只到“草稿层”的电梯按钮,贴在电脑旁;用手指按一下,再只写邮件第一句话。",
1202
+ "在纸上画三枚电梯按钮:14、草稿、暂停;闭眼按一下“草稿”,然后打开邮件写第一句话。",
1203
+ "把一张纸立在键盘旁当电梯门,画个「邮件草稿层」按钮;按一下纸门,再写一句就合上。",
1204
+ ],
1205
+ "library_signal": [
1206
+ f"做一张迷你借书卡,书名写「{anchor}」,到期日写“今晚不追讨”;把它夹进一本书里,像把梦暂存进图书馆。",
1207
+ f"把一张便签折成小楼梯,写上「{anchor}」后让一支笔从第一阶滑到第二阶;今天只允许它上这一阶。",
1208
+ ],
1209
+ "message_loss": [
1210
+ f"把手机旁边放一张小纸片当“消息降落伞”,写上「{anchor}」的一个词;让纸片从手机边滑下来,今天不真的发送。",
1211
+ f"画一个迷你信号塔,塔顶写「{anchor}」;把手机倒扣三十秒,像给没发出的消息临时下班。",
1212
+ ],
1213
+ "school_pressure": [
1214
+ f"把一支笔横放成教室门槛,在纸上写「{anchor}」和一个超小标题;让笔滚过门槛,表示今天只进门不考试。",
1215
+ f"给自己做一张假的迟到证,理由写「{anchor}」;盖一个手指印,然后只补草稿里的一个标题。",
1216
+ ],
1217
+ "road_choice": [
1218
+ f"用两支笔摆成两条路,把写着「{anchor}」的小纸条放在中间;闭眼转一下纸条,先走它指到的那一个 30 秒。",
1219
+ f"在纸上画一个空白路牌,写两个现实选项;用一枚硬币当雾灯照一下其中一个,今天只查那个入口。",
1220
+ ],
1221
+ }
1222
+ default_options = [
1223
+ f"拿一张纸把「{anchor}」画成一个现实开关,真的用指尖按一下;按完只做一个不超过一分钟的小动作。",
1224
+ f"找一个桌面小物件给「{anchor}」当临时替身,把它移动一厘米;移动完再决定今天最小的现实动作。",
1225
+ ]
1226
+ return _seeded_option(options_by_theme.get(theme, default_options), intake, answers)
1227
+ options_by_theme = {
1228
+ "lost_home": [
1229
+ f"Draw a tiny station sign labeled {anchor}, then move a key or coin from the left side of the sign to the right as if it really crossed one stop.",
1230
+ f"Put a note by the door that says Next stop, point your shoes at it for three seconds, and let that be the whole odd action.",
1231
+ ],
1232
+ "dark_water": [
1233
+ f"Place a cup of water on the table as {anchor}, then move a scrap of paper from beside the cup to a dry shore and name three real objects in the room.",
1234
+ f"Trace an invisible shoreline on the table and push one small object from the {anchor} side back toward you.",
1235
+ ],
1236
+ "stuck_elevator": [
1237
+ "Draw an elevator button labeled Draft Floor on a sticky note, press it once with your finger, then write only the first sentence of the email.",
1238
+ "Draw three elevator buttons on paper: 14, Draft, Pause. Close your eyes, press Draft once, then open the email for one sentence.",
1239
+ "Stand a piece of paper beside the keyboard like elevator doors, draw an Email Draft button, press it once, write one sentence, and close the paper doors.",
1240
+ ],
1241
+ "library_signal": [
1242
+ f"Make a tiny library card titled {anchor}, set the due date to Not tonight, and tuck it into a book.",
1243
+ f"Fold a note into a small staircase, write {anchor} on it, and slide a pen from the first step to the second; today it only gets one step.",
1244
+ ],
1245
+ "message_loss": [
1246
+ f"Put a paper scrap beside your phone as a message parachute, write one word from {anchor}, and let it slide down from the phone without sending anything.",
1247
+ f"Draw a tiny signal tower with {anchor} at the top, then place your phone face down for thirty seconds so the unsent message can clock out.",
1248
+ ],
1249
+ "school_pressure": [
1250
+ f"Lay a pen across the page as a classroom threshold, write {anchor} and one tiny heading, then roll the pen over the threshold.",
1251
+ f"Make a fake late pass with {anchor} as the reason, stamp it with your fingertip, and add only one heading to the draft.",
1252
+ ],
1253
+ "road_choice": [
1254
+ f"Place two pens like two roads, put a note labeled {anchor} between them, spin the note once, and check only the entrance it points to.",
1255
+ f"Draw a blank road sign with two real options, then use a coin as a fog light to choose which entrance to inspect today.",
1256
+ ],
1257
+ }
1258
+ default_options = [
1259
+ f"Draw {anchor} as a real button on paper, press it once, then do one physical action that takes under a minute.",
1260
+ f"Choose one desk object as a stand-in for {anchor}, move it one centimeter, then name the smallest waking-life action it points to.",
1261
+ ]
1262
+ return _seeded_option(options_by_theme.get(theme, default_options), intake, answers)
1263
+
1264
+
1265
  def _grounded_today_tip(intake: DreamIntake, language: str = "en") -> str:
1266
  anchors = _anchors_for_language(intake, language)
1267
  primary = anchors[0] if anchors else _primary_anchor(intake, language)
 
1269
  theme = _dream_theme(intake)
1270
  if not _is_zh(language):
1271
  if theme == "lost_home":
1272
+ return _numbered_suggestions(
1273
+ [
1274
+ f"Translate {anchor} into waking life: name the place where you most need guidance.",
1275
+ "Choose one real wayfinding move today: ask one person, check one entrance, or name the next stop.",
1276
+ ],
1277
+ language,
1278
+ )
1279
  if theme == "dark_water":
1280
+ return _numbered_suggestions(
1281
+ [
1282
+ f"Treat {anchor} as a body feeling after waking, not as proof of danger.",
1283
+ "Pick one real shore-like action: turn on a light, wash your face, or tell someone you woke unsettled.",
1284
+ ],
1285
+ language,
1286
+ )
1287
  if theme == "library_signal":
1288
+ return _numbered_suggestions(
1289
+ [
1290
+ f"Let {anchor} point to a real source of steadiness.",
1291
+ "Spend five minutes near one person, place, or object that gives you a home-base feeling.",
1292
+ ],
1293
+ language,
1294
+ )
1295
  if theme == "message_loss":
1296
+ return _numbered_suggestions(
1297
+ [
1298
+ f"Translate {anchor} into one real sentence you have not said yet.",
1299
+ "Write it somewhere private; do not decide today whether it needs to be sent.",
1300
+ ],
1301
+ language,
1302
+ )
1303
+ if theme == "stuck_elevator":
1304
+ return _numbered_suggestions(
1305
+ [
1306
+ f"Use {anchor} to spot the real-life place where you feel stuck at the entrance.",
1307
+ "Choose one waking-life doorway action today: open the draft, check one fact, or ask one person.",
1308
+ ],
1309
+ language,
1310
+ )
1311
  return (
1312
+ _numbered_suggestions(
1313
+ [
1314
+ f"Treat {_anchor_with_article(primary)} as a clue for waking life, not an instruction from the dream.",
1315
+ "Name one ordinary situation it resembles and choose one small action you can actually do today.",
1316
+ ],
1317
+ language,
1318
+ )
1319
  )
1320
  if theme == "lost_home":
1321
+ return _numbered_suggestions(
1322
+ [
1323
+ f"把「{anchor}」翻译成现实里需要带路的一件事。",
1324
+ "今天只做一个现实路标:问一个人、查一个入口,或写下下一站。",
1325
+ ],
1326
+ language,
1327
+ )
1328
  if theme == "dark_water":
1329
+ return _numbered_suggestions(
1330
+ [
1331
+ f"把「{anchor}」当成醒来后身体还记得的感觉,不当成危险证明。",
1332
+ "在现实里选一个上岸动作:开灯、洗脸,或告诉一个人“我醒来有点慌”。",
1333
+ ],
1334
+ language,
1335
+ )
1336
  if theme == "library_signal":
1337
+ return _numbered_suggestions(
1338
+ [
1339
+ f"让「{anchor}」指向现实里能让你稳定一点的人、地方或物件。",
1340
+ "今天靠近它五分钟,不急着整理完整答案。",
1341
+ ],
1342
+ language,
1343
+ )
1344
  if theme == "message_loss":
1345
+ return _numbered_suggestions(
1346
+ [
1347
+ f"把「{anchor}」翻译成现实里一句还没说出口的话。",
1348
+ "先把它写在私密处,今天不急着决定要不要发出。",
1349
+ ],
1350
+ language,
1351
+ )
1352
+ if theme == "stuck_elevator":
1353
+ return _numbered_suggestions(
1354
+ [
1355
+ f"从「{anchor}」找出现实里最像“卡在门口”的一件事。",
1356
+ "今天只选一个醒着能做的入口动作:打开草稿、查一个信息,或问一个人。",
1357
+ ],
1358
+ language,
1359
+ )
1360
+ return _numbered_suggestions(
1361
+ [
1362
+ f"把「{primary}」当成现实生活的线索,不当成梦里给你的命令。",
1363
+ "说出它像今天哪件普通小事,再选一个真的能做的小动作。",
1364
+ ],
1365
+ language,
1366
+ )
1367
 
1368
 
1369
+ def _answer_based_tiny_action(
1370
+ answers: str,
1371
+ intake: DreamIntake,
1372
+ anchors: List[str],
1373
+ language: str = "en",
1374
+ ) -> str:
1375
  lowered = (answers or "").lower()
1376
+ answer_terms = [
1377
+ "邮件",
1378
+ "email",
1379
+ "消息",
1380
+ "消息",
1381
+ "message",
1382
+ "作业",
1383
+ "���稿",
1384
+ "assignment",
1385
+ "homework",
1386
+ "draft",
1387
+ "presentation",
1388
+ "speech",
1389
+ "rehearse",
1390
+ "deadline",
1391
+ "application",
1392
+ "apolog",
1393
+ ]
1394
+ if any(term in lowered for term in answer_terms):
1395
+ return _weird_little_action(intake, answers, anchors, language)
1396
  return ""
1397
 
1398
 
 
1400
  lowered = (answers or "").lower()
1401
  if _is_zh(language):
1402
  if "邮件" in lowered or "email" in lowered:
1403
+ return _numbered_suggestions(
1404
+ [
1405
+ f"把「{anchor}」翻译成一个现实沟通问题:这封邮件只需要让对方知道哪一件事。",
1406
+ "先只打开草稿,补上主题和第一句话,把“开始”和“发送”拆开。",
1407
+ "给自己定一个稍后回看的时间;今天可以先存草稿,不一定马上发出去。",
1408
+ ],
1409
+ language,
1410
+ )
1411
  if "消息" in lowered or "发消息" in lowered:
1412
+ return _numbered_suggestions(
1413
+ [
1414
+ f"把「{anchor}」翻译成现实里一条需要落地的沟通。",
1415
+ "先写一条很短的进度消息,说明现在到哪一步,不要求把整件事立刻完成。",
1416
+ "如果还不确定,就先存草稿,等一个具体时间再决定是否发送。",
1417
+ ],
1418
+ language,
1419
+ )
1420
  if "作业" in lowered or "草稿" in lowered:
1421
+ return _numbered_suggestions(
1422
+ [
1423
+ f"把「{anchor}」翻译成现实里一个能靠近的草稿入口。",
1424
+ "先补一个标题或下一小段,不把它当成一次完整交卷。",
1425
+ "写完就停一下,给明天留下一个清楚的接续点。",
1426
+ ],
1427
+ language,
1428
+ )
1429
  return ""
1430
  if "email" in lowered or "message" in lowered:
1431
+ return _numbered_suggestions(
1432
+ [
1433
+ f"Translate {anchor} into a real-world communication question: what does the overdue email need the other person to know?",
1434
+ "Open the draft only long enough to add the subject and first sentence; separate starting from sending.",
1435
+ "Save it without sending, then choose one later review time.",
1436
+ ],
1437
+ language,
1438
  )
1439
  if "assignment" in lowered or "homework" in lowered or "draft" in lowered:
1440
+ return _numbered_suggestions(
1441
+ [
1442
+ f"Translate {anchor} into a real draft doorway, not a full submission.",
1443
+ "Add one heading or next tiny piece.",
1444
+ "Stop with a clear handoff note for tomorrow.",
1445
+ ],
1446
+ language,
1447
  )
1448
  if "presentation" in lowered or "speech" in lowered or "rehearse" in lowered:
1449
+ return _numbered_suggestions(
1450
+ [
1451
+ f"Let {anchor} narrow the real work to the opening minute.",
1452
+ "Rehearse that minute once, then write one note for the next pass.",
1453
+ ],
1454
+ language,
1455
+ )
1456
  if "deadline" in lowered or "application" in lowered:
1457
+ return _numbered_suggestions(
1458
+ [
1459
+ f"Translate {anchor} into one real checklist.",
1460
+ "Open the application and mark only the next missing item.",
1461
+ ],
1462
+ language,
1463
+ )
1464
  if "apolog" in lowered:
1465
+ return _numbered_suggestions(
1466
+ [
1467
+ f"Translate {anchor} into one repair step.",
1468
+ "Draft a single apology sentence privately before deciding whether to send it.",
1469
+ ],
1470
+ language,
1471
+ )
1472
  return ""
1473
 
1474
 
 
2085
  emotion_interpretation = _emotion_led_interpretation(intake, answers, anchors, language)
2086
  answer_interpretation = _answer_based_interpretation(answers, _answer_bridge_anchor(anchors), language)
2087
  answer_tip = _answer_based_today_tip(answers, anchors[0], language)
2088
+ answer_action = _answer_based_tiny_action(answers, intake, anchors, language)
2089
  answer_should_shape_visible_tip = bool((answer_tip or answer_action) and not _needs_comfort(answers, language))
2090
  if answer_interpretation and answer_should_shape_visible_tip:
2091
  polished.interpretation = answer_interpretation
 
2109
  )
2110
  elif not polished.interpretation.strip() or not _anchor_in_text(polished.interpretation, anchors):
2111
  polished.interpretation = _fallback_interpretation(intake, language)
2112
+ generic_tip_markers = [
2113
+ "drink water",
2114
+ "hydrate",
2115
+ "多休息",
2116
+ "保持积极",
2117
+ "take a walk",
2118
+ "press one very small elevator button",
2119
+ "按下一个很小的电梯按钮",
2120
+ "borrow one action",
2121
+ "借一个动作",
2122
+ "open the task",
2123
+ ]
2124
  emotion_tip = _emotion_led_today_tip(intake, answers, anchors, language)
2125
  if answer_tip and answer_should_shape_visible_tip:
2126
  polished.today_tip = answer_tip
 
2149
  or not _anchor_in_text(polished.today_tip, anchors)
2150
  ):
2151
  polished.today_tip = _grounded_today_tip(intake, language)
2152
+ hard_action_markers = [
2153
+ "address it immediately",
2154
+ "fix it immediately",
2155
+ "solve it immediately",
2156
+ "set a five-minute timer",
2157
+ "spend five minutes writing",
2158
+ "给自己 5 分钟",
2159
+ "用 5 分钟写下",
2160
+ "用 5 分钟做一个",
2161
+ ]
2162
  emotion_action = _emotion_led_tiny_action(intake, answers, anchors, language)
2163
  if answer_action and answer_should_shape_visible_tip:
2164
  polished.tiny_action = answer_action
 
2167
  elif answer_action:
2168
  polished.tiny_action = answer_action
2169
  elif _has_prophecy_frame(intake.merged_text()):
2170
+ polished.tiny_action = _weird_little_action(intake, answers, anchors, language)
 
 
 
 
2171
  elif _is_low_context_intake(intake) and _is_skip_answer(answers, language):
2172
+ polished.tiny_action = _weird_little_action(intake, answers, anchors, language)
 
 
 
 
2173
  elif (
2174
  not polished.tiny_action.strip()
2175
  or _is_placeholder_anchor(polished.tiny_action)
2176
  or not _anchor_in_text(polished.tiny_action, anchors)
2177
  or any(marker in polished.tiny_action.lower() for marker in hard_action_markers)
2178
  ):
2179
+ polished.tiny_action = _weird_little_action(intake, answers, anchors, language)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2180
  emotion_caring_note = _emotion_led_caring_note(intake, answers, language)
2181
  if emotion_caring_note:
2182
  polished.caring_note = emotion_caring_note
dream_customs/prompts.py CHANGED
@@ -50,7 +50,8 @@ Return strict JSON with:
50
  def today_tip_prompt(state: DreamQAState, language: str = "en") -> str:
51
  return f"""
52
  You are MiniCPM5-1B writing the final Dream QA result.
53
- Write a non-diagnostic interpretation draft and exactly one primary Today Tip / 今日小 Tips.
 
54
  First answer the user's stated question directly. If the user sounds scared, sad,
55
  overwhelmed, guilty, lonely, or asks for comfort, follow that emotion before giving any action.
56
  The interpretation must be step-by-step: use 2 to 4 short layers that move from
@@ -58,12 +59,17 @@ the user's feeling, to concrete dream anchors, to the 1-3 follow-up answers, to
58
  Do not collapse every dream into productivity advice such as opening a task,
59
  writing a first line, or making the first step smaller.
60
  Use non-certain language such as "也许", "可以把它当作", "maybe", or "for today, try".
61
- The today_tip and tiny_action must cite at least one concrete dream anchor and must change with the user's story,
62
- visual evidence, and follow-up answers. Do not reuse stock exercises such as "write two lines" unless the user's
63
- own dream or answer makes that exact action feel specific.
 
 
 
 
 
64
  Avoid prophecy, frightening certainty, medical advice, therapy framing, and generic wellness filler.
65
  Keep the whole result short, warm, emotionally responsive, and specific to the user's answer.
66
- The tiny_action must be a 5-minute self-check or small care step, not a command to solve the whole problem.
67
  Avoid demanding phrases such as "immediately", "must", "fix it", or "solve it".
68
  If the user asks for comfort, caring_note should be warm, specific, and validating.
69
  Use safety_note only for self-harm, harm to others, severe distress, severe insomnia, panic, or inability to function.
 
50
  def today_tip_prompt(state: DreamQAState, language: str = "en") -> str:
51
  return f"""
52
  You are MiniCPM5-1B writing the final Dream QA result.
53
+ Write a non-diagnostic interpretation draft, a waking-life Today Tip / 今日小 Tips with 1 to 3 small suggestions,
54
+ and one weird little thing / 古怪的小事.
55
  First answer the user's stated question directly. If the user sounds scared, sad,
56
  overwhelmed, guilty, lonely, or asks for comfort, follow that emotion before giving any action.
57
  The interpretation must be step-by-step: use 2 to 4 short layers that move from
 
59
  Do not collapse every dream into productivity advice such as opening a task,
60
  writing a first line, or making the first step smaller.
61
  Use non-certain language such as "也许", "可以把它当作", "maybe", or "for today, try".
62
+ The today_tip must be about the user's awake, real-world life, not about acting inside the dream scene.
63
+ It may refer to dream anchors, but each suggestion must translate them into practical waking-life choices,
64
+ ordinary constraints, and real-world consequences. Give 1 to 3 small suggestions; avoid rigid templates.
65
+ The tiny_action field is the weird little thing / 古怪的小事. It must cite at least one concrete dream anchor,
66
+ use real-world physics or an ordinary physical object, and create one strange, playful, eye-opening action
67
+ the user can actually do in 1 to 5 minutes while awake. It should feel random and fresh, not like a stock
68
+ self-check, journaling prompt, breathing exercise, or generic productivity hack.
69
+ The today_tip and tiny_action must change with the user's story, visual evidence, and follow-up answers.
70
  Avoid prophecy, frightening certainty, medical advice, therapy framing, and generic wellness filler.
71
  Keep the whole result short, warm, emotionally responsive, and specific to the user's answer.
72
+ The weird little thing must be harmless, legal, low-cost, non-embarrassing, and not a command to solve the whole problem.
73
  Avoid demanding phrases such as "immediately", "must", "fix it", or "solve it".
74
  If the user asks for comfort, caring_note should be warm, specific, and validating.
75
  Use safety_note only for self-harm, harm to others, severe distress, severe insomnia, panic, or inability to function.
dream_customs/render.py CHANGED
@@ -35,7 +35,7 @@ def render_today_tip_card(card: TodayTipCard, language: str = "en") -> str:
35
  else "Thanks for sharing this. Here is one grounded suggestion for today.",
36
  "interpretation": "补充解读" if is_zh else "Supporting reflection",
37
  "today_tip": "今天的小建议" if is_zh else "Today's small suggestion",
38
- "tiny_action": "没试过的小事" if is_zh else "Tiny action",
39
  "anchors": "关键词:" if is_zh else "Anchors:",
40
  "history": "追问记录" if is_zh else "Question record",
41
  "safety_default": "这不是诊断,只是一个温和的今日参考。"
 
35
  else "Thanks for sharing this. Here is one grounded suggestion for today.",
36
  "interpretation": "补充解读" if is_zh else "Supporting reflection",
37
  "today_tip": "今天的小建议" if is_zh else "Today's small suggestion",
38
+ "tiny_action": "古怪的小事" if is_zh else "Weird little thing",
39
  "anchors": "关键词:" if is_zh else "Anchors:",
40
  "history": "追问记录" if is_zh else "Question record",
41
  "safety_default": "这不是诊断,只是一个温和的今日参考。"
dream_customs/schema.py CHANGED
@@ -106,7 +106,7 @@ class TodayTipCard(BaseModel):
106
  f"今日小 Tips: {self.today_tip}",
107
  ]
108
  if self.tiny_action:
109
- lines.append(f"没试过的小事: {self.tiny_action}")
110
  if self.caring_note:
111
  lines.append(f"关心一句: {self.caring_note}")
112
  if self.safety_note:
 
106
  f"今日小 Tips: {self.today_tip}",
107
  ]
108
  if self.tiny_action:
109
+ lines.append(f"古怪的小事: {self.tiny_action}")
110
  if self.caring_note:
111
  lines.append(f"关心一句: {self.caring_note}")
112
  if self.safety_note:
dream_customs/ui/actions.py CHANGED
@@ -96,7 +96,7 @@ def _card_plain_text(card: TodayTipCard, language: str) -> str:
96
  f"Today Tip: {card.today_tip}",
97
  ]
98
  if card.tiny_action:
99
- lines.append(f"Tiny action: {card.tiny_action}")
100
  if card.caring_note:
101
  lines.append(f"Caring note: {card.caring_note}")
102
  if card.safety_note:
 
96
  f"Today Tip: {card.today_tip}",
97
  ]
98
  if card.tiny_action:
99
+ lines.append(f"Weird little thing: {card.tiny_action}")
100
  if card.caring_note:
101
  lines.append(f"Caring note: {card.caring_note}")
102
  if card.safety_note:
modal_backend/dream_customs_modal.py CHANGED
@@ -159,7 +159,48 @@ def _fallback_json_response(prompt: str) -> str:
159
  term in prompt.lower()
160
  for term in ("hurt myself", "self-harm", "unsafe", "撑不住", "很多天睡不着", "痛苦")
161
  )
162
- if "permit_id" in prompt or "practical_suggestion" in prompt:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  if is_cjk:
164
  payload: Dict[str, Any] = {
165
  "visitor_name": "蓝色放行章",
@@ -229,7 +270,20 @@ def _repair_json_response(prompt: str, text_output: str) -> str:
229
  if parsed is None or fallback is None:
230
  return _fallback_json_response(prompt)
231
 
232
- if "permit_id" in prompt or "practical_suggestion" in prompt:
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  required_keys = (
234
  "visitor_name",
235
  "permit_id",
 
159
  term in prompt.lower()
160
  for term in ("hurt myself", "self-harm", "unsafe", "撑不住", "很多天睡不着", "痛苦")
161
  )
162
+ if "today_tip" in prompt or "tiny_action" in prompt:
163
+ if is_cjk:
164
+ payload = {
165
+ "dream_summary": "你记录了一个带着办公楼、电梯和未发送邮件的梦。",
166
+ "main_question": "这个梦可能在提醒我什么?",
167
+ "dream_anchors": ["办公楼", "电梯", "邮件"],
168
+ "followup_questions": [],
169
+ "user_answers": [],
170
+ "interpretation": "也许这个梦不是在给出唯一答案,而是在把醒来后的卡住感放到一个具体画面里。",
171
+ "today_tip": (
172
+ "1. 把「办公楼」翻译成现实里最像卡住入口的一件事。"
173
+ "2. 只打开相关草稿或邮件。"
174
+ "3. 写下第一句话后先存起来,不要求马上发出。"
175
+ ),
176
+ "tiny_action": "找一张便利贴,画一个只到“草稿层”的电梯按钮,按一下,再只写第一句话。",
177
+ "caring_note": "你可以慢慢开始,不需要一醒来就抵达所有楼层。",
178
+ "safety_note": "",
179
+ }
180
+ else:
181
+ payload = {
182
+ "dream_summary": "You recorded a dream with an office building, elevator, and unsent email.",
183
+ "main_question": "What might this dream be asking me to notice today?",
184
+ "dream_anchors": ["office building", "elevator", "email"],
185
+ "followup_questions": [],
186
+ "user_answers": [],
187
+ "interpretation": "Maybe this dream is not giving one fixed answer; it is turning a waking stuck point into a concrete scene.",
188
+ "today_tip": (
189
+ "1. Translate the office building into the real-life doorway where you feel stuck. "
190
+ "2. Open only the related draft or email. "
191
+ "3. Add the first sentence and save it without sending yet."
192
+ ),
193
+ "tiny_action": "Draw an elevator button labeled Draft Floor on a sticky note, press it once, then write only the first sentence.",
194
+ "caring_note": "You can start slowly; you do not have to reach every floor this morning.",
195
+ "safety_note": "",
196
+ }
197
+ if distress:
198
+ payload["safety_note"] = (
199
+ "如果你已经连续很多天睡不好、非常痛苦,或担心自己会伤害自己/他人,请尽快联系可信任的人或专业支持。"
200
+ if is_cjk
201
+ else "If you have been unable to sleep for many nights, feel severe distress, or worry you may hurt yourself or someone else, please reach out to a trusted person or professional support now."
202
+ )
203
+ elif "permit_id" in prompt or "practical_suggestion" in prompt:
204
  if is_cjk:
205
  payload: Dict[str, Any] = {
206
  "visitor_name": "蓝色放行章",
 
270
  if parsed is None or fallback is None:
271
  return _fallback_json_response(prompt)
272
 
273
+ if "today_tip" in prompt or "tiny_action" in prompt:
274
+ required_keys = (
275
+ "dream_summary",
276
+ "main_question",
277
+ "dream_anchors",
278
+ "followup_questions",
279
+ "user_answers",
280
+ "interpretation",
281
+ "today_tip",
282
+ "tiny_action",
283
+ "caring_note",
284
+ "safety_note",
285
+ )
286
+ elif "permit_id" in prompt or "practical_suggestion" in prompt:
287
  required_keys = (
288
  "visitor_name",
289
  "permit_id",
scripts/evaluate_today_tip_quality.py CHANGED
@@ -13,7 +13,7 @@ from dream_customs.ui.actions import answer_to_card_action, skip_to_card_action,
13
  FIXTURE_PATH = Path("tests/fixtures/today_tip_eval_cases.json")
14
  OLD_CUSTOMS_TERMS = ["permit", "contraband", "clearance", "sealed", "pact"]
15
  FRIGHTENING_TERMS = ["you will fail", "prophecy says", "fate says", "must mean", "mental illness"]
16
- CHINESE_UI_LABELS = ["今日小", "梦境摘要", "想理解的问题", "解读草稿", "没试过的小事", "关心一句"]
17
  CHINESE_LEAKAGE_TERMS = ["数字", "电梯", "按钮", "楼层", "融化", "梦境"]
18
  HARD_COMMAND_PHRASES = ["address it immediately", "fix it immediately", "solve it immediately"]
19
 
 
13
  FIXTURE_PATH = Path("tests/fixtures/today_tip_eval_cases.json")
14
  OLD_CUSTOMS_TERMS = ["permit", "contraband", "clearance", "sealed", "pact"]
15
  FRIGHTENING_TERMS = ["you will fail", "prophecy says", "fate says", "must mean", "mental illness"]
16
+ CHINESE_UI_LABELS = ["今日小", "梦境摘要", "想理解的问题", "解读草稿", "没试过的小事", "古怪的小事", "关心一句"]
17
  CHINESE_LEAKAGE_TERMS = ["数字", "电梯", "按钮", "楼层", "融化", "梦境"]
18
  HARD_COMMAND_PHRASES = ["address it immediately", "fix it immediately", "solve it immediately"]
19
 
tests/test_pipeline.py CHANGED
@@ -7,6 +7,7 @@ from dream_customs.pipeline import (
7
  add_evidence,
8
  answer_question,
9
  ask_questions,
 
10
  build_intake,
11
  create_session,
12
  dated_permit_id,
@@ -19,7 +20,7 @@ from dream_customs.pipeline import (
19
  seal_pact,
20
  skip_question,
21
  )
22
- from dream_customs.prompts import pact_prompt
23
  from dream_customs.schema import PactCard, TodayTipCard, VisionWitness
24
 
25
 
@@ -271,6 +272,35 @@ def test_generate_today_tip_uses_chinese_concrete_answer_in_visible_tip():
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="我梦到自己掉进海里,醒来很害怕。我想知道这是不是说明我快撑不住了?",
@@ -478,6 +508,26 @@ def test_pact_prompt_requires_dream_grounded_card():
478
  assert "not a human name unless a person appears" in prompt
479
 
480
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
481
  def test_add_evidence_updates_session_with_text_image_audio_and_mood():
482
  session = add_evidence(
483
  create_session(),
 
7
  add_evidence,
8
  answer_question,
9
  ask_questions,
10
+ build_qa_state,
11
  build_intake,
12
  create_session,
13
  dated_permit_id,
 
20
  seal_pact,
21
  skip_question,
22
  )
23
+ from dream_customs.prompts import pact_prompt, today_tip_prompt
24
  from dream_customs.schema import PactCard, TodayTipCard, VisionWitness
25
 
26
 
 
272
  assert "融化" not in visible
273
 
274
 
275
+ def test_chinese_email_tip_becomes_real_world_suggestions_and_weird_action():
276
+ intake = build_intake(
277
+ dream_text=(
278
+ "昨晚梦见我在一栋很高的办公楼14层找一封迟迟没发出去的邮件。"
279
+ "电梯一直停在黑暗的走廊,我手里拿着一盏小台灯,感觉有点焦急。"
280
+ "醒来最想知道为什么梦里总是卡在电梯口。"
281
+ ),
282
+ mood="焦急",
283
+ )
284
+
285
+ card = generate_today_tip(
286
+ intake,
287
+ "我觉得那封邮件其实是在提醒我有一件事一直没有开始。今天可以先写第一句话,不一定马上发出去。",
288
+ FakeTextClient(),
289
+ language="zh",
290
+ )
291
+
292
+ assert "办公楼」当成允许慢慢开始的按钮" not in card.today_tip
293
+ assert "现实" in card.today_tip
294
+ assert "1." in card.today_tip and "2." in card.today_tip
295
+ assert "邮件" in card.today_tip
296
+ assert "第一句" in card.today_tip or "第一句话" in card.today_tip
297
+ assert "便利贴" in card.tiny_action or "纸" in card.tiny_action
298
+ assert "按钮" in card.tiny_action or "电梯" in card.tiny_action
299
+ assert "按一下" in card.tiny_action or "画" in card.tiny_action
300
+ assert "给自己 5 分钟,只打开那封邮件" not in card.tiny_action
301
+ assert "这份这个感受" not in card.caring_note
302
+
303
+
304
  def test_generate_today_tip_follows_user_question_and_comfort_need_in_chinese():
305
  intake = build_intake(
306
  dream_text="我梦到自己掉进海里,醒来很害怕。我想知道这是不是说明我快撑不住了?",
 
508
  assert "not a human name unless a person appears" in prompt
509
 
510
 
511
+ def test_today_tip_prompt_requires_waking_life_suggestions_and_weird_action():
512
+ intake = build_intake(
513
+ dream_text="我梦见办公楼里的电梯停在 14 层,一封邮件一直发不出去。",
514
+ mood="焦急",
515
+ )
516
+ state = build_qa_state(
517
+ intake,
518
+ answers=["我觉得那封邮件是在提醒我有一件现实工作沟通还没开始。"],
519
+ language="zh",
520
+ )
521
+
522
+ prompt = today_tip_prompt(state, language="zh")
523
+
524
+ assert "waking-life" in prompt
525
+ assert "1 to 3" in prompt
526
+ assert "weird little thing" in prompt
527
+ assert "real-world physics" in prompt
528
+ assert "古怪的小事" in prompt
529
+
530
+
531
  def test_add_evidence_updates_session_with_text_image_audio_and_mood():
532
  session = add_evidence(
533
  create_session(),
tests/test_render.py CHANGED
@@ -96,3 +96,25 @@ def test_today_tip_card_prioritizes_tip_before_interpretation():
96
  )
97
 
98
  assert html.index("Today's small suggestion") < html.index("Maybe this dream is pointing to")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  )
97
 
98
  assert html.index("Today's small suggestion") < html.index("Maybe this dream is pointing to")
99
+
100
+
101
+ def test_today_tip_card_renames_tiny_action_to_weird_little_thing():
102
+ card = TodayTipCard(
103
+ dream_summary="你梦见办公楼、电梯和一封迟迟没发出去的邮件。",
104
+ main_question="为什么我总是卡在电梯口?",
105
+ dream_anchors=["办公楼", "电梯", "邮件"],
106
+ followup_questions=[],
107
+ user_answers=[],
108
+ interpretation="也许它在提醒你把开始和发送分开。",
109
+ today_tip="1. 在现实里先打开邮件草稿。2. 只写第一句话。",
110
+ tiny_action="画一个只到草稿层的电梯按钮,按一下再写第一句。",
111
+ caring_note="慢一点也可以。",
112
+ )
113
+
114
+ zh_html = render_today_tip_card(card, language="zh")
115
+ en_html = render_today_tip_card(card, language="en")
116
+
117
+ assert "古怪的小事" in zh_html
118
+ assert "没试过的小事" not in zh_html
119
+ assert "Weird little thing" in en_html
120
+ assert "Tiny action" not in en_html
tests/test_schema.py CHANGED
@@ -1,4 +1,4 @@
1
- from dream_customs.schema import CustomsSession, DreamIntake, EvidenceItem, PactCard
2
 
3
 
4
  def test_dream_intake_defaults_lists():
@@ -39,6 +39,22 @@ def test_pact_card_requires_core_fields():
39
  assert "Late Elevator" in card.to_plain_text()
40
 
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  def test_customs_session_defaults_to_empty_workbench():
43
  session = CustomsSession()
44
  assert session.phase == "empty"
 
1
+ from dream_customs.schema import CustomsSession, DreamIntake, EvidenceItem, PactCard, TodayTipCard
2
 
3
 
4
  def test_dream_intake_defaults_lists():
 
39
  assert "Late Elevator" in card.to_plain_text()
40
 
41
 
42
+ def test_today_tip_plain_text_uses_weird_little_thing_label():
43
+ card = TodayTipCard(
44
+ dream_summary="你梦见办公楼、电梯和邮件。",
45
+ main_question="为什么会卡住?",
46
+ dream_anchors=["办公楼", "电梯", "邮件"],
47
+ interpretation="也许这是一个开始被放大的画面。",
48
+ today_tip="1. 在现实里打开邮件草稿。2. 只写第一句话。",
49
+ tiny_action="画一个草稿层按钮,按一下再开始。",
50
+ )
51
+
52
+ text = card.to_plain_text()
53
+
54
+ assert "古怪的小事:" in text
55
+ assert "没试过的小事:" not in text
56
+
57
+
58
  def test_customs_session_defaults_to_empty_workbench():
59
  session = CustomsSession()
60
  assert session.phase == "empty"