create-local-voice-agent 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-local-voice-agent",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "CLI to scaffold production-ready, containerized Voice AI Agents using LiveKit, Python, and Docker",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -27,6 +27,7 @@
27
27
  "templates/"
28
28
  ],
29
29
  "dependencies": {
30
+ "@inquirer/input": "^5.0.8",
30
31
  "@inquirer/prompts": "^8.3.0",
31
32
  "commander": "^14.0.3",
32
33
  "ejs": "^4.0.1"
package/src/index.js CHANGED
@@ -1,8 +1,22 @@
1
1
  import { Command } from 'commander';
2
- import { select, input, confirm } from '@inquirer/prompts';
2
+ import { select, confirm } from '@inquirer/prompts';
3
+ import input from '@inquirer/input';
3
4
  import { execSync } from 'node:child_process';
5
+ import os from 'node:os';
4
6
  import { scaffold } from './scaffold.js';
5
7
 
8
+ function getLocalIp() {
9
+ const nets = os.networkInterfaces();
10
+ for (const name of Object.keys(nets)) {
11
+ for (const net of nets[name]) {
12
+ if (net.family === 'IPv4' && !net.internal) {
13
+ return net.address;
14
+ }
15
+ }
16
+ }
17
+ return '127.0.0.1';
18
+ }
19
+
6
20
  // ─── Ollama API Fetching ──────────────────────────────────────────────
7
21
  async function fetchOllamaModels(ollamaIp = '127.0.0.1') {
8
22
  try {
@@ -172,6 +186,7 @@ program
172
186
  let includeFrontend = true;
173
187
  let shouldAutoStart = false;
174
188
  let ollamaIp = 'localhost';
189
+ let hostIp = getLocalIp();
175
190
 
176
191
  // ─── AUTO MODE ────────────────────────────────────────────────
177
192
  if (setupMode === 'auto') {
@@ -308,6 +323,12 @@ program
308
323
  shouldAutoStart = false;
309
324
  }
310
325
 
326
+ hostIp = await input({
327
+ message:
328
+ '🌐 Enter the IP address where this agent will run (Press Enter to use detected IP):',
329
+ default: getLocalIp(),
330
+ });
331
+
311
332
  // ─── Scaffold ─────────────────────────────────────────────────
312
333
  await scaffold({
313
334
  projectName,
@@ -317,6 +338,7 @@ program
317
338
  modelName,
318
339
  language,
319
340
  ollamaIp,
341
+ hostIp,
320
342
  });
321
343
 
322
344
  console.log(`\n✅ Project "${projectName}" created successfully!\n`);
@@ -345,4 +367,4 @@ program
345
367
  }
346
368
  });
347
369
 
