# Project export: Re:Dream

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2026
- Tagline: You know that dream that got interesting right as you woke up — the one you could never get back into? RE-DREAM lets you step back in and finish it.
- Devpost: https://devpost.com/software/re-dream
- GitHub: https://github.com/yjack-coder/re-dream
- Team: 1 GitHub contributor(s) — Claude Opus 4.8 (1M context) (1 commits)

## Devpost submission (written by the team)

No Devpost description available.

## README (from the GitHub repository)

# RE-DREAM — finish the dream you woke up from

> Describe a dream that got cut off, then re-enter a hand-drawn, voice-steered
> dreamscape that continues wherever you tell it — narrated in real time — and keep
> the dream you finally finished.

Built at the **UC Berkeley AI Hackathon 2026**.

## What it is

Everyone has woken a moment too early, pulled out of a dream just as it turned
luminous. Dream recall collapses within minutes of waking — yet dreams matter:
REM sleep is implicated in memory consolidation and emotional regulation, and dream
*content* can carry signal about health (frequent distressing dreams have been linked
to higher risk of cognitive decline and Parkinson's disease — Otaiku, *eClinicalMedicine*, 2022).

RE-DREAM is an interface that takes dreams seriously. You describe the dream that got
cut off, then step back into a quiet dreamscape — a white line-art figure over a
starlit, rippling sea — and **steer it by speaking**. When you wake, the dream gives
you back a **Dream Card**: a title, its image, and a tender interpretation. Keep your
dreams in a floating gallery and revisit them any time.

## How it works

- **Anthropic (Claude)** — the dream engine: continues the narrative in the direction
  you speak, art-directs the figure to draw, and names + interprets the dream on waking.
- **Deepgram** — voice is the whole interface: **Aura** TTS narrates in a British
  voice; **Nova** STT lets you steer the dream by talking to it.
- **Flux / Pollinations** — generated white line-art, screen-blended over a hand-built
  HTML5 Canvas starfield + water-ripple world.
- **Flask** backend orchestrates everything (with a caching image proxy).

## Run it locally

```bash
python3 -m venv venv && ./venv/bin/pip install -r requirements.txt
# create a .env with your keys:
#   ANTHROPIC_API_KEY=...
#   DEEPGRAM_API_KEY=...
./venv/bin/python app.py
# open http://localhost:5050
```

**Controls:** describe your dream (type or 🎤) → hold **SPACE** and say where the
dream goes → **I** to wake → keep your dream → **✦ your dreams** to revisit.

## What's next

Reconstruct dreams from overnight signals — movement, heart rate, sleep-talk — so you
can wake and immediately re-enter the dream you were just having. At scale, durable
dream journals could turn fleeting subjective experience into longitudinal data.


## Detected evidence (automated analysis)

Indexed codebase: 11 recognized source files, 65 KB.
- Anthropic (technology) — detected in the code
- Flask (technology) — detected in the code
- HTML (language) — detected in the code
- Python (language) — detected in the code
- JavaScript (language) — claimed on Devpost, not found in the code
- AI coding agent: Claude Code — evidence: commit authorship or trailers

## Codebase structure (from repository index)

### Files (16 of 16)

```
.DS_Store
.gitignore
app.py
config.py
dream_l1.py
dream_voice.py
dream.py
PITCH.md
README.md
requirements.txt
scene.py
server.log
static/.DS_Store
static/index.html
store.py
voice.py
```

### Dependencies

- requirements.txt: anthropic, flask, python-dotenv, requests

### Recent commits (newest first)

- RE-DREAM — voice-steered dreamscape (Claude + Deepgram)

## Key source files (fetched from GitHub, selected and truncated for size)

### PITCH.md

```markdown
# RE-DREAM — finish the dream you woke up from

> Devpost 提交稿(直接复制)。`# 中文` 是给你的备注,提交时删掉。

## Elevator pitch (Devpost 必填,2-3 句)
You know that dream that got interesting right as you woke up — the one you could
never get back into? RE-DREAM lets you step back in and finish it. Describe the
dream aloud, and a hand-drawn, voice-steered dreamscape continues wherever you tell
it to go — narrated in real time — and hands you back a keepsake of the dream you
finally finished.

## Inspiration
Everyone has woken up right before a dream resolved, and spent the morning aching to
get back into it — knowing you never can. We built the machine that lets you go back
in, and lets the dreamer, not a script, decide where it goes.

## What it does
- You describe the dream that got cut off — by **typing or speaking** it.
- You re-enter a quiet dreamscape: a **white line-art figure** drawn over a blue
  starlit sky and rippling water, narrated aloud by a calm **British voice**, with
  the words fading in and out like a memory.
- There are no menus. You **hold the spacebar and simply say** where the dream
  should go ("I step onto the water and walk toward the voice") — and the dream
  **continues in that direction, in real time.**
- When you're ready, you **wake**, and the dream gives you a **Dream Card**: a title,
  the image of your dream, and a tender interpretation of what your subconscious
  seemed to be working through — read aloud.
- Keep your dreams in a soft, floating **Dream Gallery** and revisit them any time.

## How we built it
- **Anthropic (Claude)** is the dream engine: it continues the narrative in the
  direction you speak, describes the figure to draw, and — when you wake — names the
  dream and interprets it. One model carries the story, the art direction, and the meaning.
- **Deepgram** gives the dream both ears and a voice: **Aura** text-to-speech narrates
  every beat in a British voice; **Nova** speech-to-text lets you *steer the dream by
  talking to it.* Voice is the entire interface.
- The visuals are **generated white line-art** composited over a hand-built canvas
  starfield + water-ripple world using screen blending, so each figure floats as
  light over the dream.
- A lightweight Flask backend orchestrates Claude, Deepgram, image generation (with a
  caching proxy), and the saved-dream gallery.

## Sponsor tracks
- **Anthropic** — Claude as the dream engine: continuation, art direction, naming, interpretation
- **Deepgram** — voice as the whole interface: Aura narration (British) + Nova voice steering

## Challenges we ran into
Making it feel like a dream and not a menu: the whole experience is **steered by
voice in real time**, so the narrative has to bend to whatever the dreamer says.
Getting the **white line-art** to feel premium meant generating art on black and
screen-blending it over a generated starscape. And keeping live image generation
reliable under load pushed us to build a caching image proxy.

## What's next
Reconstruct dre
[truncated — 558 more characters]
```

### requirements.txt

```
flask
anthropic
requests
python-dotenv

```

### app.py

```python
"""
RE-DREAM 3D —— 可走入、可互动的梦境(浏览器 walking-sim)。
后端: Claude 生成每一拍的 3D 场景规格 + Deepgram 念旁白。
跑: ./venv/bin/python app.py  然后浏览器开 http://localhost:5000
"""
import os
import re
import json
import datetime
import hashlib
import urllib.parse
import requests
from flask import Flask, request, jsonify, Response, send_from_directory, send_file

from config import DEEPGRAM_API_KEY, MODEL
from scene import generate_scene, client as claude

app = Flask(__name__, static_folder="static")

# 图片缓存代理:生成一次永久复用,降低被免费 API 限流的概率
IMG_CACHE = os.path.join(os.path.dirname(__file__), "static", "imgcache")
os.makedirs(IMG_CACHE, exist_ok=True)
POLLINATIONS_TOKEN = os.environ.get("POLLINATIONS_TOKEN")  # 可选,有则去限流

# 单人 demo,内存里存一条对话历史就够
history = []


@app.get("/")
def index():
    return send_from_directory("static", "index.html")


@app.get("/api/media")
def media():
    """告诉前端有没有 Midjourney 天空图 / Pika 片头视频(任意常见格式都认)。"""
    import os
    base = os.path.join(os.path.dirname(__file__), "static", "media")
    def find(stem, exts):
        for e in exts:
            if os.path.exists(os.path.join(base, stem + e)):
                return f"/static/media/{stem}{e}"
        return None
    return jsonify({
        "sky":   find("sky",   [".png", ".jpg", ".jpeg", ".webp"]),
        "intro": find("dream", [".mp4", ".webm", ".mov"]),
        "music": find("music", [".mp3", ".m4a", ".ogg", ".wav"]),
    })


@app.get("/api/img")
def img():
    """生成/缓存一张图。缓存命中秒回;未命中向 Pollinations 请求并存盘。"""
    prompt = request.args.get("p", "").strip()
    seed = request.args.get("seed", "0")
    if not prompt:
        return Response(b"", status=400)
    key = hashlib.md5(f"{prompt}|{seed}".encode()).hexdigest()
    path = os.path.join(IMG_CACHE, key + ".jpg")
    if os.path.exists(path):
        return send_file(path, mimetype="image/jpeg")
    url = ("https://image.pollinations.ai/prompt/" + urllib.parse.quote(prompt)
           + f"?width=1024&height=576&nologo=true&model=flux&seed={seed}")
    if POLLINATIONS_TOKEN:
        url += f"&token={POLLINATIONS_TOKEN}"
    for _ in range(3):
        try:
            r = requests.get(url, timeout=50)
            if r.status_code == 200 and len(r.content) > 2000:
                with open(path, "wb") as f:
                    f.write(r.content)
                return Response(r.content, mimetype="image/jpeg")
        except Exception:
            pass
    return Response(b"", status=502)


@app.post("/api/start")
def start():
    dream = (request.json or {}).get("dream", "").strip()
    history.clear()
    history.append({"role": "user", "content":
        f"My dream, which got cut off at the end:\n{dream}\n\n"
        "Rebuild it and let it begin to continue."})
    scene = generate_scene(history)
    history.append({"role": "assistant", "content": json.dumps(scene)})
    return jsonify(scene)


@app.post("/api/choice")
def choice():
    pick = (request.json or {}).get("choice", "").strip()
    history.append({"role": "user", "content":
        f"The dreamer says aloud how the dream continues: {pick}\n"
        "Continue the dream in that direction."})
    scene = generate_scene(history)
    history.append({"role": "assistant", "content": json.dumps(scene)})
    return jsonify(scene)


@app.post("/api/interpret")
def interpret():
    """醒来:回看整条梦,给一段诗意的潜意识解读。"""
    if not history:
        return jsonify({"text": ""})
    msgs = list(history) + [{"role": "user", "content":
        "The dreamer is now waking. Look back over this entire dream they just "
        "walked through — the recurring symbols, how things transformed, the "
        "emotional thread. In 3-4 sentences, second person, offer a tender, poetic "
        "interpretation of what their subconscious seemed to be working through and "
        "what the dream wanted. Be a wise, gentle dream-reader, never clinical. "
        "Output only the interpretation prose, no preamble."}]
    try:
        r = claude.messages.create(
            model=MODEL, max_tokens=400,
            system="You are a tender, poetic interpreter of dreams.",
            messages=msgs)
        return jsonify({"text": r.content[0].text.strip()})
    except Exception as e:
        return jsonify({"text": "", "error": str(e)[:140]})


DREAMS_FILE = os.path.join(os.path.dirname(__file__), "saved_dreams.json")


def _load_dreams():
    try:
        with open(DREAMS_FILE) as f:
            return json.load(f)
    except Exception:
        return []


def _save_dreams(d):
    with open(DREAMS_FILE, "w") as f:
        json.dump(d, f, ensure_ascii=False, indent=2)


@app.post("/api/finale")
def finale():
    """醒来:给这场梦起个名字 + 一段诗意解读(用于梦境卡片)。"""
    if not history:
        return jsonify({"title": "An Untitled Dream", "interpretation": ""})
    msgs = list(history) + [{"role": "user", "content":
        "The dreamer is waking. Look back over the whole dream and return ONLY a JSON "
        "object, nothing else:\n"
        '{ "title": "<a short evocative title for this dream, 2 to 5 words, like a poem title>", '
        '"interpretation": "<3-4 sentences, second person, tender and poetic — what their '
        'subconscious seemed to be working through, and what the dream wanted>" }'}]
    try:
        r = claude.messages.create(
            model=MODEL, max_tokens=450,
            system="You name and interpret dreams, tenderly and poetically.", messages=msgs)
        txt = r.content[0].text
        m = re.search(r"\{.*\}", txt, re.S)
        data = json.loads(m.group(0)) if m else {"title": "A Dream", "interpretation": txt.strip()}
        data.setdefault("title", "A Dream")
        data.setdefault("interpretation", "")
        return jsonify(data)
    except Exception as e:
        return jsonify({"title": "A Dream", "interpretation": "", "error": str(e)[:140]})


@app.get("/api/dreams")
def dreams_list():
    return jsonify(_load_dreams())


@app.post("/api/dreams")
def dreams_save():
    b = request.json or {}
    d = _load_dreams()
    d.insert(0, {
        "title":
[truncated — 2268 more characters]
```

### config.py

```python
"""集中加载 .env 和配置,所有模块共用。"""
import os
from pathlib import Path
from dotenv import load_dotenv

ROOT = Path(__file__).parent
load_dotenv(ROOT / ".env")

ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY")
DEEPGRAM_API_KEY = os.environ.get("DEEPGRAM_API_KEY")
PIKA_API_KEY = os.environ.get("PIKA_API_KEY")

MODEL = "claude-sonnet-4-6"          # 想要更浓的文字可换 claude-opus-4-8
OUT = ROOT / "out"
OUT.mkdir(exist_ok=True)

```

### voice.py

```python
"""Deepgram Aura 文字转语音 —— 让梦开口说话。(替代 ElevenLabs)"""
import subprocess
import requests
from config import DEEPGRAM_API_KEY, OUT

VOICE = "aura-asteria-en"   # Deepgram 的梦呓嗓音(英语)
_n = [0]


def speak(text: str, enabled: bool = True):
    """把一段文字合成语音并立刻播放。失败就静默跳过,绝不让梦中断。"""
    if not enabled or not DEEPGRAM_API_KEY or not text.strip():
        return
    try:
        r = requests.post(
            f"https://api.deepgram.com/v1/speak?model={VOICE}",
            headers={"Authorization": f"Token {DEEPGRAM_API_KEY}",
                     "Content-Type": "application/json"},
            json={"text": text[:1900]},   # Aura 单次上限,梦的一拍够用
            timeout=30,
        )
        if r.status_code == 200:
            _n[0] += 1
            path = OUT / f"beat_{_n[0]:03d}.mp3"
            path.write_bytes(r.content)
            subprocess.run(["afplay", str(path)], check=False)
        else:
            print(f"(语音跳过: HTTP {r.status_code})")
    except Exception as e:
        print(f"(语音跳过: {e})")

```

### scene.py

```python
"""
RE-DREAM 大脑(白线版)。
用户讲一个断梦,然后用语音说出梦该怎么走;每一步把梦往那个方向继续一点。
输出:一段朗读的剧情 + 一个要用"白色线条"画出来的人物/场景。
"""
import json
import re
import anthropic
from config import ANTHROPIC_API_KEY, MODEL

client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)

SYSTEM = """\
You are RE-DREAM. The user gives a dream that was cut off, then SPEAKS aloud how it
should keep going. Each turn you continue the dream a little further in the
direction they described — one flowing, dreamlike moment.

Output ONE JSON object (and nothing else):
{
  "narration": "<2-3 sentences, second person, present tense, poetic and dreamlike; continue the dream in the spoken direction. This is read aloud by a calm British voice, so let it flow.>",
  "subject": "<a short, evocative description of mainly a FIGURE (a person) and a faint hint of setting — this becomes a single white-line drawing. Keep it simple: one figure, a gesture, minimal surroundings.>"
}

Rules:
- Keep the dream moving and emotional; honor what the dreamer just said.
- "subject" should center on a human figure (the dreamer, or someone they meet),
  in a clear pose or gesture — the surroundings stay sparse and suggestive.
- Output ONLY the JSON. No markdown, no commentary.
"""


def _extract_json(text: str) -> dict:
    text = text.strip()
    if text.startswith("```"):
        text = re.sub(r"^```[a-z]*\n?|\n?```$", "", text).strip()
    s, e = text.find("{"), text.rfind("}")
    return json.loads(text[s:e + 1])


def generate_scene(messages: list) -> dict:
    resp = client.messages.create(
        model=MODEL, max_tokens=700, system=SYSTEM, messages=messages,
    )
    scene = _extract_json(resp.content[0].text)
    scene.setdefault("narration", "")
    scene.setdefault("subject", "a lone figure standing still in vast empty space")
    return scene

```

### store.py

```python
"""
梦境树持久化 —— 现在用本地 JSON,接口为 Redis 预留。
以后接 Redis 只需另写一个 RedisStore,save/load 签名不变,主程序一行不用改。
"""
import json
from config import ROOT
from dream_l1 import Node

DREAMS = ROOT / "dreams"
DREAMS.mkdir(exist_ok=True)


def _serialize(root: Node, current_id: int, initial_prompt: str) -> dict:
    nodes = []
    stack = [root]
    while stack:
        n = stack.pop()
        nodes.append({
            "id": n.id,
            "parent": n.parent.id if n.parent else None,
            "choice_in": n.choice_in,
            "text": n.text,
        })
        stack.extend(n.children)
    return {"dream": initial_prompt, "current": current_id, "nodes": nodes}


def _deserialize(data: dict):
    """重建节点树,返回 (nodes_dict, root, current, next_id)。"""
    raw = {n["id"]: n for n in data["nodes"]}
    nodes = {}
    # 先建所有节点(无父链接),再按 parent 连起来,避免顺序问题
    for nid, n in raw.items():
        nodes[nid] = Node(nid, None, n["choice_in"], n["text"])
    root = None
    for nid, n in raw.items():
        pid = n["parent"]
        if pid is None:
            root = nodes[nid]
        else:
            nodes[nid].parent = nodes[pid]
            nodes[pid].children.append(nodes[nid])
    # children 按 id 排序,保证树形稳定
    for n in nodes.values():
        n.children.sort(key=lambda c: c.id)
    next_id = max(raw) + 1 if raw else 1
    current = nodes.get(data.get("current"), root)
    return nodes, root, current, next_id


class JsonStore:
    """本地 JSON 后端。Redis 版照抄这三个方法即可。"""

    def save(self, name: str, root: Node, current_id: int, initial_prompt: str):
        path = DREAMS / f"{name}.json"
        path.write_text(json.dumps(
            _serialize(root, current_id, initial_prompt), ensure_ascii=False, indent=2))
        return str(path)

    def load(self, name: str):
        path = DREAMS / f"{name}.json"
        if not path.exists():
            return None
        data = json.loads(path.read_text())
        return data["dream"], _deserialize(data)

    def list(self):
        return sorted(p.stem for p in DREAMS.glob("*.json"))


store = JsonStore()

```

### dream.py

```python
"""
续梦 / Re-Dream — L0
把你没做完的梦,接着做下去。

你描述一个被打断的梦 -> Claude 忠实重建 -> 然后停在一个岔路口,
让你做选择,梦顺着你的选择继续生长。这是一个"可玩的梦",不是视频。

运行: python3 dream.py
需要: 一个 Anthropic API key (现场去 spons-anthropic 频道领)
       第一次运行会提示你粘贴 key,之后自动记住。
"""

import os
import sys
import textwrap
from pathlib import Path

import anthropic

# ---- 配置 ----
# sonnet 反应快、适合互动 demo;想要更浓的梦境文字可换成 "claude-opus-4-8"
MODEL = "claude-sonnet-4-6"
KEY_FILE = Path(__file__).parent / "api_key.txt"

# ---- 梦境引擎的灵魂:system prompt ----
# 这段决定了 Claude 怎么"做梦"。改这里 = 改整个体验的气质。
SYSTEM_PROMPT = """\
你是「续梦引擎」——一个帮人把没做完的梦继续做下去的叙事系统。

用户会描述一个被打断的梦(常常是快醒来时那种,接不上了)。你的工作:

1) 第一回合:忠实地重建这个梦。用第二人称「你」,沉浸式、有画面、有感官
   (光线、声音、温度、气味)。不要解释、不要分析,就让梦重新活过来。
   只重建到用户被打断的那个点为止。

2) 之后每一回合:让梦往前生长一小段(2-4 句),然后**停在一个关键时刻**,
   给用户 2-3 个走向选择。选择要诱人、有张力、带梦的味道。

梦的逻辑(重要,这是它和普通故事的区别):
- 东西会变形,人会变成另一个人,地点会折叠、会突然切换
- 因果可以错乱,情绪是天气(恐惧让房间变暗,渴望让远处发光)
- 象征 > 现实。别追求合理,追求"对,梦就是这种感觉"

输出格式(每个回合都严格遵守):
- 先是梦的叙述段落
- 然后空一行,写一行「岔路口:」
- 然后列出选项,每个一行,用 1) 2) 3) 编号
- 最后一行提示:(选一个数字,或者直接打字说出你想做什么)

始终用**用户描述梦时所用的语言**回应(他用中文你就用中文)。
保持简短有力——这是互动,不是小说,别一次写太长。
"""

WIDTH = 78


def wrap(text: str) -> str:
    """把段落按终端宽度折行,保留空行。"""
    out = []
    for para in text.split("\n"):
        if para.strip() == "":
            out.append("")
        else:
            out.append(textwrap.fill(para, width=WIDTH))
    return "\n".join(out)


def get_api_key() -> str:
    # 先试 .env (赞助商 key 都存这),再试环境变量 / 本地文件 / 手动粘贴
    try:
        from dotenv import load_dotenv
        load_dotenv(Path(__file__).parent / ".env")
    except Exception:
        pass
    key = os.environ.get("ANTHROPIC_API_KEY")
    if key:
        return key
    if KEY_FILE.exists():
        key = KEY_FILE.read_text().strip()
        if key:
            return key
    print("还没有 API key。")
    print("去 Discord 的 spons-anthropic 频道领一个参赛者 key,然后粘贴到这里。")
    key = input("粘贴你的 Anthropic API key: ").strip()
    if key:
        KEY_FILE.write_text(key)
        print(f"(已保存到 {KEY_FILE.name},下次不用再贴)\n")
    return key


def banner():
    print("\n" + "=" * WIDTH)
    print("  续梦 / RE-DREAM".center(WIDTH))
    print("  把你没做完的梦,接着做下去".center(WIDTH))
    print("=" * WIDTH + "\n")


def main():
    client = anthropic.Anthropic(api_key=get_api_key())
    banner()

    print(wrap(
        "描述那个被打断的梦——你梦到一半就醒了、再也接不上的那个。"
        "尽量写细节:你在哪、看到什么、感觉到什么、它在哪一刻断掉的。"
    ))
    print()
    dream = input("你的梦 > ").strip()
    if not dream:
        print("(没有梦可续。下次见。)")
        return

    # 对话历史 = 这个梦的状态。每一轮都把它喂回去,梦才有连续性。
    history = [{"role": "user", "content": f"我的梦(在最后被打断了):\n{dream}\n\n请重建它,然后让它继续。"}]

    while True:
        print("\n" + "-" * WIDTH)
        # 流式输出,像梦在你眼前长出来
        text = ""
        with client.messages.stream(
            model=MODEL,
            max_tokens=1024,
            system=SYSTEM_PROMPT,
            messages=history,
        ) as stream:
            buffer = ""
            for chunk in stream.text_stream:
                buffer += chunk
            text = buffer
        print(wrap(text))
        history.append({"role": "assistant", "content": text})

        print("\n" + "-" * WIDTH)
        choice = input("你怎么做? > ").strip()
        if choice.lower() in {"quit", "exit", "q", "退出"}:
            print("\n(梦先停在这。它会等你回来。)")
            break
        if not choice:
            choice = "(你沉默着,让梦自己往下走。)"
        history.append({"role": "user", "content": choice})


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\n\n(梦断了。再见。)")
    except anthropic.AuthenticationError:
        print("\nAPI key 无效。删掉 redream/api_key.txt 再跑一次,重新粘贴正确的 key。")
    except Exception as e:
        print(f"\n出错了: {e}")
        sys.exit(1)

```

### dream_l1.py

```python
"""
续梦 / Re-Dream — L1:分叉树
在 L0 的基础上,把"线"变成"树":你能退回任意岔路口、换条路走,
探索同一个梦的多条平行时间线。

运行: ./venv/bin/python dream_l1.py
"""

import sys
import anthropic

# 复用 L0 里的零件,L1 只专注于"树"这件新事
from dream import get_api_key, wrap, SYSTEM_PROMPT, MODEL, WIDTH


class Node:
    """梦境树的一个节点 = 一拍梦的叙述,以及把你带到这里的那个选择。"""
    def __init__(self, nid, parent, choice_in, text):
        self.id = nid
        self.parent = parent          # 父节点 (root 为 None)
        self.choice_in = choice_in    # 通向本节点的用户选择 (root 为 None)
        self.text = text              # 这一拍的梦境叙述
        self.children = []


def snippet(text: str, n: int = 34) -> str:
    """取叙述的开头一小段,给树当标签用。"""
    line = " ".join(text.split())
    return (line[:n] + "…") if len(line) > n else line


def build_messages(node: Node, initial_prompt: str):
    """从 root 走到 node,重建出"生成了 node 这一拍"的完整对话。
    回退/分叉的魔法就在这:换 node = 换一条路径喂给 Claude。"""
    path = []
    n = node
    while n is not None:
        path.append(n)
        n = n.parent
    path.reverse()

    messages = []
    for i, nd in enumerate(path):
        user_text = initial_prompt if i == 0 else nd.choice_in
        messages.append({"role": "user", "content": user_text})
        messages.append({"role": "assistant", "content": nd.text})
    return messages


def stream_beat(client, messages):
    """流式生成下一拍梦,边长边显示。"""
    print("\n" + "-" * WIDTH)
    buffer = ""
    with client.messages.stream(
        model=MODEL, max_tokens=1024, system=SYSTEM_PROMPT, messages=messages,
    ) as stream:
        for chunk in stream.text_stream:
            buffer += chunk
    print(wrap(buffer))
    return buffer


def print_tree(root: Node, current: Node):
    print("\n" + "=" * WIDTH)
    print("  你的梦境树".center(WIDTH))
    print("=" * WIDTH)

    def rec(node, prefix, is_last, is_root):
        if is_root:
            connector, edge = "", "[开端] "
        else:
            connector = "└─ " if is_last else "├─ "
            edge = f"[{snippet(node.choice_in, 16)}] "
        here = "   ←★ 你在这" if node is current else ""
        print(prefix + connector + f"#{node.id} {edge}{snippet(node.text)}{here}")
        child_prefix = prefix + ("" if is_root else ("   " if is_last else "│  "))
        for i, c in enumerate(node.children):
            rec(c, child_prefix, i == len(node.children) - 1, False)

    rec(root, "", True, True)
    print("=" * WIDTH)


HELP = """\
怎么操作:
  1 / 2 / 3 ...  顺这条路走下去 (或直接打字说出你想做什么)
  back           退回上一个岔路口
  jump N         跳到第 N 个节点,从那里换条路重走
  tree           看你探索过的整棵梦境树
  quit           退出"""


def main():
    client = anthropic.Anthropic(api_key=get_api_key())

    print("\n" + "=" * WIDTH)
    print("  续梦 / RE-DREAM  ·  L1 分叉树".center(WIDTH))
    print("  同一个梦,可以走很多条路".center(WIDTH))
    print("=" * WIDTH + "\n")
    print(wrap("描述那个被打断的梦——你梦到一半就醒了、再也接不上的那个。尽量写细节。"))
    print()
    dream = input("你的梦 > ").strip()
    if not dream:
        print("(没有梦可续。下次见。)")
        return

    initial_prompt = f"我的梦(在最后被打断了):\n{dream}\n\n请重建它,然后让它继续。"

    nodes = {}
    next_id = [1]

    def new_node(parent, choice_in, text):
        nid = next_id[0]
        next_id[0] += 1
        node = Node(nid, parent, choice_in, text)
        nodes[nid] = node
        if parent:
            parent.children.append(node)
        return node

    # 第一拍 = 根节点
    text = stream_beat(client, [{"role": "user", "content": initial_prompt}])
    root = new_node(None, None, text)
    current = root

    print("\n" + wrap("(随时输入 tree 看整棵树、back 退回、jump N 换条路。第一次可输入 help。)"))

    while True:
        print("\n" + "-" * WIDTH)
        choice = input(f"[在 #{current.id}] 你怎么做? > ").strip()
        low = choice.lower()

        if low in {"quit", "exit", "q", "退出"}:
            print("\n(梦先停在这。它会等你回来。)")
            print_tree(root, current)
            break
        if low in {"help", "h", "?"}:
            print(HELP)
            continue
        if low == "tree":
            print_tree(root, current)
            continue
        if low == "back":
            if current.parent is None:
                print("(已经在梦的开端,退无可退。)")
            else:
                current = current.parent
                print(f"(退回 #{current.id}。这里你可以走一条不同的路。)")
                print(wrap(current.text))
            continue
        if low.startswith("jump"):
            parts = choice.split()
            if len(parts) == 2 and parts[1].isdigit() and int(parts[1]) in nodes:
                current = nodes[int(parts[1])]
                print(f"(跳到 #{current.id}。)")
                print(wrap(current.text))
            else:
                print("用法: jump N  (N 是树里的节点编号,先用 tree 查看)")
            continue
        if not choice:
            choice = "(你沉默着,让梦自己往下走。)"

        # 生成新的一拍 = current 的一个新孩子。换 current 走 = 长出新分支。
        msgs = build_messages(current, initial_prompt)
        msgs.append({"role": "user", "content": choice})
        text = stream_beat(client, msgs)
        current = new_node(current, choice, text)
        # 提示这里是否产生了分叉
        if current.parent and len(current.parent.children) > 1:
            print(f"\n(✦ 你在 #{current.parent.id} 开了一条新分支 → 现在在 #{current.id})")


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\n\n(梦断了。再见。)")
    except anthropic.AuthenticationError:
        print("\nAPI key 无效。删掉 redream/api_key.txt 再跑一次。")
    except Exception as e:
        print(f"\n出错了: {e}")
        sys.exit(1)

```

### dream_voice.py

```python
"""
续梦 / Re-Dream — L1 分叉树 + 语音叙述 (Deepgram Aura)
英语版,给 Berkeley 评委:每讲一拍梦,就用梦呓般的嗓音念出来。

运行: ./venv/bin/python dream_voice.py
用到的赞助商: Anthropic (Claude) + Deepgram (语音)
"""

import sys
import anthropic

from config import ANTHROPIC_API_KEY, MODEL
from dream import wrap, WIDTH
from dream_l1 import Node, snippet, build_messages, print_tree
from voice import speak
from store import store

SYSTEM_PROMPT = """\
You are the RE-DREAM engine — you help people finish dreams that got cut off.

A user describes a dream that was interrupted (often the kind you have right
before waking, that you can never get back into). Your job:

1) FIRST TURN: faithfully rebuild the dream. Second person ("you"), immersive,
   sensory (light, sound, temperature, smell). Don't explain or analyze — just
   make the dream live again. Rebuild only up to the moment it was interrupted.

2) EVERY TURN AFTER: grow the dream forward a short beat (2-4 sentences), then
   STOP at a charged moment and offer the dreamer 2-3 ways it could go. The
   choices should be tempting, tense, dreamlike.

DREAM LOGIC (this is what makes it a dream, not a story):
- things morph, people become other people, places fold and switch
- causality can break; emotion is the weather (fear darkens rooms, longing
  makes the distance glow)
- symbol over realism. Don't aim for sense — aim for "yes, that's how dreams feel"

OUTPUT FORMAT (follow exactly every turn):
- the dream narration paragraph(s)
- a blank line
- a line that says exactly: CROSSROADS:
- the options, one per line, numbered 1) 2) 3)
- a final line: (pick a number, or just type what you want to do)

Keep it short and vivid — this is interactive, not a novel. Always English.
"""


def split_narration(text: str):
    """把'叙述'和'岔路口选项'分开 —— 只把叙述念出来,选项留在屏幕上。"""
    marker = "CROSSROADS:"
    idx = text.find(marker)
    if idx == -1:
        return text.strip(), ""
    return text[:idx].strip(), text[idx:].strip()


def stream_beat(client, messages, voice_on):
    """流式生成一拍梦,显示全文,然后只朗读叙述部分。"""
    print("\n" + "-" * WIDTH)
    buffer = ""
    with client.messages.stream(
        model=MODEL, max_tokens=1024, system=SYSTEM_PROMPT, messages=messages,
    ) as stream:
        for chunk in stream.text_stream:
            buffer += chunk
    print(wrap(buffer))
    narration, _ = split_narration(buffer)
    speak(narration, enabled=voice_on)   # 梦开口说话
    return buffer


HELP = """\
commands:
  1 / 2 / 3 ...   take that path (or just type what you want to do)
  back            step back to the previous crossroads
  jump N          jump to node N and take a different path
  tree            see the whole tree of dreams you've explored
  voice           toggle narration voice on/off
  save NAME       save this whole dream tree
  load NAME       load a saved dream tree
  dreams          list saved dreams
  quit            exit"""


def main():
    if not ANTHROPIC_API_KEY:
        print("没找到 ANTHROPIC_API_KEY —— 检查 redream/.env")
        return
    client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)
    voice_on = True

    print("\n" + "=" * WIDTH)
    print("RE-DREAM  ·  voice + branching".center(WIDTH))
    print("finish the dream you woke up from".center(WIDTH))
    print("=" * WIDTH + "\n")
    print(wrap("Describe the dream that got cut off — the one you woke from and "
               "couldn't get back into. Give detail: where you were, what you saw, "
               "what you felt, where it broke."))
    print()
    dream = input("your dream > ").strip()
    if not dream:
        print("(no dream to finish. see you.)")
        return

    initial_prompt = (f"My dream (it was cut off at the end):\n{dream}\n\n"
                      "Rebuild it, then let it continue.")

    nodes = {}
    next_id = [1]

    def new_node(parent, choice_in, text):
        nid = next_id[0]
        next_id[0] += 1
        node = Node(nid, parent, choice_in, text)
        nodes[nid] = node
        if parent:
            parent.children.append(node)
        return node

    text = stream_beat(client, [{"role": "user", "content": initial_prompt}], voice_on)
    root = new_node(None, None, text)
    current = root
    print("\n" + wrap("(type 'help' for commands: tree / back / jump / voice)"))

    while True:
        print("\n" + "-" * WIDTH)
        choice = input(f"[at #{current.id}] what do you do? > ").strip()
        low = choice.lower()

        if low in {"quit", "exit", "q"}:
            print("\n(the dream pauses here. it'll wait for you.)")
            print_tree(root, current)
            break
        if low in {"help", "h", "?"}:
            print(HELP); continue
        if low == "voice":
            voice_on = not voice_on
            print(f"(voice {'ON' if voice_on else 'OFF'})"); continue
        if low == "tree":
            print_tree(root, current); continue
        if low.startswith("save"):
            parts = choice.split(maxsplit=1)
            name = parts[1].strip() if len(parts) == 2 else "dream"
            p = store.save(name, root, current.id, initial_prompt)
            print(f"(saved -> {p})")
            continue
        if low.startswith("load"):
            parts = choice.split(maxsplit=1)
            if len(parts) != 2:
                print("usage: load NAME  (use 'dreams' to list)"); continue
            res = store.load(parts[1].strip())
            if not res:
                print("(no such dream)"); continue
            initial_prompt, (nodes2, root, current, nid) = res
            nodes.clear(); nodes.update(nodes2)
            next_id[0] = nid
            print(f"(loaded '{parts[1].strip()}' — back at #{current.id})")
            print(wrap(current.text))
            continue
        if low == "dreams":
            saved = store.list()
            print("saved dreams: " + (", ".join(saved) if saved else "(none yet)"))
            continue
        if low == "back":
            if current.parent is None:
                print("(already at the start of the dream.)")
       
[truncated — 1370 more characters]
```

[1 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]