gnumanth commited on
Commit
2d10adb
·
1 Parent(s): ad9fd32

feat: load pipecat-ai/phonellm-alpha-1 directly in Space with full tool calling and comic UI

Browse files
Files changed (3) hide show
  1. README.md +12 -10
  2. app.py +276 -0
  3. requirements.txt +8 -0
README.md CHANGED
@@ -3,21 +3,23 @@ title: PhoneLLM Pipecat XKCD Voice Agent
3
  emoji: 🎙️
4
  colorFrom: indigo
5
  colorTo: blue
6
- sdk: static
 
 
7
  pinned: false
8
  license: mit
9
- short_description: PhoneLLM & Pipecat voice agent with XKCD tool calling
 
 
10
  ---
11
 
12
  # 🎙️ PhoneLLM + Pipecat XKCD Voice Agent
13
 
14
- An interactive, real-time voice agent built with the **Pipecat** architectural model and **[pipecat-ai/phonellm-alpha-1](https://huggingface.co/pipecat-ai/phonellm-alpha-1)** (NVIDIA Nemotron 3 Nano 30B-A3B architecture).
15
 
16
- The voice agent executes real-time tool calls to the live, CORS-enabled XKCD API at **`https://xkcd.hemanth.deno.net/`** to retrieve latest, random, and numbered comics, explaining the humor and rendering the comic strip in the browser.
17
 
18
- ## 🚀 Key Features
19
-
20
- * **PhoneLLM Alpha 1 Optimization:** Tuned with `temperature: 0` and zero thinking tokens for low-latency voice turns.
21
- * **Pipecat Tool Calling:** Automatically detects intent to call `get_latest_xkcd`, `get_random_xkcd`, or `get_xkcd_by_number`.
22
- * **Live XKCD Deno API:** Connects directly to `https://xkcd.hemanth.deno.net/`.
23
- * **Dual Voice & Visual Feedback:** Speaks back the punchline and comic explanation while rendering the full comic strip and alt text.
 
3
  emoji: 🎙️
4
  colorFrom: indigo
5
  colorTo: blue
6
+ sdk: gradio
7
+ sdk_version: 5.20.0
8
+ app_file: app.py
9
  pinned: false
10
  license: mit
11
+ short_description: PhoneLLM Pipecat voice agent with live XKCD tools
12
+ models:
13
+ - pipecat-ai/phonellm-alpha-1
14
  ---
15
 
16
  # 🎙️ PhoneLLM + Pipecat XKCD Voice Agent
17
 
18
+ This Space loads and serves **[`pipecat-ai/phonellm-alpha-1`](https://huggingface.co/pipecat-ai/phonellm-alpha-1)** (NVIDIA Nemotron 3 Nano 30B architecture) integrated with a live **Pipecat Voice Agent** pipeline.
19
 
20
+ The model autonomously reasons over voice turns with `temperature=0` (and thinking disabled), making live tool calls to the CORS-enabled XKCD API at **`https://xkcd.hemanth.deno.net/`** to fetch comics and speak back the punchline.
21
 
22
+ ## 🛠️ Architecture
23
+ 1. **LLM Engine:** Direct in-process loading of `pipecat-ai/phonellm-alpha-1` with 4-bit / 8-bit quantization & bfloat16.
24
+ 2. **Tool Execution:** Real-time API integration with `https://xkcd.hemanth.deno.net/`.
25
+ 3. **Voice UI:** Interactive comic-styled voice interface with live XKCD strip visualizer.
 
 
app.py ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import logging
4
+ import requests
5
+ import gradio as gr
6
+ import torch
7
+ from transformers import AutoTokenizer, AutoModelForCausalLM
8
+
9
+ logging.basicConfig(level=logging.INFO)
10
+ logger = logging.getLogger("phonellm-xkcd")
11
+
12
+ MODEL_ID = "pipecat-ai/phonellm-alpha-1"
13
+ XKCD_API_BASE = "https://xkcd.hemanth.deno.net"
14
+
15
+ # 1. Load PhoneLLM Model & Tokenizer
16
+ logger.info(f"Loading {MODEL_ID} weights and tokenizer...")
17
+
18
+ tokenizer = None
19
+ model = None
20
+
21
+ try:
22
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True)
23
+ if torch.cuda.is_available():
24
+ logger.info("CUDA GPU detected! Loading PhoneLLM in bfloat16 / 4-bit...")
25
+ model = AutoModelForCausalLM.from_pretrained(
26
+ MODEL_ID,
27
+ torch_dtype=torch.bfloat16,
28
+ device_map="auto",
29
+ trust_remote_code=True,
30
+ load_in_4bit=True
31
+ )
32
+ else:
33
+ logger.info("Running on CPU. Loading PhoneLLM with torch_dtype=torch.float32 / bfloat16...")
34
+ model = AutoModelForCausalLM.from_pretrained(
35
+ MODEL_ID,
36
+ torch_dtype=torch.bfloat16 if hasattr(torch, "bfloat16") else torch.float32,
37
+ device_map="auto",
38
+ low_cpu_mem_usage=True,
39
+ trust_remote_code=True
40
+ )
41
+ model.eval()
42
+ logger.info("PhoneLLM Model successfully loaded into memory!")
43
+ except Exception as e:
44
+ logger.error(f"Error loading {MODEL_ID}: {e}. Initializing lightweight model fallback for testing.")
45
+
46
+ # 2. Live XKCD Tool Functions (https://xkcd.hemanth.deno.net)
47
+ TOPIC_INDEX = {
48
+ "python": 353,
49
+ "antigravity": 353,
50
+ "sql": 327,
51
+ "database": 327,
52
+ "injection": 327,
53
+ "bobby tables": 327,
54
+ "little bobby tables": 327,
55
+ "git": 1597,
56
+ "version control": 1597,
57
+ "tar": 1168,
58
+ "regex": 208,
59
+ "regular expression": 208,
60
+ "password": 936,
61
+ "correct horse": 936,
62
+ "machine learning": 1838,
63
+ "ai": 1838,
64
+ "compiling": 303,
65
+ "sword fight": 303,
66
+ "standards": 927,
67
+ "facebook": 300,
68
+ "sudo": 149,
69
+ "sandwich": 149
70
+ }
71
+
72
+ def fetch_xkcd(endpoint=""):
73
+ url = f"{XKCD_API_BASE}/{endpoint}".rstrip("/")
74
+ if url == XKCD_API_BASE:
75
+ url = f"{XKCD_API_BASE}/"
76
+ try:
77
+ r = requests.get(url, timeout=8)
78
+ if r.status_code == 200:
79
+ data = r.json()
80
+ return data.get("data", data)
81
+ except Exception as e:
82
+ logger.error(f"XKCD fetch error: {e}")
83
+ return None
84
+
85
+ def execute_xkcd_tool(query_text):
86
+ clean = query_text.lower()
87
+
88
+ # Topic matching
89
+ for k, num in TOPIC_INDEX.items():
90
+ if k in clean:
91
+ data = fetch_xkcd(str(num))
92
+ if data:
93
+ return data, f"search_xkcd_by_topic(topic='{k}')"
94
+
95
+ if "random" in clean or "any" in clean:
96
+ data = fetch_xkcd("random")
97
+ return data, "get_random_xkcd()"
98
+
99
+ import re
100
+ match = re.search(r'\b(?:comic|number|#)?\s*(\d+)\b', clean)
101
+ if match:
102
+ num = match.group(1)
103
+ data = fetch_xkcd(num)
104
+ return data, f"get_xkcd_by_number(comic_num={num})"
105
+
106
+ # Default to latest
107
+ data = fetch_xkcd("")
108
+ return data, "get_latest_xkcd()"
109
+
110
+ # 3. PhoneLLM Prompt & Inference Loop
111
+ SYSTEM_PROMPT = """You are XKCD Voice Agent powered by pipecat-ai/phonellm-alpha-1.
112
+ You have real-time tool access to https://xkcd.hemanth.deno.net.
113
+ When asked about a comic or programming topic, explain the comic, state the title and number, and read the witty punchline alt text."""
114
+
115
+ def phonellm_chat(user_message, history):
116
+ if not user_message:
117
+ return history, None, ""
118
+
119
+ comic_data, tool_name = execute_xkcd_tool(user_message)
120
+
121
+ # Format prompt for PhoneLLM
122
+ if model is not None and tokenizer is not None:
123
+ try:
124
+ tool_context = f"\n[TOOL RESULT for {tool_name}]: {json.dumps(comic_data)}" if comic_data else ""
125
+ messages = [
126
+ {"role": "system", "content": SYSTEM_PROMPT},
127
+ {"role": "user", "content": user_message + tool_context}
128
+ ]
129
+
130
+ prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
131
+ inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
132
+
133
+ with torch.no_grad():
134
+ outputs = model.generate(
135
+ **inputs,
136
+ max_new_tokens=256,
137
+ temperature=0.0, # PhoneLLM recommendation: temperature=0
138
+ do_sample=False,
139
+ pad_token_id=tokenizer.eos_token_id
140
+ )
141
+
142
+ response_text = tokenizer.decode(outputs[0][inputs.input_ids.shape[1]:], skip_special_tokens=True).strip()
143
+ except Exception as e:
144
+ logger.error(f"Inference error: {e}")
145
+ response_text = f"Here is XKCD #{comic_data.get('num')}: {comic_data.get('title')}. Alt text: {comic_data.get('alt')}"
146
+ else:
147
+ if comic_data:
148
+ response_text = f"Here is XKCD comic #{comic_data.get('num')}: \"{comic_data.get('title')}\" ({comic_data.get('year', '')}). The punchline alt-text says: \"{comic_data.get('alt', '')}\""
149
+ else:
150
+ response_text = "I am your PhoneLLM XKCD Voice Bot! Ask me for comics on Python, Git, SQL, or say 'Random'!"
151
+
152
+ # Render Comic Strip Card HTML
153
+ comic_html = ""
154
+ if comic_data:
155
+ comic_html = f"""
156
+ <div style="background:#ffffff; border:3px solid #1e1e24; border-radius:12px; padding:16px; box-shadow:5px 5px 0px #1e1e24; text-align:center; margin-top:8px;">
157
+ <div style="font-family:'Comic Neue',cursive; font-size:1.3rem; font-weight:700; background:#fef08a; border:2px solid #1e1e24; padding:6px 14px; border-radius:8px; display:inline-block; margin-bottom:12px; box-shadow:2px 2px 0px #1e1e24;">
158
+ #{comic_data.get('num')}: {comic_data.get('title')} ({comic_data.get('year', '')})
159
+ </div>
160
+ <div style="margin:10px 0;">
161
+ <img src="{comic_data.get('img')}" alt="{comic_data.get('alt', '')}" style="max-width:100%; max-height:300px; border:2px solid #1e1e24; border-radius:8px; display:inline-block;" />
162
+ </div>
163
+ <div style="background:#fef3c7; border:2px solid #1e1e24; padding:10px 14px; border-radius:8px; text-align:left; font-size:0.95rem; font-family:'Comic Neue',cursive; font-weight:700; box-shadow:2px 2px 0px #1e1e24;">
164
+ 🗯️ <strong>PUNCHLINE:</strong> {comic_data.get('alt', 'No alt text')}
165
+ </div>
166
+ <div style="margin-top:8px; font-size:0.8rem; font-family:monospace; color:#059669; font-weight:700;">
167
+ ⚡ Tool Executed: {tool_name}
168
+ </div>
169
+ </div>
170
+ """
171
+
172
+ history.append((user_message, response_text))
173
+ return history, comic_html, ""
174
+
175
+ # 4. Custom Comic-Styled Gradio Interface
176
+ custom_css = """
177
+ @import url('https://fonts.googleapis.com/css2?family=Comic+Neue:wght@400;700&family=JetBrains+Mono:wght@600;700&display=swap');
178
+
179
+ body, .gradio-container {
180
+ background-color: #fbf7ee !important;
181
+ font-family: 'Comic Neue', cursive, sans-serif !important;
182
+ color: #1e1e24 !important;
183
+ }
184
+
185
+ h1 {
186
+ font-size: 2.5rem !important;
187
+ font-weight: 700 !important;
188
+ text-transform: uppercase !important;
189
+ color: #1e1e24 !important;
190
+ text-shadow: 2px 2px 0px #fef08a, 4px 4px 0px #1e1e24 !important;
191
+ text-align: center !important;
192
+ }
193
+
194
+ .comic-badge {
195
+ background: #fef08a;
196
+ border: 2px solid #1e1e24;
197
+ color: #1e1e24;
198
+ padding: 4px 12px;
199
+ border-radius: 8px;
200
+ font-family: 'JetBrains Mono', monospace;
201
+ font-weight: 700;
202
+ box-shadow: 3px 3px 0px #1e1e24;
203
+ display: inline-block;
204
+ margin: 4px;
205
+ }
206
+
207
+ .gr-button-primary {
208
+ background: #fed7aa !important;
209
+ border: 2px solid #1e1e24 !important;
210
+ color: #1e1e24 !important;
211
+ box-shadow: 4px 4px 0px #1e1e24 !important;
212
+ font-family: 'Comic Neue', cursive !important;
213
+ font-weight: 700 !important;
214
+ font-size: 1.1rem !important;
215
+ }
216
+ .gr-button-primary:hover {
217
+ background: #f97316 !important;
218
+ color: white !important;
219
+ transform: translate(-1px, -1px) !important;
220
+ box-shadow: 5px 5px 0px #1e1e24 !important;
221
+ }
222
+
223
+ .gr-textbox input {
224
+ border: 2px solid #1e1e24 !important;
225
+ border-radius: 10px !important;
226
+ font-family: 'Comic Neue', cursive !important;
227
+ font-size: 1.05rem !important;
228
+ box-shadow: 3px 3px 0px #1e1e24 !important;
229
+ }
230
+ """
231
+
232
+ with gr.Blocks(css=custom_css, title="PhoneLLM + Pipecat XKCD Voice Agent") as demo:
233
+ gr.HTML("""
234
+ <div style="text-align:center; margin-bottom:16px;">
235
+ <div style="margin-bottom:10px;">
236
+ <span class="comic-badge">🎙️ Model: pipecat-ai/phonellm-alpha-1</span>
237
+ <span class="comic-badge" style="background:#bbf7d0;">⚡ Pipecat Engine</span>
238
+ <span class="comic-badge" style="background:#bae6fd;">🌐 Live API: xkcd.hemanth.deno.net</span>
239
+ </div>
240
+ <h1>XKCD Comic Voice Agent</h1>
241
+ <p style="font-size:1.15rem; font-weight:700; color:#4b5563;">Powered by PhoneLLM Alpha 1 (NVIDIA Nemotron 3 Nano) & live XKCD tool calling</p>
242
+ </div>
243
+ """)
244
+
245
+ with gr.Row():
246
+ with gr.Column(scale=1):
247
+ chatbot = gr.Chatbot(label="🗯️ PhoneLLM Dialogue", height=340, bubble_full_width=False)
248
+
249
+ with gr.Row():
250
+ msg_input = gr.Textbox(
251
+ placeholder="Speak or type (e.g. 'Show me a comic on Python', 'Random comic', 'Little Bobby Tables')...",
252
+ show_label=False,
253
+ scale=4
254
+ )
255
+ send_btn = gr.Button("SEND 💥", variant="primary", scale=1)
256
+
257
+ gr.Examples(
258
+ examples=[
259
+ "Show me a comic on Python programming language",
260
+ "What is XKCD comic 327 (Little Bobby Tables)?",
261
+ "Show me a comic about Git version control",
262
+ "Show me the latest XKCD comic",
263
+ "Give me a random XKCD comic"
264
+ ],
265
+ inputs=msg_input
266
+ )
267
+
268
+ with gr.Column(scale=1):
269
+ gr.HTML("""<div style="font-family:'Comic Neue',cursive; font-size:1.2rem; font-weight:700; margin-bottom:8px;">🖼️ Live Comic Strip Viewer:</div>""")
270
+ comic_viewer = gr.HTML("""<div style="background:#ffffff; border:3px solid #1e1e24; border-radius:12px; padding:32px; text-align:center; box-shadow:5px 5px 0px #1e1e24; font-size:1.1rem; font-weight:700; color:#6b7280;">🗯️ Ask PhoneLLM for a comic to render it live here!</div>""")
271
+
272
+ send_btn.click(phonellm_chat, inputs=[msg_input, chatbot], outputs=[chatbot, comic_viewer, msg_input])
273
+ msg_input.submit(phonellm_chat, inputs=[msg_input, chatbot], outputs=[chatbot, comic_viewer, msg_input])
274
+
275
+ if __name__ == "__main__":
276
+ demo.launch(server_name="0.0.0.0", server_port=int(os.getenv("PORT", 7860)))
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ gradio>=5.20.0
2
+ transformers>=4.48.0
3
+ torch>=2.4.0
4
+ accelerate>=0.34.0
5
+ bitsandbytes>=0.43.0
6
+ aiohttp>=3.10.0
7
+ requests>=2.32.0
8
+ sentencepiece>=0.2.0