348
- program.parse();
370
+ program.parse();
package/src/scaffold.js CHANGED
@@ -51,6 +51,7 @@ export async function scaffold({
51
51
  modelName = 'qwen3-vl:2b',
52
52
  language = 'en',
53
53
  ollamaIp = 'localhost',
54
+ hostIp = '127.0.0.1',
54
55
  }) {
55
56
  const projectDir = path.resolve(process.cwd(), projectName);
56
57
 
@@ -71,6 +72,7 @@ export async function scaffold({
71
72
  modelName,
72
73
  language,
73
74
  ollamaIp,
75
+ hostIp,
74
76
  cpuLimit,
75
77
  };
76
78
 
@@ -99,4 +101,3 @@ export async function scaffold({
99
101
 
100
102
  console.log(`\n📁 Generated files in ./${projectName}/`);
101
103
  }
102
-
@@ -36,18 +36,17 @@ services:
36
36
  redis:
37
37
  condition: service_healthy
38
38
  ports:
39
- - "7880:7880" # HTTP / WebSocket
40
- - "7881:7881" # RTC TCP
41
- - "50000-50050:50000-50050/udp" # WebRTC UDP (strictly limited range)
39
+ - "7880:7880"
40
+ - "7881:7881"
41
+ - "50000-50050:50000-50050/udp"
42
42
  environment:
43
- - LIVEKIT_KEYS=${LIVEKIT_API_KEY}:${LIVEKIT_API_SECRET}
43
+ - "LIVEKIT_KEYS=${LIVEKIT_API_KEY}: ${LIVEKIT_API_SECRET}"
44
+ - "NODE_IP=<%= hostIp %>"
45
+ - "UDP_PORT=50000-50050"
46
+ - "REDIS_HOST=redis:6379"
44
47
  command: >
45
- --config /etc/livekit.yaml
46
48
  --bind 0.0.0.0
47
- --redis-host redis:6379
48
- --rtc.port-range-start 50000
49
- --rtc.port-range-end 50050
50
- --rtc.tcp-port 7881
49
+ --dev
51
50
 
52
51
  # ─── Python Voice Agent ────────────────────────────────────────────
53
52
  agent:
@@ -6,31 +6,102 @@ import {
6
6
  RoomAudioRenderer,
7
7
  useVoiceAssistant,
8
8
  BarVisualizer,
9
+ Chat,
9
10
  DisconnectButton,
10
11
  } from "@livekit/components-react";
11
12
  import "@livekit/components-styles";
12
13
 
13
14
  function VoiceAssistantUI() {
14
15
  const { state, audioTrack } = useVoiceAssistant();
16
+ const [temperature, setTemperature] = useState(0.7);
17
+ const [selectedModel, setSelectedModel] = useState("qwen3-vl:2b");
15
18
 
16
19
  return (
17
20
  <div
18
21
  style={{
19
- display: "flex",
20
- flexDirection: "column",
21
- alignItems: "center",
22
+ display: "grid",
23
+ gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
22
24
  gap: "1rem",
23
- padding: "2rem",
25
+ padding: "1rem 0",
26
+ textAlign: "left",
24
27
  }}
25
28
  >
26
- <h2>Agent Status: {state}</h2>
27
- <BarVisualizer
28
- state={state}
29
- barCount={5}
30
- trackRef={audioTrack}
31
- style={{ width: "300px", height: "80px" }}
32
- />
33
- <DisconnectButton>Disconnect</DisconnectButton>
29
+ <section
30
+ style={{
31
+ border: "1px solid #e5e7eb",
32
+ borderRadius: "12px",
33
+ padding: "1rem",
34
+ background: "#ffffff",
35
+ }}
36
+ >
37
+ <h2 style={{ marginTop: 0, marginBottom: "0.5rem" }}>Voice + Text</h2>
38
+ <p style={{ marginTop: 0, color: "#374151" }}>Agent Status: {state}</p>
39
+ <BarVisualizer
40
+ state={state}
41
+ barCount={5}
42
+ trackRef={audioTrack}
43
+ style={{ width: "100%", height: "80px" }}
44
+ />
45
+ <div
46
+ style={{
47
+ marginTop: "1rem",
48
+ border: "1px solid #e5e7eb",
49
+ borderRadius: "10px",
50
+ overflow: "hidden",
51
+ minHeight: "340px",
52
+ }}
53
+ >
54
+ <Chat />
55
+ </div>
56
+ </section>
57
+
58
+ <aside
59
+ style={{
60
+ border: "1px solid #e5e7eb",
61
+ borderRadius: "12px",
62
+ padding: "1rem",
63
+ background: "#f8fafc",
64
+ display: "flex",
65
+ flexDirection: "column",
66
+ gap: "1rem",
67
+ }}
68
+ >
69
+ <h3 style={{ margin: 0 }}>Control Panel</h3>
70
+
71
+ <div>
72
+ <h4 style={{ margin: "0 0 0.5rem 0" }}>System Status</h4>
73
+ <p style={{ margin: 0, color: "#374151" }}>Connection State: {state}</p>
74
+ </div>
75
+
76
+ <div style={{ display: "flex", flexDirection: "column", gap: "0.75rem" }}>
77
+ <h4 style={{ margin: 0 }}>Model Settings</h4>
78
+ <label style={{ display: "flex", flexDirection: "column", gap: "0.35rem" }}>
79
+ <span>Temperature: {temperature.toFixed(2)}</span>
80
+ <input
81
+ type="range"
82
+ min="0"
83
+ max="1"
84
+ step="0.01"
85
+ value={temperature}
86
+ onChange={(event) => setTemperature(Number(event.target.value))}
87
+ />
88
+ </label>
89
+ <label style={{ display: "flex", flexDirection: "column", gap: "0.35rem" }}>
90
+ <span>Select Model</span>
91
+ <select
92
+ value={selectedModel}
93
+ onChange={(event) => setSelectedModel(event.target.value)}
94
+ style={{ padding: "0.5rem", borderRadius: "8px", border: "1px solid #d1d5db" }}
95
+ >
96
+ <option value="qwen3-vl:2b">qwen3-vl:2b</option>
97
+ <option value="qwen3-vl:4b">qwen3-vl:4b</option>
98
+ <option value="llama3.2:3b">llama3.2:3b</option>
99
+ </select>
100
+ </label>
101
+ </div>
102
+
103
+ <DisconnectButton>Disconnect</DisconnectButton>
104
+ </aside>
34
105
  </div>
35
106
  );
36
107
  }
@@ -40,12 +111,23 @@ export default function Home() {
40
111
  const [error, setError] = useState<string | null>(null);
41
112
  const [connected, setConnected] = useState(false);
42
113
  const [serverUrl, setServerUrl] = useState("");
114
+ const [showSecurityWarning, setShowSecurityWarning] = useState(false);
43
115
 
44
116
  useEffect(() => {
45
117
  if (typeof window !== "undefined") {
46
- const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
47
- const defaultUrl = `${protocol}//${window.location.hostname}:7880`;
48
- setServerUrl(process.env.NEXT_PUBLIC_LIVEKIT_URL || defaultUrl);
118
+ let envUrl = process.env.NEXT_PUBLIC_LIVEKIT_URL;
119
+ // Aggressively override if the baked-in variable is empty, localhost, or 127.0.0.1
120
+ if (!envUrl || envUrl.includes("127.0.0.1") || envUrl.includes("localhost")) {
121
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
122
+ envUrl = `${protocol}//${window.location.hostname}:7880`;
123
+ }
124
+ setServerUrl(envUrl);
125
+
126
+ const isInsecureRemoteHost =
127
+ window.location.protocol === "http:" &&
128
+ window.location.hostname !== "localhost" &&
129
+ window.location.hostname !== "127.0.0.1";
130
+ setShowSecurityWarning(isInsecureRemoteHost);
49
131
  }
50
132
  }, []);
51
133
 
@@ -63,9 +145,27 @@ export default function Home() {
63
145
  fetchToken();
64
146
  }, []);
65
147
 
148
+ const securityBanner = showSecurityWarning ? (
149
+ <div
150
+ style={{
151
+ background: "#fee2e2",
152
+ border: "1px solid #ef4444",
153
+ color: "#991b1b",
154
+ borderRadius: "10px",
155
+ padding: "0.85rem 1rem",
156
+ marginBottom: "1rem",
157
+ fontWeight: 600,
158
+ }}
159
+ >
160
+ ⚠️ Microphone access is blocked by browsers over HTTP on remote IPs. Use HTTPS, Ngrok,
161
+ or enable 'Insecure origins treated as secure' in chrome://flags.
162
+ </div>
163
+ ) : null;
164
+
66
165
  if (error) {
67
166
  return (
68
167
  <div style={{ padding: "2rem", textAlign: "center" }}>
168
+ {securityBanner}
69
169
  <h1>Error</h1>
70
170
  <p>{error}</p>
71
171
  </div>
@@ -75,14 +175,16 @@ export default function Home() {
75
175
  if (!token) {
76
176
  return (
77
177
  <div style={{ padding: "2rem", textAlign: "center" }}>
178
+ {securityBanner}
78
179
  <p>Connecting to voice agent...</p>
79
180
  </div>
80
181
  );
81
182
  }
82
183
 
83
184
  return (
84
- <main style={{ padding: "2rem", textAlign: "center" }}>
185
+ <main style={{ padding: "2rem", textAlign: "center", maxWidth: "1200px", margin: "0 auto" }}>
85
186
  <h1><%= projectName %></h1>
187
+ {securityBanner}
86
188
  {!connected && (
87
189
  <button
88
190
  type="button"