infinicode 2.8.86 → 2.8.87
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.
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* registration mechanics is reimplemented here.
|
|
19
19
|
*/
|
|
20
20
|
import { spawn, execFile } from 'node:child_process';
|
|
21
|
-
import { existsSync } from 'node:fs';
|
|
21
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
22
22
|
import { hostname, networkInterfaces } from 'node:os';
|
|
23
23
|
import { join } from 'node:path';
|
|
24
24
|
import chalk from 'chalk';
|
|
@@ -103,6 +103,48 @@ function validateComposePath(path) {
|
|
|
103
103
|
throw new Error(`no docker-compose.yaml/.yml found in '${path}'. Pass --path pointing at a ROBOVOICE checkout ` +
|
|
104
104
|
`(the directory containing docker-compose.yaml), not this repo.`);
|
|
105
105
|
}
|
|
106
|
+
/** Persist the single-track greeting fix in a ROBOVOICE checkout. */
|
|
107
|
+
function ensureRobovoiceSingleAudioTrack(path) {
|
|
108
|
+
const candidates = [
|
|
109
|
+
join(path, 'voice_agent.py'),
|
|
110
|
+
join(path, 'app', 'voice_agent.py'),
|
|
111
|
+
join(path, 'src', 'voice_agent.py'),
|
|
112
|
+
];
|
|
113
|
+
const file = candidates.find((candidate) => existsSync(candidate));
|
|
114
|
+
if (!file) {
|
|
115
|
+
return { patched: false, warning: 'voice_agent.py was not found; skipping RoboPark greeting compatibility patch' };
|
|
116
|
+
}
|
|
117
|
+
const source = readFileSync(file, 'utf8');
|
|
118
|
+
if (source.includes('Initial greeting completed through AgentSession audio track')) {
|
|
119
|
+
return { patched: false, file };
|
|
120
|
+
}
|
|
121
|
+
const startMarker = ' # Synthesize the greeting first';
|
|
122
|
+
const endMarker = ' except Exception as e:';
|
|
123
|
+
const start = source.indexOf(startMarker);
|
|
124
|
+
const end = start >= 0 ? source.indexOf(endMarker, start) : -1;
|
|
125
|
+
if (start < 0 || end < 0) {
|
|
126
|
+
return {
|
|
127
|
+
patched: false,
|
|
128
|
+
file,
|
|
129
|
+
warning: 'voice_agent.py does not contain the known duplicate greeting-track block; no automatic patch applied',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const newline = source.includes('\r\n') ? '\r\n' : '\n';
|
|
133
|
+
const replacement = [
|
|
134
|
+
' # Use the existing conversation track. A dedicated greeting',
|
|
135
|
+
' # track makes Linux robots compete for the same ALSA device.',
|
|
136
|
+
' import inspect as _inspect',
|
|
137
|
+
' speech = session.say(greeting, allow_interruptions=False)',
|
|
138
|
+
' if _inspect.isawaitable(speech):',
|
|
139
|
+
' await speech',
|
|
140
|
+
" elif hasattr(speech, 'wait_for_playout'):",
|
|
141
|
+
' await speech.wait_for_playout()',
|
|
142
|
+
" logger.info('Initial greeting completed through AgentSession audio track')",
|
|
143
|
+
'',
|
|
144
|
+
].join(newline);
|
|
145
|
+
writeFileSync(file, source.slice(0, start) + replacement + source.slice(end), 'utf8');
|
|
146
|
+
return { patched: true, file };
|
|
147
|
+
}
|
|
106
148
|
export async function roboparkAgentUp(opts) {
|
|
107
149
|
console.log(chalk.bold('\n robopark agent up'));
|
|
108
150
|
console.log(chalk.dim(' ' + '─'.repeat(52)));
|
|
@@ -125,8 +167,18 @@ export async function roboparkAgentUp(opts) {
|
|
|
125
167
|
console.log(chalk.dim(` using: ${compose.cmd} ${compose.baseArgs.join(' ')}`.trimEnd()));
|
|
126
168
|
console.log(` path: ${chalk.cyan(opts.path)}`);
|
|
127
169
|
console.log();
|
|
170
|
+
const greetingPatch = ensureRobovoiceSingleAudioTrack(opts.path);
|
|
171
|
+
if (greetingPatch.patched) {
|
|
172
|
+
console.log(chalk.green(` ✓ persisted single-track RoboPark greeting in ${greetingPatch.file}`));
|
|
173
|
+
}
|
|
174
|
+
else if (greetingPatch.warning) {
|
|
175
|
+
console.log(chalk.yellow(` ⚠ ${greetingPatch.warning}`));
|
|
176
|
+
}
|
|
128
177
|
console.log(chalk.bold(' 1. starting ROBOVOICE docker stack (docker compose up -d)…'));
|
|
129
|
-
|
|
178
|
+
// Rebuild only when compatibility source changed, making the fix survive
|
|
179
|
+
// container recreation without slowing normal restarts.
|
|
180
|
+
const upArgs = greetingPatch.patched ? ['up', '-d', '--build'] : ['up', '-d'];
|
|
181
|
+
const upCode = await runCompose(compose, upArgs, opts.path);
|
|
130
182
|
if (upCode !== 0) {
|
|
131
183
|
console.log(chalk.red(`\n ✗ docker compose up -d exited with code ${upCode}`));
|
|
132
184
|
process.exit(upCode);
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# RoboPark Production Memory
|
|
2
|
+
|
|
3
|
+
Last verified: 2026-07-16
|
|
4
|
+
|
|
5
|
+
This records production facts future operators and coding agents must
|
|
6
|
+
preserve. Never add enrollment tokens, mesh tokens, API keys, or device tokens.
|
|
7
|
+
|
|
8
|
+
## Verified Architecture
|
|
9
|
+
|
|
10
|
+
- `livekit-1` is the Windows hub.
|
|
11
|
+
- Mesh/dashboard: TCP `47913`.
|
|
12
|
+
- RoboPark scheduler: TCP `8080`, proxied under `/robopark`.
|
|
13
|
+
- LiveKit: TCP `7880`.
|
|
14
|
+
- Production voice worker container: `caal-agent`.
|
|
15
|
+
- BMW maps to device `dev_0422e545` and character `vixen` / `VIXEN (BMW)`.
|
|
16
|
+
- Robots publish camera and microphone tracks to LiveKit. Park camera preview
|
|
17
|
+
uses the RoboVision relay rather than LiveKit video.
|
|
18
|
+
|
|
19
|
+
## BMW Launch Device Lock
|
|
20
|
+
|
|
21
|
+
- Camera: `/dev/video0` (`H65 USB CAMERA`).
|
|
22
|
+
- Microphone: scheduler ID `2`, `Usb Audio Device: USB Audio (hw:3,0)`.
|
|
23
|
+
- Speaker: scheduler ID `1`, `USB Audio Device: - (hw:2,0)`.
|
|
24
|
+
- Linux live playback must use ALSA `plughw:2,0` through `aplay`.
|
|
25
|
+
- Production mic gain defaults to `ROBOPARK_MIC_GAIN=4.0` and is bounded to
|
|
26
|
+
`1.0..12.0` before PCM frames enter LiveKit.
|
|
27
|
+
- RoboVision/sounddevice numeric IDs are not PyAudio numeric IDs.
|
|
28
|
+
- Do not use HDMI or metadata-only `/dev/video*` devices as launch defaults.
|
|
29
|
+
|
|
30
|
+
## Verified Production Flow
|
|
31
|
+
|
|
32
|
+
BMW passed these measured stages in one production session:
|
|
33
|
+
|
|
34
|
+
1. `scheduler_session`
|
|
35
|
+
2. `livekit_join`
|
|
36
|
+
3. `tts_subscribed`
|
|
37
|
+
4. `playback_started`
|
|
38
|
+
5. `camera_published`
|
|
39
|
+
6. `microphone_published`
|
|
40
|
+
7. `robot_online`
|
|
41
|
+
8. `camera_ready`
|
|
42
|
+
9. `microphone_ready`
|
|
43
|
+
10. `speaker_ready`
|
|
44
|
+
|
|
45
|
+
`playback_started` means the first remote TTS frame reached the robot. Never
|
|
46
|
+
report end-to-end audio delivery without this event.
|
|
47
|
+
|
|
48
|
+
## Root Causes Fixed
|
|
49
|
+
|
|
50
|
+
### Silent Greeting
|
|
51
|
+
|
|
52
|
+
The old ROBOVOICE greeting published a second dedicated LiveKit audio track.
|
|
53
|
+
The Pi subscribed to both tracks and attempted to open the same speaker twice.
|
|
54
|
+
ROBOVOICE must call `session.say(...)` so greeting and conversation share one
|
|
55
|
+
track. `robopark agent-up` now detects the old source block, patches the
|
|
56
|
+
ROBOVOICE checkout, and rebuilds Compose once.
|
|
57
|
+
|
|
58
|
+
### Linux Speaker Playback
|
|
59
|
+
|
|
60
|
+
The preview agent previously used PyAudio for the selected speaker. BMW's
|
|
61
|
+
proven output is ALSA `aplay -D plughw:2,0`. Infinicode `2.8.86` uses that
|
|
62
|
+
same endpoint for live TTS whenever a Linux output selection contains `hw:`.
|
|
63
|
+
|
|
64
|
+
### Silent Conversation After Greeting
|
|
65
|
+
|
|
66
|
+
Publishing a LiveKit microphone track does not prove that PCM capture started.
|
|
67
|
+
On Linux the production capture path is `arecord -D plughw:3,0 -f S16_LE -r
|
|
68
|
+
48000 -c 1`; it must work even when PyAudio is unavailable. The Pi now waits
|
|
69
|
+
for actual PCM frames before reporting `microphone_published`, reports a blocked
|
|
70
|
+
stage when capture does not start within five seconds, and applies bounded mic
|
|
71
|
+
gain before publication.
|
|
72
|
+
|
|
73
|
+
### Duplicate Speaker Tests
|
|
74
|
+
|
|
75
|
+
Only one robot process may claim a scheduler `speaker_test`. The scheduler
|
|
76
|
+
uses an atomic lease, and supervisor-status queries exclude speaker-test rows.
|
|
77
|
+
|
|
78
|
+
### BMW Alias Configuration
|
|
79
|
+
|
|
80
|
+
Scheduler routes canonicalize alias `bmw` to `dev_0422e545` before reading
|
|
81
|
+
character, voice-stack, and greeting configuration.
|
|
82
|
+
|
|
83
|
+
## Operational Rules
|
|
84
|
+
|
|
85
|
+
- Run one unified robot runtime per robot. Do not manually start preview,
|
|
86
|
+
vision, or mesh beside the systemd runtime.
|
|
87
|
+
- Do not run audio diagnostics during a live conversation.
|
|
88
|
+
- A direct ALSA test does not validate production if production uses another
|
|
89
|
+
API, index space, sample format, or opens the device twice.
|
|
90
|
+
- After a hub reboot, verify `47913`, `8080`, and `7880` before robot media.
|
|
91
|
+
- `tts_subscribed` alone is insufficient; require `playback_started`.
|
|
92
|
+
- Greeting `playback_started` alone does not prove conversation. A complete
|
|
93
|
+
turn also requires a user/STT event, an LLM response, and response playback.
|
|
94
|
+
- Container-local changes are temporary. Persist voice changes in the
|
|
95
|
+
ROBOVOICE checkout/image or use `robopark agent-up --path <checkout>`.
|
|
96
|
+
|
|
97
|
+
## Minimal Verification
|
|
98
|
+
|
|
99
|
+
Run from the hub:
|
|
100
|
+
|
|
101
|
+
```powershell
|
|
102
|
+
try { Invoke-RestMethod -Method Post http://127.0.0.1:8080/api/robots/bmw/pipeline-test/stop | Out-Null } catch {}
|
|
103
|
+
Start-Sleep 3
|
|
104
|
+
Invoke-RestMethod -Method Post http://127.0.0.1:8080/api/robots/bmw/pipeline-test/start | Out-Null
|
|
105
|
+
Start-Sleep 30
|
|
106
|
+
$s = Invoke-RestMethod http://127.0.0.1:8080/api/robots/bmw/pipeline-status
|
|
107
|
+
$id = $s.latest_session.id
|
|
108
|
+
$s.events | Where-Object session_id -eq $id | Select-Object timestamp,stage,status,message
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Required result: `playback_started ok`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infinicode",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.87",
|
|
4
4
|
"description": "OpenKernel — provider-agnostic AI execution kernel. Native coding agent + mission-driven execution runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/kernel/index.js",
|
|
@@ -29,8 +29,9 @@
|
|
|
29
29
|
"static",
|
|
30
30
|
"packages/robopark/scheduler",
|
|
31
31
|
"packages/robopark/pi-client",
|
|
32
|
-
"packages/robopark/vision",
|
|
33
|
-
".
|
|
32
|
+
"packages/robopark/vision",
|
|
33
|
+
"docs/ROBOPARK_PRODUCTION_MEMORY.md",
|
|
34
|
+
".opencode/plugins",
|
|
34
35
|
".opencode/themes",
|
|
35
36
|
".opencode/tui.json",
|
|
36
37
|
"README.md"
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
# robopark
|
|
1
|
+
# robopark
|
|
2
|
+
|
|
3
|
+
Production architecture, device locks, verified stages, and incident memory
|
|
4
|
+
are recorded in [`../../docs/ROBOPARK_PRODUCTION_MEMORY.md`](../../docs/ROBOPARK_PRODUCTION_MEMORY.md).
|
|
2
5
|
|
|
3
6
|
The **RoboPark fleet control CLI** — set up, watch, and drive a room full of
|
|
4
7
|
talking robots from one command. `robopark` is the operator front-end; it rides
|
|
@@ -1134,6 +1134,7 @@ class LiveKitPublisher:
|
|
|
1134
1134
|
self._tasks: list[asyncio.Task] = []
|
|
1135
1135
|
self._capture: Optional["VideoCapture"] = None
|
|
1136
1136
|
self._mic_capture: Optional["AudioCapture"] = None
|
|
1137
|
+
self._mic_streaming = asyncio.Event()
|
|
1137
1138
|
# Motion sampling state belongs to the publisher instance. Keeping it
|
|
1138
1139
|
# initialized here prevents shutdown/reopen paths from raising while
|
|
1139
1140
|
# the camera is being handed between preview and voice sessions.
|
|
@@ -1212,13 +1213,12 @@ class LiveKitPublisher:
|
|
|
1212
1213
|
self.video_source.capture_frame(first_frame)
|
|
1213
1214
|
await self.agent._report_pipeline("camera_published", "ok", "Camera track published")
|
|
1214
1215
|
|
|
1215
|
-
if audio_enabled:
|
|
1216
|
-
self.audio_source = self.rtc.AudioSource(48000, 1)
|
|
1217
|
-
self.audio_track = self.rtc.LocalAudioTrack.create_audio_track("microphone", self.audio_source)
|
|
1218
|
-
aopts = self.rtc.TrackPublishOptions()
|
|
1219
|
-
aopts.source = self.rtc.TrackSource.SOURCE_MICROPHONE
|
|
1216
|
+
if audio_enabled:
|
|
1217
|
+
self.audio_source = self.rtc.AudioSource(48000, 1)
|
|
1218
|
+
self.audio_track = self.rtc.LocalAudioTrack.create_audio_track("microphone", self.audio_source)
|
|
1219
|
+
aopts = self.rtc.TrackPublishOptions()
|
|
1220
|
+
aopts.source = self.rtc.TrackSource.SOURCE_MICROPHONE
|
|
1220
1221
|
await self.room.local_participant.publish_track(self.audio_track, aopts)
|
|
1221
|
-
await self.agent._report_pipeline("microphone_published", "ok", "Microphone track published")
|
|
1222
1222
|
|
|
1223
1223
|
# Register speaker playback (subscribe to the voice agent's TTS audio
|
|
1224
1224
|
# track) BEFORE opening the camera. Camera open is a slow/occasionally
|
|
@@ -1227,8 +1227,18 @@ class LiveKitPublisher:
|
|
|
1227
1227
|
# entire event loop — including receiving the agent's greeting audio
|
|
1228
1228
|
# — until it finished, so a short "Hello friend!" greeting could be
|
|
1229
1229
|
# over and gone before we ever got a chance to subscribe to it.
|
|
1230
|
-
if
|
|
1231
|
-
self._tasks.append(asyncio.create_task(self._audio_loop()))
|
|
1230
|
+
if audio_enabled:
|
|
1231
|
+
self._tasks.append(asyncio.create_task(self._audio_loop()))
|
|
1232
|
+
try:
|
|
1233
|
+
await asyncio.wait_for(self._mic_streaming.wait(), timeout=5.0)
|
|
1234
|
+
await self.agent._report_pipeline(
|
|
1235
|
+
"microphone_published", "ok", "Microphone track published with live PCM frames"
|
|
1236
|
+
)
|
|
1237
|
+
except asyncio.TimeoutError:
|
|
1238
|
+
await self.agent._report_pipeline(
|
|
1239
|
+
"microphone_published", "blocked", "Microphone track published but PCM capture did not start"
|
|
1240
|
+
)
|
|
1241
|
+
raise RuntimeError(f"microphone capture did not start for {self.agent.audio_capture_device}")
|
|
1232
1242
|
|
|
1233
1243
|
if self._capture and self.video_source:
|
|
1234
1244
|
self._tasks.append(asyncio.create_task(self._video_loop()))
|
|
@@ -1557,7 +1567,8 @@ class LiveKitPublisher:
|
|
|
1557
1567
|
samples_per_frame = int(48000 * frame_ms / 1000)
|
|
1558
1568
|
samples_per_batch = int(48000 * BATCH_MS / 1000)
|
|
1559
1569
|
bytes_per_frame = samples_per_frame * 2 # int16 mono
|
|
1560
|
-
import audioop
|
|
1570
|
+
import audioop
|
|
1571
|
+
mic_gain = max(1.0, min(float(os.getenv("ROBOPARK_MIC_GAIN", "4.0")), 12.0))
|
|
1561
1572
|
_diag_peak = 0
|
|
1562
1573
|
_diag_count = 0
|
|
1563
1574
|
_diag_last_log = time.monotonic()
|
|
@@ -1573,11 +1584,14 @@ class LiveKitPublisher:
|
|
|
1573
1584
|
data = bytes(batch.data)
|
|
1574
1585
|
_pub_start = time.monotonic()
|
|
1575
1586
|
for off in range(0, len(data) - bytes_per_frame + 1, bytes_per_frame):
|
|
1576
|
-
chunk = data[off:off + bytes_per_frame]
|
|
1577
|
-
|
|
1587
|
+
chunk = data[off:off + bytes_per_frame]
|
|
1588
|
+
if mic_gain != 1.0:
|
|
1589
|
+
chunk = audioop.mul(chunk, 2, mic_gain)
|
|
1590
|
+
frame = self.rtc.AudioFrame(
|
|
1578
1591
|
data=chunk, sample_rate=48000, num_channels=1, samples_per_channel=samples_per_frame,
|
|
1579
1592
|
)
|
|
1580
|
-
await self.audio_source.capture_frame(frame)
|
|
1593
|
+
await self.audio_source.capture_frame(frame)
|
|
1594
|
+
self._mic_streaming.set()
|
|
1581
1595
|
p = audioop.max(chunk, 2)
|
|
1582
1596
|
_diag_peak = max(_diag_peak, p)
|
|
1583
1597
|
_diag_count += 1
|