style: convert dream customs copy to english
#11
by ADJCJH - opened
- README.md +1 -1
- dream_customs/models.py +10 -35
- dream_customs/pipeline.py +26 -41
- dream_customs/prompts.py +4 -2
- dream_customs/render.py +1 -1
- dream_customs/ui/actions.py +24 -24
- dream_customs/ui/app.py +66 -66
- dream_customs/ui/copy.py +8 -8
- dream_customs/ui/styles.py +5 -0
- tests/test_app_logic.py +3 -3
- tests/test_pipeline.py +4 -4
- tests/test_ui_actions.py +13 -8
README.md
CHANGED
|
@@ -20,7 +20,7 @@ tags:
|
|
| 20 |
- dream-journal
|
| 21 |
---
|
| 22 |
|
| 23 |
-
# Dream Customs
|
| 24 |
|
| 25 |
A Build Small Hackathon Gradio app that helps users form a playful alliance with last night's dream.
|
| 26 |
|
|
|
|
| 20 |
- dream-journal
|
| 21 |
---
|
| 22 |
|
| 23 |
+
# Dream Customs
|
| 24 |
|
| 25 |
A Build Small Hackathon Gradio app that helps users form a playful alliance with last night's dream.
|
| 26 |
|
dream_customs/models.py
CHANGED
|
@@ -10,10 +10,6 @@ from dream_customs.prompts import visual_clue_prompt
|
|
| 10 |
from dream_customs.schema import PactCard
|
| 11 |
|
| 12 |
|
| 13 |
-
def _contains_cjk(text: str) -> bool:
|
| 14 |
-
return bool(re.search(r"[\u4e00-\u9fff]", text))
|
| 15 |
-
|
| 16 |
-
|
| 17 |
class FakeVisionClient:
|
| 18 |
def extract_clues(self, image_path: Optional[str]) -> List[str]:
|
| 19 |
if not image_path:
|
|
@@ -30,47 +26,26 @@ class FakeASRClient:
|
|
| 30 |
|
| 31 |
class FakeTextClient:
|
| 32 |
def generate_negotiation(self, prompt: str) -> Dict[str, Any]:
|
| 33 |
-
if _contains_cjk(prompt):
|
| 34 |
-
return {
|
| 35 |
-
"visitor_name": "迟到的电梯",
|
| 36 |
-
"questions": [
|
| 37 |
-
"这个梦醒来后最明显的感觉是什么:紧张、疲惫、好奇,还是别的?",
|
| 38 |
-
"今天有没有一件现实里的小事,你希望先轻一点开始?",
|
| 39 |
-
"你想让我给你偏生活建议,还是偏 5 分钟怪趣任务?",
|
| 40 |
-
],
|
| 41 |
-
"tone_note": "这个来访者也许不是敌人,而是在提醒你把开始和完成分开。",
|
| 42 |
-
}
|
| 43 |
return {
|
| 44 |
"visitor_name": "Late Elevator",
|
| 45 |
"questions": [
|
| 46 |
-
"
|
| 47 |
-
"
|
| 48 |
-
"Would you
|
| 49 |
],
|
| 50 |
-
"tone_note": "This visitor may be asking
|
| 51 |
}
|
| 52 |
|
| 53 |
def generate_pact(self, prompt: str) -> PactCard:
|
| 54 |
-
if _contains_cjk(prompt):
|
| 55 |
-
return PactCard(
|
| 56 |
-
visitor_name="迟到的电梯",
|
| 57 |
-
permit_id="DC-DEMO-014",
|
| 58 |
-
contraband=["未申报的焦虑", "融化的按钮", "一小袋没来得及开始的事"],
|
| 59 |
-
risk_level="橙色:需要被安置,但不需要被害怕",
|
| 60 |
-
alliance_reading="这个梦也许在提醒你,今天先把启动一件事和完成一件事分开。",
|
| 61 |
-
practical_suggestion="今天先把最重要的一件事写成 10 分钟能开始的版本,只要求打开它,不要求完成。",
|
| 62 |
-
weird_task="在纸上画一个很小的电梯按钮,给今天的最小任务按一下,然后做 5 分钟。",
|
| 63 |
-
bedtime_release="今日电梯已停靠,未完成事项明日再报关。",
|
| 64 |
-
)
|
| 65 |
return PactCard(
|
| 66 |
visitor_name="Late Elevator",
|
| 67 |
permit_id="DC-DEMO-014",
|
| 68 |
-
contraband=["unfiled
|
| 69 |
-
risk_level="orange:
|
| 70 |
-
alliance_reading="This
|
| 71 |
-
practical_suggestion="
|
| 72 |
-
weird_task="
|
| 73 |
-
bedtime_release="
|
| 74 |
)
|
| 75 |
|
| 76 |
|
|
|
|
| 10 |
from dream_customs.schema import PactCard
|
| 11 |
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
class FakeVisionClient:
|
| 14 |
def extract_clues(self, image_path: Optional[str]) -> List[str]:
|
| 15 |
if not image_path:
|
|
|
|
| 26 |
|
| 27 |
class FakeTextClient:
|
| 28 |
def generate_negotiation(self, prompt: str) -> Dict[str, Any]:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
return {
|
| 30 |
"visitor_name": "Late Elevator",
|
| 31 |
"questions": [
|
| 32 |
+
"What feeling was strongest when you woke up: pressure, tiredness, curiosity, or something else?",
|
| 33 |
+
"Is there one real-life thing you would like to make easier today?",
|
| 34 |
+
"Would you rather receive a practical life tip, a tiny odd task, or both?",
|
| 35 |
],
|
| 36 |
+
"tone_note": "This visitor may be asking for a smaller start, not a perfect finish.",
|
| 37 |
}
|
| 38 |
|
| 39 |
def generate_pact(self, prompt: str) -> PactCard:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
return PactCard(
|
| 41 |
visitor_name="Late Elevator",
|
| 42 |
permit_id="DC-DEMO-014",
|
| 43 |
+
contraband=["unfiled pressure", "melted buttons", "a pocket of unstarted tasks"],
|
| 44 |
+
risk_level="orange: worth placing gently, not fearing",
|
| 45 |
+
alliance_reading="This dream may be pointing to the pressure of starting and finishing at the same time.",
|
| 46 |
+
practical_suggestion="Choose one task and define only its first 10 minutes. Open it, then pause for water.",
|
| 47 |
+
weird_task="Draw a tiny elevator button on paper, press it once, and work for five minutes.",
|
| 48 |
+
bedtime_release="The elevator has docked for tonight. Unfinished floors can report tomorrow.",
|
| 49 |
)
|
| 50 |
|
| 51 |
|
dream_customs/pipeline.py
CHANGED
|
@@ -63,32 +63,34 @@ def _looks_unclear_or_dream_literal(text: str) -> bool:
|
|
| 63 |
"香薰皮",
|
| 64 |
"果酱",
|
| 65 |
"Dreamer",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
]
|
| 67 |
return any(term in clean for term in dream_literals)
|
| 68 |
|
| 69 |
|
| 70 |
def _safe_practical_suggestion(intake: DreamIntake) -> str:
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
|
|
|
| 76 |
return "Do one low-risk stabilizing thing today: drink water, eat something, and write the most important task as a 10-minute first step."
|
| 77 |
|
| 78 |
|
| 79 |
def _safe_weird_task(intake: DreamIntake) -> str:
|
| 80 |
-
if _contains_cjk(intake.merged_text()):
|
| 81 |
-
return "把今天最小的任务写在纸上,旁边画一个很小的通行章,然后只做 5 分钟。"
|
| 82 |
return "Write your smallest task on paper, draw a tiny clearance stamp beside it, and work on it for just five minutes."
|
| 83 |
|
| 84 |
|
| 85 |
def _polish_card_for_daily_use(card: PactCard, intake: DreamIntake, answers: str) -> PactCard:
|
| 86 |
polished = card.model_copy(deep=True)
|
| 87 |
merged = "\n".join([intake.merged_text(), answers or ""])
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
if chinese and (not polished.visitor_name.strip() or re.fullmatch(r"[A-Za-z\s_-]+", polished.visitor_name.strip())):
|
| 91 |
-
polished.visitor_name = "昨夜来访者"
|
| 92 |
|
| 93 |
if _looks_unclear_or_dream_literal(polished.practical_suggestion):
|
| 94 |
polished.practical_suggestion = _safe_practical_suggestion(intake)
|
|
@@ -98,11 +100,13 @@ def _polish_card_for_daily_use(card: PactCard, intake: DreamIntake, answers: str
|
|
| 98 |
elif len((polished.weird_task or "").strip()) < 8:
|
| 99 |
polished.weird_task = _safe_weird_task(intake)
|
| 100 |
|
| 101 |
-
if
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
|
|
|
|
|
|
| 106 |
|
| 107 |
if not needs_escalation(merged):
|
| 108 |
polished.safety_note = ""
|
|
@@ -157,10 +161,6 @@ def create_session() -> CustomsSession:
|
|
| 157 |
)
|
| 158 |
|
| 159 |
|
| 160 |
-
def _contains_cjk(text: str) -> bool:
|
| 161 |
-
return bool(re.search(r"[\u4e00-\u9fff]", text))
|
| 162 |
-
|
| 163 |
-
|
| 164 |
def _append_text(existing: str, new_text: str) -> str:
|
| 165 |
new_text = new_text.strip()
|
| 166 |
if not new_text:
|
|
@@ -315,10 +315,7 @@ def ask_questions(session: CustomsSession, text_client, force_another: bool = Fa
|
|
| 315 |
questions = [question for question in negotiation.get("questions", []) if question]
|
| 316 |
fresh_questions = [question for question in questions if question not in next_session.question_history]
|
| 317 |
if force_another and not fresh_questions:
|
| 318 |
-
|
| 319 |
-
fresh_questions = ["如果海关只批准一个更小的动作,你希望今天先放行哪一件事?"]
|
| 320 |
-
else:
|
| 321 |
-
fresh_questions = ["If customs approves one smaller action today, which one should it release first?"]
|
| 322 |
if not fresh_questions:
|
| 323 |
fresh_questions = questions[:3]
|
| 324 |
|
|
@@ -353,10 +350,7 @@ def answer_question(session: CustomsSession, answer: str) -> CustomsSession:
|
|
| 353 |
|
| 354 |
def skip_question(session: CustomsSession) -> CustomsSession:
|
| 355 |
next_session = session.model_copy(deep=True)
|
| 356 |
-
|
| 357 |
-
skip_text = "用户选择跳过这一轮问题。"
|
| 358 |
-
else:
|
| 359 |
-
skip_text = "The user chose to skip this question."
|
| 360 |
next_session.answer_history.append(skip_text)
|
| 361 |
next_session.phase = "negotiating"
|
| 362 |
next_session.events.append(_event("user", "Question skipped", skip_text, status="skipped"))
|
|
@@ -392,29 +386,20 @@ def draft_pact(session: CustomsSession, text_client) -> CustomsSession:
|
|
| 392 |
def _apply_revision_hint(card: PactCard, revision_request: str) -> PactCard:
|
| 393 |
request = revision_request.lower()
|
| 394 |
revised = card.model_copy(deep=True)
|
| 395 |
-
cjk = _contains_cjk(" ".join([revision_request, card.visitor_name, card.alliance_reading]))
|
| 396 |
if any(term in request for term in ["strange", "weird", "怪", "更奇怪", "更怪"]):
|
| 397 |
revised.weird_task = (
|
| 398 |
-
"
|
| 399 |
-
if cjk
|
| 400 |
-
else "Write the smallest task on paper and stamp it with an invisible release mark."
|
| 401 |
)
|
| 402 |
elif any(term in request for term in ["gentle", "softer", "温和", "轻一点"]):
|
| 403 |
revised.risk_level = (
|
| 404 |
-
"
|
| 405 |
-
if cjk
|
| 406 |
-
else "soft orange: place it gently before interpreting it"
|
| 407 |
)
|
| 408 |
revised.practical_suggestion = (
|
| 409 |
-
"
|
| 410 |
-
if cjk
|
| 411 |
-
else "Choose one start that does not need finishing today. Stop after five minutes."
|
| 412 |
)
|
| 413 |
elif revision_request.strip():
|
| 414 |
revised.practical_suggestion = (
|
| 415 |
-
f"{revised.practical_suggestion}
|
| 416 |
-
if cjk
|
| 417 |
-
else f"{revised.practical_suggestion} Revision note: {revision_request.strip()}"
|
| 418 |
)
|
| 419 |
return revised
|
| 420 |
|
|
|
|
| 63 |
"香薰皮",
|
| 64 |
"果酱",
|
| 65 |
"Dreamer",
|
| 66 |
+
"dream content has no medical value",
|
| 67 |
+
"release stamp",
|
| 68 |
+
"customs stamp",
|
| 69 |
+
"clearance stamp",
|
| 70 |
+
"magical",
|
| 71 |
]
|
| 72 |
return any(term in clean for term in dream_literals)
|
| 73 |
|
| 74 |
|
| 75 |
def _safe_practical_suggestion(intake: DreamIntake) -> str:
|
| 76 |
+
mood = intake.mood.strip().lower()
|
| 77 |
+
if mood in {"uneasy", "foggy", "tired", "焦虑", "迷雾", "累"}:
|
| 78 |
+
return (
|
| 79 |
+
"Start with one body-level reset today: drink water, eat something simple, "
|
| 80 |
+
"then write the most important task as a 10-minute first step."
|
| 81 |
+
)
|
| 82 |
return "Do one low-risk stabilizing thing today: drink water, eat something, and write the most important task as a 10-minute first step."
|
| 83 |
|
| 84 |
|
| 85 |
def _safe_weird_task(intake: DreamIntake) -> str:
|
|
|
|
|
|
|
| 86 |
return "Write your smallest task on paper, draw a tiny clearance stamp beside it, and work on it for just five minutes."
|
| 87 |
|
| 88 |
|
| 89 |
def _polish_card_for_daily_use(card: PactCard, intake: DreamIntake, answers: str) -> PactCard:
|
| 90 |
polished = card.model_copy(deep=True)
|
| 91 |
merged = "\n".join([intake.merged_text(), answers or ""])
|
| 92 |
+
if not polished.visitor_name.strip() or polished.visitor_name.strip().lower() == "dreamer":
|
| 93 |
+
polished.visitor_name = "Night Visitor"
|
|
|
|
|
|
|
| 94 |
|
| 95 |
if _looks_unclear_or_dream_literal(polished.practical_suggestion):
|
| 96 |
polished.practical_suggestion = _safe_practical_suggestion(intake)
|
|
|
|
| 100 |
elif len((polished.weird_task or "").strip()) < 8:
|
| 101 |
polished.weird_task = _safe_weird_task(intake)
|
| 102 |
|
| 103 |
+
if len((polished.alliance_reading or "").strip()) < 12 or "联盟成员" in polished.alliance_reading:
|
| 104 |
+
polished.alliance_reading = (
|
| 105 |
+
"You can treat this as a small signal from last night's feelings, not a prophecy. "
|
| 106 |
+
"Today, protect a realistic pace."
|
| 107 |
+
)
|
| 108 |
+
if polished.risk_level.strip() in {"低", "中", "高", "low", "medium", "high"}:
|
| 109 |
+
polished.risk_level = "medium: handle gently, without treating it as a warning sign"
|
| 110 |
|
| 111 |
if not needs_escalation(merged):
|
| 112 |
polished.safety_note = ""
|
|
|
|
| 161 |
)
|
| 162 |
|
| 163 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
def _append_text(existing: str, new_text: str) -> str:
|
| 165 |
new_text = new_text.strip()
|
| 166 |
if not new_text:
|
|
|
|
| 315 |
questions = [question for question in negotiation.get("questions", []) if question]
|
| 316 |
fresh_questions = [question for question in questions if question not in next_session.question_history]
|
| 317 |
if force_another and not fresh_questions:
|
| 318 |
+
fresh_questions = ["If today only needs one smaller first step, what should that step be?"]
|
|
|
|
|
|
|
|
|
|
| 319 |
if not fresh_questions:
|
| 320 |
fresh_questions = questions[:3]
|
| 321 |
|
|
|
|
| 350 |
|
| 351 |
def skip_question(session: CustomsSession) -> CustomsSession:
|
| 352 |
next_session = session.model_copy(deep=True)
|
| 353 |
+
skip_text = "The user chose to skip this question."
|
|
|
|
|
|
|
|
|
|
| 354 |
next_session.answer_history.append(skip_text)
|
| 355 |
next_session.phase = "negotiating"
|
| 356 |
next_session.events.append(_event("user", "Question skipped", skip_text, status="skipped"))
|
|
|
|
| 386 |
def _apply_revision_hint(card: PactCard, revision_request: str) -> PactCard:
|
| 387 |
request = revision_request.lower()
|
| 388 |
revised = card.model_copy(deep=True)
|
|
|
|
| 389 |
if any(term in request for term in ["strange", "weird", "怪", "更奇怪", "更怪"]):
|
| 390 |
revised.weird_task = (
|
| 391 |
+
"Write the smallest task on paper and stamp it with an invisible release mark."
|
|
|
|
|
|
|
| 392 |
)
|
| 393 |
elif any(term in request for term in ["gentle", "softer", "温和", "轻一点"]):
|
| 394 |
revised.risk_level = (
|
| 395 |
+
"soft orange: place it gently before interpreting it"
|
|
|
|
|
|
|
| 396 |
)
|
| 397 |
revised.practical_suggestion = (
|
| 398 |
+
"Choose one start that does not need finishing today. Stop after five minutes."
|
|
|
|
|
|
|
| 399 |
)
|
| 400 |
elif revision_request.strip():
|
| 401 |
revised.practical_suggestion = (
|
| 402 |
+
f"{revised.practical_suggestion} Revision note: {revision_request.strip()}"
|
|
|
|
|
|
|
| 403 |
)
|
| 404 |
return revised
|
| 405 |
|
dream_customs/prompts.py
CHANGED
|
@@ -62,8 +62,9 @@ Give:
|
|
| 62 |
- practical_suggestion: one safe, concrete life tip for today, such as hydration, eating, writing one task down, taking a short walk, reducing one task, checking the calendar, or asking for help. It must not be mystical or dream-literal.
|
| 63 |
- weird_task: one harmless, playful, slightly odd thing doable in 5 minutes. It can be imaginative, but it must be understandable.
|
| 64 |
- safety_note: empty string unless the user mentions self-harm, harming others, severe distress, severe insomnia, panic, or inability to function.
|
| 65 |
-
Use warm, non-clinical language.
|
| 66 |
-
Avoid generic
|
|
|
|
| 67 |
|
| 68 |
Dream intake:
|
| 69 |
{intake.merged_text()}
|
|
@@ -85,6 +86,7 @@ Do not diagnose. Do not make the dream sound certain or frightening.
|
|
| 85 |
Keep practical_suggestion safe, concrete, and useful for daily life.
|
| 86 |
Keep weird_task playful but understandable and separate from the practical suggestion.
|
| 87 |
Use safety_note only for severe distress or safety risk; otherwise return an empty string.
|
|
|
|
| 88 |
|
| 89 |
Dream intake:
|
| 90 |
{intake.merged_text()}
|
|
|
|
| 62 |
- practical_suggestion: one safe, concrete life tip for today, such as hydration, eating, writing one task down, taking a short walk, reducing one task, checking the calendar, or asking for help. It must not be mystical or dream-literal.
|
| 63 |
- weird_task: one harmless, playful, slightly odd thing doable in 5 minutes. It can be imaginative, but it must be understandable.
|
| 64 |
- safety_note: empty string unless the user mentions self-harm, harming others, severe distress, severe insomnia, panic, or inability to function.
|
| 65 |
+
Use warm, non-clinical language.
|
| 66 |
+
Avoid generic names like "Dreamer"; name the dream visitor with a short, vivid phrase.
|
| 67 |
+
Write the final card in polished English for an English-language demo, even if the dream fragment contains another language.
|
| 68 |
|
| 69 |
Dream intake:
|
| 70 |
{intake.merged_text()}
|
|
|
|
| 86 |
Keep practical_suggestion safe, concrete, and useful for daily life.
|
| 87 |
Keep weird_task playful but understandable and separate from the practical suggestion.
|
| 88 |
Use safety_note only for severe distress or safety risk; otherwise return an empty string.
|
| 89 |
+
Write the revised card in polished English for an English-language demo, even if the dream fragment contains another language.
|
| 90 |
|
| 91 |
Dream intake:
|
| 92 |
{intake.merged_text()}
|
dream_customs/render.py
CHANGED
|
@@ -33,7 +33,7 @@ def render_status_bar(session: CustomsSession, text_backend: str = "demo", visio
|
|
| 33 |
<div class="dc-brand-lockup">
|
| 34 |
<span class="dc-brand-mark">DC</span>
|
| 35 |
<div>
|
| 36 |
-
<h1>Dream Customs
|
| 37 |
<p>Turn last night's strange visitor into one gentle pact for the day: a safe life tip, a small weird task, and a bedtime release.</p>
|
| 38 |
</div>
|
| 39 |
</div>
|
|
|
|
| 33 |
<div class="dc-brand-lockup">
|
| 34 |
<span class="dc-brand-mark">DC</span>
|
| 35 |
<div>
|
| 36 |
+
<h1>Dream Customs</h1>
|
| 37 |
<p>Turn last night's strange visitor into one gentle pact for the day: a safe life tip, a small weird task, and a bedtime release.</p>
|
| 38 |
</div>
|
| 39 |
</div>
|
dream_customs/ui/actions.py
CHANGED
|
@@ -42,20 +42,20 @@ def _trim_to_one_visible_question(session: CustomsSession, previous_count: int)
|
|
| 42 |
|
| 43 |
|
| 44 |
def _card_plain_text(card: PactCard) -> str:
|
| 45 |
-
contraband = "
|
| 46 |
parts = [
|
| 47 |
-
"
|
| 48 |
-
f"
|
| 49 |
-
f"
|
| 50 |
-
f"
|
| 51 |
-
f"
|
| 52 |
-
f"
|
| 53 |
-
f"
|
| 54 |
-
f"5
|
| 55 |
-
f"
|
| 56 |
]
|
| 57 |
if card.safety_note:
|
| 58 |
-
parts.append(f"
|
| 59 |
return "\n".join(parts)
|
| 60 |
|
| 61 |
|
|
@@ -63,7 +63,7 @@ def _render_today_pass(card: PactCard) -> str:
|
|
| 63 |
contraband = "".join(f"<li>{escape(item)}</li>" for item in card.contraband)
|
| 64 |
safety = (
|
| 65 |
"<section class='dc-pass-safety'>"
|
| 66 |
-
"<strong>
|
| 67 |
f"<p>{escape(card.safety_note)}</p>"
|
| 68 |
"</section>"
|
| 69 |
if card.safety_note
|
|
@@ -72,33 +72,33 @@ def _render_today_pass(card: PactCard) -> str:
|
|
| 72 |
return f"""
|
| 73 |
<article class="dc-pass-card">
|
| 74 |
<div class="dc-pass-topline">
|
| 75 |
-
<span>
|
| 76 |
<span>{escape(card.permit_id)}</span>
|
| 77 |
</div>
|
| 78 |
<h2>{escape(card.visitor_name)}</h2>
|
| 79 |
<p class="dc-pass-risk">{escape(card.risk_level)}</p>
|
| 80 |
<section>
|
| 81 |
-
<h3>
|
| 82 |
<p>{escape(card.alliance_reading)}</p>
|
| 83 |
</section>
|
| 84 |
<section>
|
| 85 |
-
<h3>
|
| 86 |
<p>{escape(card.practical_suggestion)}</p>
|
| 87 |
</section>
|
| 88 |
<section>
|
| 89 |
-
<h3>5
|
| 90 |
<p>{escape(card.weird_task)}</p>
|
| 91 |
</section>
|
| 92 |
<section>
|
| 93 |
-
<h3>
|
| 94 |
<ul>{contraband}</ul>
|
| 95 |
</section>
|
| 96 |
<section>
|
| 97 |
-
<h3>
|
| 98 |
<p>{escape(card.bedtime_release)}</p>
|
| 99 |
</section>
|
| 100 |
{safety}
|
| 101 |
-
<div class="dc-pass-seal">SEALED /
|
| 102 |
</article>
|
| 103 |
""".strip()
|
| 104 |
|
|
@@ -116,7 +116,7 @@ def _view_payload(session: CustomsSession, text_backend: str, vision_backend: st
|
|
| 116 |
"phase": session.phase,
|
| 117 |
"question": _questions(session)[0] if _questions(session) else "",
|
| 118 |
"questions": _questions(session),
|
| 119 |
-
"card_title": "
|
| 120 |
"card_text": _card_plain_text(card) if card else "",
|
| 121 |
"card_html": _render_today_pass(card) if card else "",
|
| 122 |
"error": error,
|
|
@@ -135,12 +135,12 @@ def _view(session: CustomsSession, text_backend: str, vision_backend: str, **set
|
|
| 135 |
|
| 136 |
def _notice_for_status(status: str, error: str = "") -> str:
|
| 137 |
if status == "error":
|
| 138 |
-
return error or "
|
| 139 |
if status == "question":
|
| 140 |
-
return "
|
| 141 |
if status == "card":
|
| 142 |
-
return "
|
| 143 |
-
return "
|
| 144 |
|
| 145 |
|
| 146 |
def initial_mobile_state(
|
|
|
|
| 42 |
|
| 43 |
|
| 44 |
def _card_plain_text(card: PactCard) -> str:
|
| 45 |
+
contraband = ", ".join(card.contraband)
|
| 46 |
parts = [
|
| 47 |
+
"Today's Clearance Pass",
|
| 48 |
+
f"Visitor: {card.visitor_name}",
|
| 49 |
+
f"Permit ID: {card.permit_id}",
|
| 50 |
+
f"Emotional contraband: {contraband}",
|
| 51 |
+
f"Risk level: {card.risk_level}",
|
| 52 |
+
f"Alliance reading: {card.alliance_reading}",
|
| 53 |
+
f"Life tip for today: {card.practical_suggestion}",
|
| 54 |
+
f"5-minute odd task: {card.weird_task}",
|
| 55 |
+
f"Bedtime release: {card.bedtime_release}",
|
| 56 |
]
|
| 57 |
if card.safety_note:
|
| 58 |
+
parts.append(f"Support note: {card.safety_note}")
|
| 59 |
return "\n".join(parts)
|
| 60 |
|
| 61 |
|
|
|
|
| 63 |
contraband = "".join(f"<li>{escape(item)}</li>" for item in card.contraband)
|
| 64 |
safety = (
|
| 65 |
"<section class='dc-pass-safety'>"
|
| 66 |
+
"<strong>Support note, if needed</strong>"
|
| 67 |
f"<p>{escape(card.safety_note)}</p>"
|
| 68 |
"</section>"
|
| 69 |
if card.safety_note
|
|
|
|
| 72 |
return f"""
|
| 73 |
<article class="dc-pass-card">
|
| 74 |
<div class="dc-pass-topline">
|
| 75 |
+
<span>Today's Clearance Pass</span>
|
| 76 |
<span>{escape(card.permit_id)}</span>
|
| 77 |
</div>
|
| 78 |
<h2>{escape(card.visitor_name)}</h2>
|
| 79 |
<p class="dc-pass-risk">{escape(card.risk_level)}</p>
|
| 80 |
<section>
|
| 81 |
+
<h3>What it may be carrying</h3>
|
| 82 |
<p>{escape(card.alliance_reading)}</p>
|
| 83 |
</section>
|
| 84 |
<section>
|
| 85 |
+
<h3>Life tip for today</h3>
|
| 86 |
<p>{escape(card.practical_suggestion)}</p>
|
| 87 |
</section>
|
| 88 |
<section>
|
| 89 |
+
<h3>5-minute odd task</h3>
|
| 90 |
<p>{escape(card.weird_task)}</p>
|
| 91 |
</section>
|
| 92 |
<section>
|
| 93 |
+
<h3>Emotional contraband</h3>
|
| 94 |
<ul>{contraband}</ul>
|
| 95 |
</section>
|
| 96 |
<section>
|
| 97 |
+
<h3>Bedtime release</h3>
|
| 98 |
<p>{escape(card.bedtime_release)}</p>
|
| 99 |
</section>
|
| 100 |
{safety}
|
| 101 |
+
<div class="dc-pass-seal">SEALED / CLEARED</div>
|
| 102 |
</article>
|
| 103 |
""".strip()
|
| 104 |
|
|
|
|
| 116 |
"phase": session.phase,
|
| 117 |
"question": _questions(session)[0] if _questions(session) else "",
|
| 118 |
"questions": _questions(session),
|
| 119 |
+
"card_title": "Today's Clearance Pass" if card else "",
|
| 120 |
"card_text": _card_plain_text(card) if card else "",
|
| 121 |
"card_html": _render_today_pass(card) if card else "",
|
| 122 |
"error": error,
|
|
|
|
| 135 |
|
| 136 |
def _notice_for_status(status: str, error: str = "") -> str:
|
| 137 |
if status == "error":
|
| 138 |
+
return error or "Dream Customs has not received a fragment yet."
|
| 139 |
if status == "question":
|
| 140 |
+
return "Optional check-in: add one grounded detail, or skip straight to the pass."
|
| 141 |
if status == "card":
|
| 142 |
+
return "Today's pass is sealed. Treat it as a gentle action cue, not a diagnosis."
|
| 143 |
+
return "Write a sentence, a few lines, or paste a dream fragment. Text always works."
|
| 144 |
|
| 145 |
|
| 146 |
def initial_mobile_state(
|
dream_customs/ui/app.py
CHANGED
|
@@ -86,7 +86,7 @@ VOICE_JS = r"""
|
|
| 86 |
button.addEventListener("click", async () => {
|
| 87 |
const Recognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
| 88 |
if (!Recognition) {
|
| 89 |
-
setStatus("
|
| 90 |
textarea.focus();
|
| 91 |
return;
|
| 92 |
}
|
|
@@ -97,12 +97,12 @@ VOICE_JS = r"""
|
|
| 97 |
stream.getTracks().forEach((track) => track.stop());
|
| 98 |
}
|
| 99 |
} catch (error) {
|
| 100 |
-
setStatus("
|
| 101 |
return;
|
| 102 |
}
|
| 103 |
|
| 104 |
const recognition = new Recognition();
|
| 105 |
-
recognition.lang = "
|
| 106 |
recognition.interimResults = true;
|
| 107 |
recognition.continuous = false;
|
| 108 |
recognition.maxAlternatives = 1;
|
|
@@ -110,7 +110,7 @@ VOICE_JS = r"""
|
|
| 110 |
let latestTranscript = "";
|
| 111 |
|
| 112 |
recognition.onstart = () => {
|
| 113 |
-
setStatus("
|
| 114 |
};
|
| 115 |
|
| 116 |
recognition.onresult = (event) => {
|
|
@@ -119,23 +119,23 @@ VOICE_JS = r"""
|
|
| 119 |
.join("")
|
| 120 |
.trim();
|
| 121 |
if (latestTranscript) {
|
| 122 |
-
setStatus(`
|
| 123 |
}
|
| 124 |
};
|
| 125 |
|
| 126 |
recognition.onerror = (event) => {
|
| 127 |
const message = event.error === "not-allowed"
|
| 128 |
-
? "
|
| 129 |
-
: "
|
| 130 |
setStatus(message, "error");
|
| 131 |
};
|
| 132 |
|
| 133 |
recognition.onend = () => {
|
| 134 |
if (latestTranscript) {
|
| 135 |
appendTranscript(latestTranscript);
|
| 136 |
-
setStatus("
|
| 137 |
} else if (button.dataset.mode === "listening") {
|
| 138 |
-
setStatus("
|
| 139 |
}
|
| 140 |
};
|
| 141 |
|
|
@@ -154,7 +154,7 @@ def _load_view(view_json: str) -> dict:
|
|
| 154 |
try:
|
| 155 |
return json.loads(view_json or "{}")
|
| 156 |
except json.JSONDecodeError:
|
| 157 |
-
return {"status": "error", "error": "
|
| 158 |
|
| 159 |
|
| 160 |
def _notice_html(view: dict) -> str:
|
|
@@ -166,17 +166,17 @@ def _notice_html(view: dict) -> str:
|
|
| 166 |
def _question_markdown(view: dict) -> str:
|
| 167 |
question = escape(view.get("question") or "")
|
| 168 |
optional_question = (
|
| 169 |
-
f"<p class='dc-question-original'><span>
|
| 170 |
if question
|
| 171 |
else ""
|
| 172 |
)
|
| 173 |
return f"""
|
| 174 |
<div class="dc-question-card">
|
| 175 |
<span class="dc-question-kicker">Optional check-in</span>
|
| 176 |
-
<h2>
|
| 177 |
-
<p>
|
| 178 |
{optional_question}
|
| 179 |
-
<p class="dc-question-note">
|
| 180 |
</div>
|
| 181 |
""".strip()
|
| 182 |
|
|
@@ -299,7 +299,7 @@ def build_demo() -> gr.Blocks:
|
|
| 299 |
initial_state, initial_view = initial_mobile_state()
|
| 300 |
initial = _load_view(initial_view)
|
| 301 |
|
| 302 |
-
with gr.Blocks(css=CSS, js=VOICE_JS, title="
|
| 303 |
session_state = gr.State(initial_state)
|
| 304 |
view_state = gr.State(initial_view)
|
| 305 |
|
|
@@ -346,7 +346,7 @@ def build_demo() -> gr.Blocks:
|
|
| 346 |
""".strip()
|
| 347 |
)
|
| 348 |
dream_text = gr.Textbox(
|
| 349 |
-
label="
|
| 350 |
placeholder=DREAM_PLACEHOLDER,
|
| 351 |
lines=12,
|
| 352 |
value="",
|
|
@@ -355,10 +355,10 @@ def build_demo() -> gr.Blocks:
|
|
| 355 |
gr.HTML(
|
| 356 |
"""
|
| 357 |
<div class="dc-mic-control">
|
| 358 |
-
<button type="button" class="dc-mic-button" aria-label="
|
| 359 |
<span class="dc-mic-glyph" aria-hidden="true"></span>
|
| 360 |
</button>
|
| 361 |
-
<div class="dc-mic-status" aria-live="polite">
|
| 362 |
</div>
|
| 363 |
""".strip()
|
| 364 |
)
|
|
@@ -369,32 +369,32 @@ def build_demo() -> gr.Blocks:
|
|
| 369 |
""".strip()
|
| 370 |
)
|
| 371 |
with gr.Row(elem_classes=["dc-submit-row"]):
|
| 372 |
-
example_button = gr.Button("
|
| 373 |
-
submit_button = gr.Button("
|
| 374 |
-
with gr.Accordion("
|
| 375 |
-
image_input = gr.Image(label="
|
| 376 |
|
| 377 |
with gr.Group(visible=False, elem_classes=["dc-stage", "dc-question"]) as question_group:
|
| 378 |
question_markdown = gr.HTML(_question_markdown(initial))
|
| 379 |
answer_text = gr.Textbox(
|
| 380 |
-
label="
|
| 381 |
placeholder=ANSWER_PLACEHOLDER,
|
| 382 |
lines=4,
|
| 383 |
value="",
|
| 384 |
)
|
| 385 |
with gr.Row(elem_classes=["dc-question-actions"]):
|
| 386 |
-
answer_button = gr.Button("
|
| 387 |
-
skip_button = gr.Button("
|
| 388 |
|
| 389 |
with gr.Group(visible=False, elem_classes=["dc-stage", "dc-card"]) as card_group:
|
| 390 |
card_html = gr.HTML("")
|
| 391 |
with gr.Row(elem_classes=["dc-actions"]):
|
| 392 |
-
gentle_button = gr.Button("
|
| 393 |
-
weird_button = gr.Button("
|
| 394 |
-
copy_button = gr.Button("
|
| 395 |
-
reset_button = gr.Button("
|
| 396 |
card_text = gr.Textbox(
|
| 397 |
-
label="
|
| 398 |
value="",
|
| 399 |
lines=8,
|
| 400 |
show_copy_button=True,
|
|
@@ -410,7 +410,7 @@ def build_demo() -> gr.Blocks:
|
|
| 410 |
</div>
|
| 411 |
""".strip()
|
| 412 |
)
|
| 413 |
-
mood = gr.Dropdown(label="
|
| 414 |
gr.HTML(
|
| 415 |
"""
|
| 416 |
<div class="dc-side-stamp">
|
|
@@ -420,112 +420,112 @@ def build_demo() -> gr.Blocks:
|
|
| 420 |
</div>
|
| 421 |
""".strip()
|
| 422 |
)
|
| 423 |
-
with gr.Accordion("
|
| 424 |
gr.HTML(
|
| 425 |
"""
|
| 426 |
<div class="dc-dev-help">
|
| 427 |
-
<strong>
|
| 428 |
-
<span>
|
| 429 |
</div>
|
| 430 |
""".strip()
|
| 431 |
)
|
| 432 |
text_backend = gr.Dropdown(
|
| 433 |
-
label="
|
| 434 |
choices=[
|
| 435 |
-
("
|
| 436 |
-
("
|
| 437 |
-
("Modal/API
|
| 438 |
-
("
|
| 439 |
],
|
| 440 |
value=DEFAULT_TEXT_BACKEND,
|
| 441 |
)
|
| 442 |
vision_backend = gr.Dropdown(
|
| 443 |
-
label="
|
| 444 |
choices=[
|
| 445 |
-
("
|
| 446 |
-
("
|
| 447 |
-
("Modal/API
|
| 448 |
-
("
|
| 449 |
],
|
| 450 |
value=DEFAULT_VISION_BACKEND,
|
| 451 |
)
|
| 452 |
asr_backend = gr.Dropdown(
|
| 453 |
-
label="
|
| 454 |
choices=[
|
| 455 |
-
("
|
| 456 |
-
("Modal ASR
|
| 457 |
-
("Hugging Face ASR
|
| 458 |
],
|
| 459 |
value="demo",
|
| 460 |
)
|
| 461 |
-
with gr.Accordion("
|
| 462 |
-
text_endpoint = gr.Textbox(label="
|
| 463 |
-
vision_endpoint = gr.Textbox(label="
|
| 464 |
asr_endpoint = gr.Textbox(label="ASR Endpoint", value="")
|
| 465 |
hosted_token = gr.Textbox(label="Hosted Token", value="", type="password")
|
| 466 |
-
text_model = gr.Textbox(label="
|
| 467 |
-
vision_model = gr.Textbox(label="
|
| 468 |
ollama_url = gr.Textbox(label="Ollama URL", value="http://localhost:11434")
|
| 469 |
text_timeout_seconds = gr.Number(
|
| 470 |
-
label="
|
| 471 |
value=DEFAULT_HOSTED_TIMEOUT_SECONDS,
|
| 472 |
precision=1,
|
| 473 |
)
|
| 474 |
vision_timeout_seconds = gr.Number(
|
| 475 |
-
label="
|
| 476 |
value=DEFAULT_HOSTED_TIMEOUT_SECONDS,
|
| 477 |
precision=1,
|
| 478 |
)
|
| 479 |
asr_timeout_seconds = gr.Number(
|
| 480 |
-
label="ASR
|
| 481 |
value=DEFAULT_ASR_TIMEOUT_SECONDS,
|
| 482 |
precision=1,
|
| 483 |
)
|
| 484 |
text_latency_budget_ms = gr.Number(
|
| 485 |
-
label="Modal
|
| 486 |
value=DEFAULT_TEXT_LATENCY_BUDGET_MS,
|
| 487 |
precision=0,
|
| 488 |
)
|
| 489 |
vision_latency_budget_ms = gr.Number(
|
| 490 |
-
label="Modal
|
| 491 |
value=DEFAULT_VISION_LATENCY_BUDGET_MS,
|
| 492 |
precision=0,
|
| 493 |
)
|
| 494 |
asr_latency_budget_ms = gr.Number(
|
| 495 |
-
label="ASR
|
| 496 |
value=DEFAULT_ASR_LATENCY_BUDGET_MS,
|
| 497 |
precision=0,
|
| 498 |
)
|
| 499 |
text_temperature = gr.Slider(
|
| 500 |
-
label="
|
| 501 |
minimum=0,
|
| 502 |
maximum=0.7,
|
| 503 |
step=0.05,
|
| 504 |
value=DEFAULT_TEXT_TEMPERATURE,
|
| 505 |
)
|
| 506 |
vision_temperature = gr.Slider(
|
| 507 |
-
label="
|
| 508 |
minimum=0,
|
| 509 |
maximum=0.7,
|
| 510 |
step=0.05,
|
| 511 |
value=DEFAULT_VISION_TEMPERATURE,
|
| 512 |
)
|
| 513 |
text_max_tokens = gr.Slider(
|
| 514 |
-
label="
|
| 515 |
minimum=64,
|
| 516 |
maximum=1200,
|
| 517 |
step=1,
|
| 518 |
value=DEFAULT_TEXT_MAX_TOKENS,
|
| 519 |
)
|
| 520 |
vision_max_tokens = gr.Slider(
|
| 521 |
-
label="
|
| 522 |
minimum=64,
|
| 523 |
maximum=800,
|
| 524 |
step=1,
|
| 525 |
value=DEFAULT_VISION_MAX_TOKENS,
|
| 526 |
)
|
| 527 |
debug_json = gr.Code(
|
| 528 |
-
label="
|
| 529 |
value=json.dumps(initial.get("debug", {}), ensure_ascii=False, indent=2),
|
| 530 |
language="json",
|
| 531 |
visible=False,
|
|
@@ -581,12 +581,12 @@ def build_demo() -> gr.Blocks:
|
|
| 581 |
)
|
| 582 |
gentle_button.click(
|
| 583 |
_revise,
|
| 584 |
-
inputs=[session_state, gr.State("
|
| 585 |
outputs=outputs,
|
| 586 |
)
|
| 587 |
weird_button.click(
|
| 588 |
_revise,
|
| 589 |
-
inputs=[session_state, gr.State("
|
| 590 |
outputs=outputs,
|
| 591 |
)
|
| 592 |
copy_button.click(lambda text: text, inputs=card_text, outputs=card_text)
|
|
|
|
| 86 |
button.addEventListener("click", async () => {
|
| 87 |
const Recognition = window.SpeechRecognition || window.webkitSpeechRecognition;
|
| 88 |
if (!Recognition) {
|
| 89 |
+
setStatus("This browser cannot transcribe speech directly yet. You can still type the dream fragment.", "error");
|
| 90 |
textarea.focus();
|
| 91 |
return;
|
| 92 |
}
|
|
|
|
| 97 |
stream.getTracks().forEach((track) => track.stop());
|
| 98 |
}
|
| 99 |
} catch (error) {
|
| 100 |
+
setStatus("Microphone permission was not granted. Allow recording in the browser, then try again.", "error");
|
| 101 |
return;
|
| 102 |
}
|
| 103 |
|
| 104 |
const recognition = new Recognition();
|
| 105 |
+
recognition.lang = "en-US";
|
| 106 |
recognition.interimResults = true;
|
| 107 |
recognition.continuous = false;
|
| 108 |
recognition.maxAlternatives = 1;
|
|
|
|
| 110 |
let latestTranscript = "";
|
| 111 |
|
| 112 |
recognition.onstart = () => {
|
| 113 |
+
setStatus("Listening. Speak the dream fragment when you are ready.", "listening");
|
| 114 |
};
|
| 115 |
|
| 116 |
recognition.onresult = (event) => {
|
|
|
|
| 119 |
.join("")
|
| 120 |
.trim();
|
| 121 |
if (latestTranscript) {
|
| 122 |
+
setStatus(`Recording: ${latestTranscript}`, "listening");
|
| 123 |
}
|
| 124 |
};
|
| 125 |
|
| 126 |
recognition.onerror = (event) => {
|
| 127 |
const message = event.error === "not-allowed"
|
| 128 |
+
? "Microphone permission was denied. Allow recording, then try again."
|
| 129 |
+
: "I did not catch that clearly. Tap the mic once more if you want to retry.";
|
| 130 |
setStatus(message, "error");
|
| 131 |
};
|
| 132 |
|
| 133 |
recognition.onend = () => {
|
| 134 |
if (latestTranscript) {
|
| 135 |
appendTranscript(latestTranscript);
|
| 136 |
+
setStatus("Added to the dream fragment.", "done");
|
| 137 |
} else if (button.dataset.mode === "listening") {
|
| 138 |
+
setStatus("No speech detected. Tap the mic once more if you want to retry.", "idle");
|
| 139 |
}
|
| 140 |
};
|
| 141 |
|
|
|
|
| 154 |
try:
|
| 155 |
return json.loads(view_json or "{}")
|
| 156 |
except json.JSONDecodeError:
|
| 157 |
+
return {"status": "error", "error": "The interface state could not be read. Please start a new declaration."}
|
| 158 |
|
| 159 |
|
| 160 |
def _notice_html(view: dict) -> str:
|
|
|
|
| 166 |
def _question_markdown(view: dict) -> str:
|
| 167 |
question = escape(view.get("question") or "")
|
| 168 |
optional_question = (
|
| 169 |
+
f"<p class='dc-question-original'><span>Optional question</span>{question}</p>"
|
| 170 |
if question
|
| 171 |
else ""
|
| 172 |
)
|
| 173 |
return f"""
|
| 174 |
<div class="dc-question-card">
|
| 175 |
<span class="dc-question-kicker">Optional check-in</span>
|
| 176 |
+
<h2>Anything worth adding?</h2>
|
| 177 |
+
<p>If it helps, name one real-world thing you want to make easier today, or describe how the dream left your body feeling.</p>
|
| 178 |
{optional_question}
|
| 179 |
+
<p class="dc-question-note">This step is optional. You can skip it and create the pass now.</p>
|
| 180 |
</div>
|
| 181 |
""".strip()
|
| 182 |
|
|
|
|
| 299 |
initial_state, initial_view = initial_mobile_state()
|
| 300 |
initial = _load_view(initial_view)
|
| 301 |
|
| 302 |
+
with gr.Blocks(css=CSS, js=VOICE_JS, title="Dream Customs") as demo:
|
| 303 |
session_state = gr.State(initial_state)
|
| 304 |
view_state = gr.State(initial_view)
|
| 305 |
|
|
|
|
| 346 |
""".strip()
|
| 347 |
)
|
| 348 |
dream_text = gr.Textbox(
|
| 349 |
+
label="Dream declaration",
|
| 350 |
placeholder=DREAM_PLACEHOLDER,
|
| 351 |
lines=12,
|
| 352 |
value="",
|
|
|
|
| 355 |
gr.HTML(
|
| 356 |
"""
|
| 357 |
<div class="dc-mic-control">
|
| 358 |
+
<button type="button" class="dc-mic-button" aria-label="Tap the microphone to dictate">
|
| 359 |
<span class="dc-mic-glyph" aria-hidden="true"></span>
|
| 360 |
</button>
|
| 361 |
+
<div class="dc-mic-status" aria-live="polite">Tap the microphone to dictate</div>
|
| 362 |
</div>
|
| 363 |
""".strip()
|
| 364 |
)
|
|
|
|
| 369 |
""".strip()
|
| 370 |
)
|
| 371 |
with gr.Row(elem_classes=["dc-submit-row"]):
|
| 372 |
+
example_button = gr.Button("Try the sample →", variant="secondary")
|
| 373 |
+
submit_button = gr.Button("Issue today's pass →", variant="primary")
|
| 374 |
+
with gr.Accordion("Additional material", open=False, elem_classes=["dc-attachment-drawer"]):
|
| 375 |
+
image_input = gr.Image(label="Image clue", type="filepath", height=160)
|
| 376 |
|
| 377 |
with gr.Group(visible=False, elem_classes=["dc-stage", "dc-question"]) as question_group:
|
| 378 |
question_markdown = gr.HTML(_question_markdown(initial))
|
| 379 |
answer_text = gr.Textbox(
|
| 380 |
+
label="Your optional note",
|
| 381 |
placeholder=ANSWER_PLACEHOLDER,
|
| 382 |
lines=4,
|
| 383 |
value="",
|
| 384 |
)
|
| 385 |
with gr.Row(elem_classes=["dc-question-actions"]):
|
| 386 |
+
answer_button = gr.Button("Use this note", variant="primary")
|
| 387 |
+
skip_button = gr.Button("Skip and create pass", variant="secondary")
|
| 388 |
|
| 389 |
with gr.Group(visible=False, elem_classes=["dc-stage", "dc-card"]) as card_group:
|
| 390 |
card_html = gr.HTML("")
|
| 391 |
with gr.Row(elem_classes=["dc-actions"]):
|
| 392 |
+
gentle_button = gr.Button("Softer", variant="secondary")
|
| 393 |
+
weird_button = gr.Button("Stranger", variant="secondary")
|
| 394 |
+
copy_button = gr.Button("Copy text", variant="secondary")
|
| 395 |
+
reset_button = gr.Button("New declaration", variant="secondary")
|
| 396 |
card_text = gr.Textbox(
|
| 397 |
+
label="Copy-ready text",
|
| 398 |
value="",
|
| 399 |
lines=8,
|
| 400 |
show_copy_button=True,
|
|
|
|
| 410 |
</div>
|
| 411 |
""".strip()
|
| 412 |
)
|
| 413 |
+
mood = gr.Dropdown(label="After-waking feeling", choices=MOOD_OPTIONS, value=DEFAULT_MOOD)
|
| 414 |
gr.HTML(
|
| 415 |
"""
|
| 416 |
<div class="dc-side-stamp">
|
|
|
|
| 420 |
</div>
|
| 421 |
""".strip()
|
| 422 |
)
|
| 423 |
+
with gr.Accordion("Runtime settings", open=True, elem_classes=["dc-dev"]):
|
| 424 |
gr.HTML(
|
| 425 |
"""
|
| 426 |
<div class="dc-dev-help">
|
| 427 |
+
<strong>For debugging only. Most people can leave this alone.</strong>
|
| 428 |
+
<span>Auto mode uses the backend configured for this Space, then safely falls back to demo data when no endpoint is available.</span>
|
| 429 |
</div>
|
| 430 |
""".strip()
|
| 431 |
)
|
| 432 |
text_backend = gr.Dropdown(
|
| 433 |
+
label="Text generation",
|
| 434 |
choices=[
|
| 435 |
+
("Auto: configured Space model", "model"),
|
| 436 |
+
("Demo: stable sample data", "demo"),
|
| 437 |
+
("Modal/API: private endpoint", "modal"),
|
| 438 |
+
("Local Ollama", "ollama"),
|
| 439 |
],
|
| 440 |
value=DEFAULT_TEXT_BACKEND,
|
| 441 |
)
|
| 442 |
vision_backend = gr.Dropdown(
|
| 443 |
+
label="Image understanding",
|
| 444 |
choices=[
|
| 445 |
+
("Auto: configured vision model", "model"),
|
| 446 |
+
("Demo: skip image model", "demo"),
|
| 447 |
+
("Modal/API: private endpoint", "modal"),
|
| 448 |
+
("Local Ollama", "ollama"),
|
| 449 |
],
|
| 450 |
value=DEFAULT_VISION_BACKEND,
|
| 451 |
)
|
| 452 |
asr_backend = gr.Dropdown(
|
| 453 |
+
label="Voice input",
|
| 454 |
choices=[
|
| 455 |
+
("Browser dictation now", "demo"),
|
| 456 |
+
("Modal ASR endpoint, planned", "modal"),
|
| 457 |
+
("Hugging Face ASR endpoint, planned", "huggingface"),
|
| 458 |
],
|
| 459 |
value="demo",
|
| 460 |
)
|
| 461 |
+
with gr.Accordion("Advanced endpoints", open=False, elem_classes=["dc-dev-advanced"]):
|
| 462 |
+
text_endpoint = gr.Textbox(label="Text endpoint", value="")
|
| 463 |
+
vision_endpoint = gr.Textbox(label="Image endpoint", value="")
|
| 464 |
asr_endpoint = gr.Textbox(label="ASR Endpoint", value="")
|
| 465 |
hosted_token = gr.Textbox(label="Hosted Token", value="", type="password")
|
| 466 |
+
text_model = gr.Textbox(label="Text model", value=DEFAULT_TEXT_MODEL)
|
| 467 |
+
vision_model = gr.Textbox(label="Image model", value=DEFAULT_VISION_MODEL)
|
| 468 |
ollama_url = gr.Textbox(label="Ollama URL", value="http://localhost:11434")
|
| 469 |
text_timeout_seconds = gr.Number(
|
| 470 |
+
label="Text timeout, seconds",
|
| 471 |
value=DEFAULT_HOSTED_TIMEOUT_SECONDS,
|
| 472 |
precision=1,
|
| 473 |
)
|
| 474 |
vision_timeout_seconds = gr.Number(
|
| 475 |
+
label="Image timeout, seconds",
|
| 476 |
value=DEFAULT_HOSTED_TIMEOUT_SECONDS,
|
| 477 |
precision=1,
|
| 478 |
)
|
| 479 |
asr_timeout_seconds = gr.Number(
|
| 480 |
+
label="ASR timeout, seconds",
|
| 481 |
value=DEFAULT_ASR_TIMEOUT_SECONDS,
|
| 482 |
precision=1,
|
| 483 |
)
|
| 484 |
text_latency_budget_ms = gr.Number(
|
| 485 |
+
label="Modal text latency budget, ms",
|
| 486 |
value=DEFAULT_TEXT_LATENCY_BUDGET_MS,
|
| 487 |
precision=0,
|
| 488 |
)
|
| 489 |
vision_latency_budget_ms = gr.Number(
|
| 490 |
+
label="Modal image latency budget, ms",
|
| 491 |
value=DEFAULT_VISION_LATENCY_BUDGET_MS,
|
| 492 |
precision=0,
|
| 493 |
)
|
| 494 |
asr_latency_budget_ms = gr.Number(
|
| 495 |
+
label="ASR latency budget, ms",
|
| 496 |
value=DEFAULT_ASR_LATENCY_BUDGET_MS,
|
| 497 |
precision=0,
|
| 498 |
)
|
| 499 |
text_temperature = gr.Slider(
|
| 500 |
+
label="Text temperature",
|
| 501 |
minimum=0,
|
| 502 |
maximum=0.7,
|
| 503 |
step=0.05,
|
| 504 |
value=DEFAULT_TEXT_TEMPERATURE,
|
| 505 |
)
|
| 506 |
vision_temperature = gr.Slider(
|
| 507 |
+
label="Image temperature",
|
| 508 |
minimum=0,
|
| 509 |
maximum=0.7,
|
| 510 |
step=0.05,
|
| 511 |
value=DEFAULT_VISION_TEMPERATURE,
|
| 512 |
)
|
| 513 |
text_max_tokens = gr.Slider(
|
| 514 |
+
label="Text max tokens",
|
| 515 |
minimum=64,
|
| 516 |
maximum=1200,
|
| 517 |
step=1,
|
| 518 |
value=DEFAULT_TEXT_MAX_TOKENS,
|
| 519 |
)
|
| 520 |
vision_max_tokens = gr.Slider(
|
| 521 |
+
label="Image max tokens",
|
| 522 |
minimum=64,
|
| 523 |
maximum=800,
|
| 524 |
step=1,
|
| 525 |
value=DEFAULT_VISION_MAX_TOKENS,
|
| 526 |
)
|
| 527 |
debug_json = gr.Code(
|
| 528 |
+
label="Current state",
|
| 529 |
value=json.dumps(initial.get("debug", {}), ensure_ascii=False, indent=2),
|
| 530 |
language="json",
|
| 531 |
visible=False,
|
|
|
|
| 581 |
)
|
| 582 |
gentle_button.click(
|
| 583 |
_revise,
|
| 584 |
+
inputs=[session_state, gr.State("softer"), text_backend, vision_backend] + settings_inputs,
|
| 585 |
outputs=outputs,
|
| 586 |
)
|
| 587 |
weird_button.click(
|
| 588 |
_revise,
|
| 589 |
+
inputs=[session_state, gr.State("stranger"), text_backend, vision_backend] + settings_inputs,
|
| 590 |
outputs=outputs,
|
| 591 |
)
|
| 592 |
copy_button.click(lambda text: text, inputs=card_text, outputs=card_text)
|
dream_customs/ui/copy.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
| 1 |
-
APP_TITLE = "
|
| 2 |
-
APP_SUBTITLE = "
|
| 3 |
|
| 4 |
-
DREAM_PLACEHOLDER = "
|
| 5 |
-
ANSWER_PLACEHOLDER = "
|
| 6 |
|
| 7 |
-
EXAMPLE_DREAM = "
|
| 8 |
-
EXAMPLE_MOOD = "
|
| 9 |
|
| 10 |
-
DEFAULT_MOOD = "
|
| 11 |
-
MOOD_OPTIONS = ["
|
|
|
|
| 1 |
+
APP_TITLE = "Dream Customs"
|
| 2 |
+
APP_SUBTITLE = "Turn last night's strange fragment into one gentle clearance pass for today."
|
| 3 |
|
| 4 |
+
DREAM_PLACEHOLDER = "For example: I kept missing an elevator, and the buttons melted like wax."
|
| 5 |
+
ANSWER_PLACEHOLDER = "Add one sentence if it helps, or leave this blank and skip ahead."
|
| 6 |
|
| 7 |
+
EXAMPLE_DREAM = "I kept missing an elevator. The buttons melted like wax, and the floor number froze at 14."
|
| 8 |
+
EXAMPLE_MOOD = "Uneasy"
|
| 9 |
|
| 10 |
+
DEFAULT_MOOD = "Calm"
|
| 11 |
+
MOOD_OPTIONS = ["Foggy", "Uneasy", "Curious", "Tired", "Calm"]
|
dream_customs/ui/styles.py
CHANGED
|
@@ -804,6 +804,11 @@ button.secondary {
|
|
| 804 |
min-height: 130px !important;
|
| 805 |
}
|
| 806 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 807 |
.dc-dev {
|
| 808 |
background: rgba(255, 249, 238, 0.7) !important;
|
| 809 |
border: 1px solid rgba(189, 168, 143, 0.58) !important;
|
|
|
|
| 804 |
min-height: 130px !important;
|
| 805 |
}
|
| 806 |
|
| 807 |
+
footer.svelte-1rjryqp,
|
| 808 |
+
a.built-with[href*="gradio.app"] {
|
| 809 |
+
display: none !important;
|
| 810 |
+
}
|
| 811 |
+
|
| 812 |
.dc-dev {
|
| 813 |
background: rgba(255, 249, 238, 0.7) !important;
|
| 814 |
border: 1px solid rgba(189, 168, 143, 0.58) !important;
|
tests/test_app_logic.py
CHANGED
|
@@ -17,16 +17,16 @@ from dream_customs.models import HostedASRClient, HostedMiniCPMTextClient, Hoste
|
|
| 17 |
|
| 18 |
def test_run_customs_once_generates_demo_outputs():
|
| 19 |
negotiation, pact_text, html, debug_json = run_customs_once(
|
| 20 |
-
dream_text="
|
| 21 |
mood="foggy",
|
| 22 |
-
answers="
|
| 23 |
)
|
| 24 |
debug = json.loads(debug_json)
|
| 25 |
assert "Visitor:" in negotiation
|
| 26 |
assert "Dream visitor:" in pact_text
|
| 27 |
assert "Today's Pact" in html
|
| 28 |
assert debug["status"] == "ok"
|
| 29 |
-
assert debug["intake"]["dream_text"] == "
|
| 30 |
|
| 31 |
|
| 32 |
def test_run_customs_once_requires_one_modality():
|
|
|
|
| 17 |
|
| 18 |
def test_run_customs_once_generates_demo_outputs():
|
| 19 |
negotiation, pact_text, html, debug_json = run_customs_once(
|
| 20 |
+
dream_text="I dreamed of a late elevator.",
|
| 21 |
mood="foggy",
|
| 22 |
+
answers="I want to start one small thing first.",
|
| 23 |
)
|
| 24 |
debug = json.loads(debug_json)
|
| 25 |
assert "Visitor:" in negotiation
|
| 26 |
assert "Dream visitor:" in pact_text
|
| 27 |
assert "Today's Pact" in html
|
| 28 |
assert debug["status"] == "ok"
|
| 29 |
+
assert debug["intake"]["dream_text"] == "I dreamed of a late elevator."
|
| 30 |
|
| 31 |
|
| 32 |
def test_run_customs_once_requires_one_modality():
|
tests/test_pipeline.py
CHANGED
|
@@ -88,8 +88,8 @@ def test_generate_pact_polishes_unclear_model_output_into_daily_tip():
|
|
| 88 |
intake = build_intake(dream_text="我梦见电梯按钮融化,数字停在 14。", mood="焦虑")
|
| 89 |
card, html = generate_pact(intake, "", UnclearTextClient())
|
| 90 |
|
| 91 |
-
assert card.visitor_name == "
|
| 92 |
-
assert "
|
| 93 |
assert "电梯运行" not in card.practical_suggestion
|
| 94 |
assert card.safety_note == ""
|
| 95 |
assert "Life tip" in html
|
|
@@ -98,7 +98,7 @@ def test_generate_pact_polishes_unclear_model_output_into_daily_tip():
|
|
| 98 |
def test_add_evidence_updates_session_with_text_image_audio_and_mood():
|
| 99 |
session = add_evidence(
|
| 100 |
create_session(),
|
| 101 |
-
dream_text="
|
| 102 |
image_path="demo.png",
|
| 103 |
audio_path="demo.wav",
|
| 104 |
mood="anxious",
|
|
@@ -106,7 +106,7 @@ def test_add_evidence_updates_session_with_text_image_audio_and_mood():
|
|
| 106 |
asr_client=FakeASRClient(),
|
| 107 |
)
|
| 108 |
assert session.phase == "declaring"
|
| 109 |
-
assert "
|
| 110 |
assert "blue hallway" in session.intake.visual_clues
|
| 111 |
assert "The buttons melted" in session.intake.voice_transcript
|
| 112 |
assert session.intake.mood == "anxious"
|
|
|
|
| 88 |
intake = build_intake(dream_text="我梦见电梯按钮融化,数字停在 14。", mood="焦虑")
|
| 89 |
card, html = generate_pact(intake, "", UnclearTextClient())
|
| 90 |
|
| 91 |
+
assert card.visitor_name == "Night Visitor"
|
| 92 |
+
assert "drink water" in card.practical_suggestion
|
| 93 |
assert "电梯运行" not in card.practical_suggestion
|
| 94 |
assert card.safety_note == ""
|
| 95 |
assert "Life tip" in html
|
|
|
|
| 98 |
def test_add_evidence_updates_session_with_text_image_audio_and_mood():
|
| 99 |
session = add_evidence(
|
| 100 |
create_session(),
|
| 101 |
+
dream_text="I dreamed the elevator buttons melted.",
|
| 102 |
image_path="demo.png",
|
| 103 |
audio_path="demo.wav",
|
| 104 |
mood="anxious",
|
|
|
|
| 106 |
asr_client=FakeASRClient(),
|
| 107 |
)
|
| 108 |
assert session.phase == "declaring"
|
| 109 |
+
assert "elevator" in session.intake.dream_text
|
| 110 |
assert "blue hallway" in session.intake.visual_clues
|
| 111 |
assert "The buttons melted" in session.intake.voice_transcript
|
| 112 |
assert session.intake.mood == "anxious"
|
tests/test_ui_actions.py
CHANGED
|
@@ -48,8 +48,8 @@ def test_zerogpu_probe_is_importable_without_local_gpu():
|
|
| 48 |
|
| 49 |
def test_mobile_mvp_submit_then_skip_auto_seals_pact():
|
| 50 |
state, view_json = submit_dream_action(
|
| 51 |
-
dream_text="
|
| 52 |
-
mood="
|
| 53 |
text_backend="demo",
|
| 54 |
vision_backend="demo",
|
| 55 |
)
|
|
@@ -65,23 +65,28 @@ def test_mobile_mvp_submit_then_skip_auto_seals_pact():
|
|
| 65 |
|
| 66 |
assert view["status"] == "card"
|
| 67 |
assert view["phase"] == "sealed"
|
| 68 |
-
assert "
|
| 69 |
assert f"DREAM{date.today():%Y%m%d}-014" in view["card_text"]
|
| 70 |
assert "DC-DEMO-014" not in view["card_text"]
|
| 71 |
-
assert "
|
| 72 |
|
| 73 |
|
| 74 |
def test_mobile_mvp_answer_to_card_auto_seals_pact():
|
| 75 |
state, _view_json = submit_dream_action(
|
| 76 |
-
dream_text="
|
| 77 |
-
mood="
|
| 78 |
text_backend="demo",
|
| 79 |
vision_backend="demo",
|
| 80 |
)
|
| 81 |
|
| 82 |
-
state, view_json = answer_to_card_action(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
view = json.loads(view_json)
|
| 84 |
|
| 85 |
assert view["status"] == "card"
|
| 86 |
assert view["phase"] == "sealed"
|
| 87 |
-
assert "
|
|
|
|
| 48 |
|
| 49 |
def test_mobile_mvp_submit_then_skip_auto_seals_pact():
|
| 50 |
state, view_json = submit_dream_action(
|
| 51 |
+
dream_text="I dreamed of a late elevator.",
|
| 52 |
+
mood="Uneasy",
|
| 53 |
text_backend="demo",
|
| 54 |
vision_backend="demo",
|
| 55 |
)
|
|
|
|
| 65 |
|
| 66 |
assert view["status"] == "card"
|
| 67 |
assert view["phase"] == "sealed"
|
| 68 |
+
assert "Today's Clearance Pass" in view["card_title"]
|
| 69 |
assert f"DREAM{date.today():%Y%m%d}-014" in view["card_text"]
|
| 70 |
assert "DC-DEMO-014" not in view["card_text"]
|
| 71 |
+
assert "Late Elevator" in view["card_html"]
|
| 72 |
|
| 73 |
|
| 74 |
def test_mobile_mvp_answer_to_card_auto_seals_pact():
|
| 75 |
state, _view_json = submit_dream_action(
|
| 76 |
+
dream_text="I dreamed the elevator buttons melted and the elevator never came.",
|
| 77 |
+
mood="Foggy",
|
| 78 |
text_backend="demo",
|
| 79 |
vision_backend="demo",
|
| 80 |
)
|
| 81 |
|
| 82 |
+
state, view_json = answer_to_card_action(
|
| 83 |
+
state,
|
| 84 |
+
"It may be asking me to slow down.",
|
| 85 |
+
text_backend="demo",
|
| 86 |
+
vision_backend="demo",
|
| 87 |
+
)
|
| 88 |
view = json.loads(view_json)
|
| 89 |
|
| 90 |
assert view["status"] == "card"
|
| 91 |
assert view["phase"] == "sealed"
|
| 92 |
+
assert "It may be asking me to slow down." in view["debug"]["session"]["answer_history"]
|