AnimeshK509 commited on
Commit
e6dccad
·
1 Parent(s): c2b8480

Initial Oneironauts MVP: text input, branching at depth 4, closing scene + journal interpretation

Browse files
Files changed (5) hide show
  1. .gitignore +9 -0
  2. README.md +33 -0
  3. app.py +165 -0
  4. llama_endpoint.py +214 -0
  5. requirements.txt +2 -0
.gitignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ venv/
5
+ .env
6
+ .modal.toml
7
+ hello_modal.py
8
+ gpu_test.py
9
+ llama_test.py
README.md ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Oneironauts
3
+ emoji: 🌙
4
+ colorFrom: purple
5
+ colorTo: indigo
6
+ sdk: gradio
7
+ sdk_version: 4.44.1
8
+ app_file: app.py
9
+ pinned: false
10
+ license: mit
11
+ short_description: A fragment of a dream becomes a branching exploration.
12
+ tags:
13
+ - gradio
14
+ - build-small-hackathon
15
+ - llama
16
+ - modal
17
+ ---
18
+
19
+ # Oneironauts
20
+
21
+ A fragment of a half-remembered dream becomes a branching, exploratory narrative. Type a fragment, walk through scenes the dream weaver generates, make choices that shape where the dream goes, and end with a journal-voice interpretation.
22
+
23
+ Built for the Hugging Face Build Small Hackathon, Track 2.
24
+
25
+ ## Stack
26
+
27
+ - **Llama 3.2 3B Instruct** served as a persistent Modal endpoint (A10G)
28
+ - **Gradio** frontend
29
+ - Branching state managed via `gr.State`, 4-level depth
30
+
31
+ ## Status
32
+
33
+ Work in progress — voice input (Whisper) and UI polish coming.
app.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import modal
3
+
4
+ import os
5
+ if not os.getenv("MODAL_TOKEN_ID") and not os.path.exists(os.path.expanduser("~/.modal.toml")):
6
+ raise RuntimeError(
7
+ "Modal credentials missing. Set MODAL_TOKEN_ID and MODAL_TOKEN_SECRET, "
8
+ "or run `modal token new` locally."
9
+ )
10
+
11
+ print("[oneironauts] connecting to Modal endpoint...")
12
+ DreamWeaver = modal.Cls.from_name("oneironauts", "DreamWeaver")
13
+ weaver = DreamWeaver()
14
+ print("[oneironauts] ready")
15
+
16
+ MAX_DEPTH = 4
17
+
18
+
19
+ def start_dream(fragment, state):
20
+ if not fragment.strip():
21
+ return (
22
+ state,
23
+ gr.update(value="Please enter a dream fragment first.", visible=True),
24
+ "—", "—", "—",
25
+ gr.update(visible=False),
26
+ gr.update(visible=True),
27
+ gr.update(visible=False),
28
+ "", "",
29
+ )
30
+
31
+ print(f"[oneironauts] generating opening scene...")
32
+ result = weaver.generate.remote(fragment)
33
+ print(f"[oneironauts] opening done")
34
+
35
+ state = {
36
+ "fragment": fragment,
37
+ "history": [result["scene"]],
38
+ "depth": 1,
39
+ }
40
+ choices = result["choices"]
41
+ return (
42
+ state,
43
+ gr.update(value=result["scene"], visible=True),
44
+ choices[0], choices[1], choices[2],
45
+ gr.update(visible=True),
46
+ gr.update(visible=False),
47
+ gr.update(visible=False),
48
+ "", "",
49
+ )
50
+
51
+
52
+ def pick_choice(choice_text, state):
53
+ state["history"].append(f"Chose: {choice_text}")
54
+
55
+ if state["depth"] >= MAX_DEPTH:
56
+ print(f"[oneironauts] closing dream...")
57
+ closing = weaver.close_dream.remote(
58
+ state["fragment"],
59
+ state["history"],
60
+ choice_text,
61
+ )
62
+ print(f"[oneironauts] closing done")
63
+ return (
64
+ state,
65
+ gr.update(value=closing["scene"], visible=True),
66
+ "—", "—", "—",
67
+ gr.update(visible=False),
68
+ gr.update(visible=True),
69
+ closing["interpretation"],
70
+ )
71
+
72
+ print(f"[oneironauts] continuing at depth {state['depth']}...")
73
+ result = weaver.continue_dream.remote(
74
+ state["fragment"],
75
+ state["history"],
76
+ choice_text,
77
+ )
78
+ state["history"].append(result["scene"])
79
+ state["depth"] += 1
80
+ choices = result["choices"]
81
+ return (
82
+ state,
83
+ gr.update(value=result["scene"], visible=True),
84
+ choices[0], choices[1], choices[2],
85
+ gr.update(visible=True),
86
+ gr.update(visible=False),
87
+ "",
88
+ )
89
+
90
+
91
+ def reset_dream():
92
+ return (
93
+ {},
94
+ gr.update(visible=True),
95
+ gr.update(visible=False),
96
+ gr.update(visible=False),
97
+ gr.update(value="", visible=False),
98
+ "",
99
+ "",
100
+ )
101
+
102
+
103
+ with gr.Blocks(title="Oneironauts") as demo:
104
+ gr.Markdown("# Oneironauts\nA fragment of a dream becomes a branching exploration.")
105
+
106
+ state = gr.State({})
107
+
108
+ with gr.Column(visible=True) as input_panel:
109
+ fragment_box = gr.Textbox(
110
+ label="Describe a fragment of a dream",
111
+ placeholder="I was walking down stairs that wouldn't end...",
112
+ lines=3,
113
+ )
114
+ start_btn = gr.Button("Begin", variant="primary")
115
+
116
+ scene = gr.Markdown(visible=False)
117
+
118
+ with gr.Column(visible=False) as choices_panel:
119
+ gr.Markdown("**Where do you go?**")
120
+ btn_a = gr.Button("A")
121
+ btn_b = gr.Button("B")
122
+ btn_c = gr.Button("C")
123
+
124
+ with gr.Column(visible=False) as ending_panel:
125
+ gr.Markdown("---")
126
+ gr.Markdown("### Dream journal")
127
+ interpretation_box = gr.Markdown()
128
+ reset_btn = gr.Button("Begin a new dream", variant="primary")
129
+
130
+ start_btn.click(
131
+ start_dream,
132
+ inputs=[fragment_box, state],
133
+ outputs=[
134
+ state, scene,
135
+ btn_a, btn_b, btn_c,
136
+ choices_panel, input_panel, ending_panel,
137
+ interpretation_box, fragment_box,
138
+ ],
139
+ )
140
+
141
+ for btn in (btn_a, btn_b, btn_c):
142
+ btn.click(
143
+ pick_choice,
144
+ inputs=[btn, state],
145
+ outputs=[
146
+ state, scene,
147
+ btn_a, btn_b, btn_c,
148
+ choices_panel, ending_panel,
149
+ interpretation_box,
150
+ ],
151
+ )
152
+
153
+ reset_btn.click(
154
+ reset_dream,
155
+ inputs=[],
156
+ outputs=[
157
+ state,
158
+ input_panel, choices_panel, ending_panel,
159
+ scene, interpretation_box, fragment_box,
160
+ ],
161
+ )
162
+
163
+
164
+ if __name__ == "__main__":
165
+ demo.launch()
llama_endpoint.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import modal
2
+
3
+ image = (
4
+ modal.Image.debian_slim()
5
+ .pip_install(
6
+ "torch==2.4.0",
7
+ "transformers==4.45.0",
8
+ "accelerate==1.0.0",
9
+ "huggingface_hub==0.25.0",
10
+ "fastapi[standard]",
11
+ )
12
+ )
13
+
14
+ app = modal.App("oneironauts")
15
+
16
+
17
+ @app.cls(
18
+ image=image,
19
+ gpu="A10G",
20
+ secrets=[modal.Secret.from_name("huggingface-secret")],
21
+ scaledown_window=300,
22
+ timeout=600,
23
+ )
24
+ class DreamWeaver:
25
+ @modal.enter()
26
+ def load(self):
27
+ import torch
28
+ from transformers import AutoModelForCausalLM, AutoTokenizer
29
+
30
+ model_id = "meta-llama/Llama-3.2-3B-Instruct"
31
+ self.tokenizer = AutoTokenizer.from_pretrained(model_id)
32
+ self.model = AutoModelForCausalLM.from_pretrained(
33
+ model_id,
34
+ torch_dtype=torch.bfloat16,
35
+ device_map="auto",
36
+ )
37
+
38
+ def _parse(self, raw: str):
39
+ if "CHOICES:" not in raw:
40
+ return {"scene": raw.strip(), "choices": []}
41
+
42
+ scene_part, choices_part = raw.split("CHOICES:", 1)
43
+ scene = scene_part.replace("SCENE:", "").strip()
44
+
45
+ choices = []
46
+ for line in choices_part.strip().splitlines():
47
+ line = line.strip()
48
+ if not line:
49
+ continue
50
+ for prefix in ("A)", "B)", "C)", "A.", "B.", "C."):
51
+ if line.startswith(prefix):
52
+ choices.append(line[len(prefix):].strip())
53
+ break
54
+ return {"scene": scene, "choices": choices[:3]}
55
+
56
+ @modal.method()
57
+ def generate(self, dream_fragment: str) -> dict:
58
+ system_prompt = (
59
+ "You are a dream weaver. Given a fragment of a dream, write a vivid, "
60
+ "atmospheric scene that expands on it. The scene must be EXACTLY 2 short "
61
+ "paragraphs, 3-4 sentences each. Then provide exactly 3 choices the "
62
+ "dreamer can take next, each ONE short sentence (max 15 words).\n\n"
63
+ "Format:\n"
64
+ "SCENE:\n<scene text>\n\nCHOICES:\nA) <choice>\nB) <choice>\nC) <choice>"
65
+ )
66
+
67
+ messages = [
68
+ {"role": "system", "content": system_prompt},
69
+ {"role": "user", "content": f"Dream fragment: {dream_fragment}"},
70
+ ]
71
+
72
+ inputs = self.tokenizer(
73
+ self.tokenizer.apply_chat_template(messages, add_generation_prompt=True, tokenize=False),
74
+ return_tensors="pt",
75
+ ).to(self.model.device)
76
+
77
+ outputs = self.model.generate(
78
+ **inputs,
79
+ max_new_tokens=600,
80
+ do_sample=True,
81
+ temperature=0.9,
82
+ top_p=0.95,
83
+ pad_token_id=self.tokenizer.eos_token_id,
84
+ )
85
+
86
+ raw = self.tokenizer.decode(
87
+ outputs[0][inputs.input_ids.shape[1]:],
88
+ skip_special_tokens=True,
89
+ )
90
+ return self._parse(raw)
91
+ @modal.method()
92
+ def continue_dream(self, original_fragment: str, history: list, choice: str) -> dict:
93
+ system_prompt = (
94
+ "You are a dream weaver continuing an ongoing dream. The dreamer made "
95
+ "a choice and the dream must continue from that choice, growing stranger "
96
+ "and more dreamlike. Settings can morph mid-scene. Logic can bend. "
97
+ "Things should NOT resolve helpfully — complications deepen.\n\n"
98
+ "Write EXACTLY 2 short paragraphs (3-4 sentences each), then 3 new "
99
+ "choices, each ONE short sentence (max 15 words).\n\n"
100
+ "Format:\n"
101
+ "SCENE:\n<scene text>\n\nCHOICES:\nA) <choice>\nB) <choice>\nC) <choice>"
102
+ )
103
+
104
+ history_text = "\n".join(
105
+ f"- {entry}" for entry in history
106
+ )
107
+
108
+ user_msg = (
109
+ f"Original dream fragment: {original_fragment}\n\n"
110
+ f"What has happened so far:\n{history_text}\n\n"
111
+ f"The dreamer just chose: {choice}\n\n"
112
+ f"Continue the dream from this choice."
113
+ )
114
+
115
+ messages = [
116
+ {"role": "system", "content": system_prompt},
117
+ {"role": "user", "content": user_msg},
118
+ ]
119
+
120
+ inputs = self.tokenizer(
121
+ self.tokenizer.apply_chat_template(messages, add_generation_prompt=True, tokenize=False),
122
+ return_tensors="pt",
123
+ ).to(self.model.device)
124
+
125
+ outputs = self.model.generate(
126
+ **inputs,
127
+ max_new_tokens=600,
128
+ do_sample=True,
129
+ temperature=0.95,
130
+ top_p=0.95,
131
+ pad_token_id=self.tokenizer.eos_token_id,
132
+ )
133
+
134
+ raw = self.tokenizer.decode(
135
+ outputs[0][inputs.input_ids.shape[1]:],
136
+ skip_special_tokens=True,
137
+ )
138
+ return self._parse(raw)
139
+ @modal.method()
140
+ def close_dream(self, original_fragment: str, history: list, final_choice: str) -> dict:
141
+ system_prompt = (
142
+ "You are a dream weaver bringing a dream to its end. The dreamer just "
143
+ "made a final choice. Write ONE closing scene that uses their choice and "
144
+ "ends the dream the way dreams actually end — fading, dissolving, or "
145
+ "hitting a moment of strange clarity before slipping away. Do NOT resolve "
146
+ "the story logically. Do NOT add choices at the end.\n\n"
147
+ "Then, on a new section, write a short interpretation in the voice of "
148
+ "someone writing in their dream journal the next morning, 2-3 sentences, "
149
+ "reflective and a little uncertain. Reference what the dreamer chose.\n\n"
150
+ "Format:\n"
151
+ "SCENE:\n<closing scene, 2 short paragraphs>\n\n"
152
+ "INTERPRETATION:\n<2-3 sentences in journal voice>"
153
+ )
154
+
155
+ history_text = "\n".join(f"- {entry}" for entry in history)
156
+
157
+ user_msg = (
158
+ f"Original dream fragment: {original_fragment}\n\n"
159
+ f"What has happened so far:\n{history_text}\n\n"
160
+ f"The dreamer's final choice: {final_choice}\n\n"
161
+ f"End the dream."
162
+ )
163
+
164
+ messages = [
165
+ {"role": "system", "content": system_prompt},
166
+ {"role": "user", "content": user_msg},
167
+ ]
168
+
169
+ inputs = self.tokenizer(
170
+ self.tokenizer.apply_chat_template(messages, add_generation_prompt=True, tokenize=False),
171
+ return_tensors="pt",
172
+ ).to(self.model.device)
173
+
174
+ outputs = self.model.generate(
175
+ **inputs,
176
+ max_new_tokens=500,
177
+ do_sample=True,
178
+ temperature=0.85,
179
+ top_p=0.95,
180
+ pad_token_id=self.tokenizer.eos_token_id,
181
+ )
182
+
183
+ raw = self.tokenizer.decode(
184
+ outputs[0][inputs.input_ids.shape[1]:],
185
+ skip_special_tokens=True,
186
+ )
187
+ return self._parse_closing(raw)
188
+
189
+ def _parse_closing(self, raw: str):
190
+ if "INTERPRETATION:" not in raw:
191
+ return {"scene": raw.strip(), "interpretation": ""}
192
+
193
+ scene_part, interp_part = raw.split("INTERPRETATION:", 1)
194
+ scene = scene_part.replace("SCENE:", "").strip()
195
+ interpretation = interp_part.strip()
196
+ return {"scene": scene, "interpretation": interpretation}
197
+
198
+
199
+ @app.local_entrypoint()
200
+ def main():
201
+ import json
202
+ weaver = DreamWeaver()
203
+ fragment = "I was in an elevator that kept going up past my floor, and the buttons rearranged themselves every time I looked away"
204
+
205
+ print("=== OPENING ===")
206
+ opening = weaver.generate.remote(fragment)
207
+ print(json.dumps(opening, indent=2))
208
+
209
+ print("\n=== DEPTH 1 ===")
210
+ choice = opening["choices"][2]
211
+ history = [opening["scene"], f"Chose: {choice}"]
212
+ next_scene = weaver.continue_dream.remote(fragment, history, choice)
213
+ print(f"Chose: {choice}\n")
214
+ print(json.dumps(next_scene, indent=2))
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gradio==4.44.1
2
+ modal==0.66.0