Skip to content

Commit 1b677c2

Browse files
adamweeksAdam Weeks
andauthored
feat(gecx): improve mu-law output quality (#46)
* feat(gecx): improve mu-law output quality * fix(gecx): suppress converted quiet lead frames * test(gecx): cover autonomous PCM output * fix(release): include generated gRPC modules --------- Co-authored-by: Adam Weeks <adweeks+cisco@cisco.com>
1 parent 472c8c8 commit 1b677c2

20 files changed

Lines changed: 689 additions & 100 deletions

AGENTS.MD

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,10 @@ allowlist:
404404
scripts/build-runtime-release.sh --ref HEAD --output /tmp/byova-gateway-runtime.tar.gz
405405
```
406406

407+
The builder must regenerate the Python gRPC modules from the selected ref's protobuf
408+
definitions and include them under `src/generated/`. Run it with `grpcio-tools` available
409+
from `requirements.txt`; never rely on ignored workstation-generated modules.
410+
407411
Do not deploy or archive the repository root. In particular, `tools/byova_e2e/`,
408412
`tools/voice_agent_lab/`, `tools/gecx_audio_lab/`, `tests/`, `docs/`,
409413
`requirements-dev.txt`, npm manifests and lockfiles, and AppleDouble files

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ The sample includes:
3636
- A configuration-driven connector router
3737
- Local audio, AWS Lex, and Google CX Agent Studio connectors
3838
- Immediate 8 kHz mu-law BYOVA `CHUNK` output for Google CX Agent Studio,
39+
either requested directly from CES or converted once from 24 kHz Linear16,
3940
with guarded suppression of anomalously long low-energy pre-roll and one
4041
ordered `FINAL` per completed normal or terminal turn
4142
- Direct delivery of autonomous CES no-input prompts to the active WxCC stream;
@@ -216,8 +217,9 @@ production without the controls described in the production guide.
216217
- **AWS Lex**: Connects to Amazon Lex V2 through the standard AWS SDK credential chain.
217218
- **Google CX Agent Studio**: Streams caller audio to Gemini Enterprise for Customer
218219
Experience through CES `BidiRunSession`. Caller speech starts an isolated response turn,
219-
so an overlapping CES no-input prompt cannot consume the caller's post-input reply. See the
220-
[GECX Setup Guide](docs/guides/byova-gecx-setup.md).
220+
so an overlapping CES no-input prompt cannot consume the caller's post-input reply. The
221+
connector can request 24 kHz Linear16 synthesis and perform the final anti-aliased 8 kHz
222+
mu-law conversion locally. See the [GECX Setup Guide](docs/guides/byova-gecx-setup.md).
221223

222224
Connectors implement `IVendorConnector` and are loaded from `config/config.yaml`. See the
223225
[Connector Guide](src/connectors/README.md) for the interface contract and extension pattern.
@@ -260,6 +262,11 @@ metadata, `audio/`, `config/`, `proto/`, and `src/`. It deliberately excludes `t
260262
`tests/`, `docs/`, `requirements-dev.txt`, JavaScript package manifests and lockfiles, and
261263
macOS AppleDouble files.
262264

265+
The builder regenerates every Python gRPC module from the selected ref's `proto/*.proto`
266+
files and includes those modules under `src/generated/`. Run it from an environment where
267+
`requirements.txt` is installed so `grpcio-tools` is available; an archive missing any
268+
required generated module fails validation before it can be deployed.
269+
263270
`tools/byova_e2e/`, `tools/voice_agent_lab/`, and its `tools/gecx_audio_lab/` compatibility
264271
implementation are local validation utilities. Their browser assets and development
265272
dependencies are installed through `requirements-dev.txt` and must not be copied to an EC2

config/README.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,10 @@ and end-to-end sandbox test paths.
118118
### GECX / CX Agent Studio Connector
119119

120120
The GECX connector streams WxCC caller audio to Google CX Agent Studio through the CES
121-
`BidiRunSession` API. CES 8 kHz mu-law output frames are forwarded immediately
122-
as raw BYOVA `CHUNK` responses, followed by exactly one normal or terminal
123-
`FINAL`.
121+
`BidiRunSession` API. It can request 24 kHz Linear16 output and perform the final
122+
anti-aliased 8 kHz mu-law conversion locally, or forward CES 8 kHz mu-law output
123+
directly. Both paths emit immediate BYOVA `CHUNK` responses followed by exactly
124+
one normal or terminal `FINAL`.
124125

125126
```yaml
126127
connectors:
@@ -135,8 +136,10 @@ connectors:
135136
language_code: "en-US"
136137
input_sample_rate_hertz: 8000
137138
input_audio_encoding: "MULAW"
138-
output_sample_rate_hertz: 8000
139-
output_audio_encoding: "MULAW"
139+
# Recommended quality path. Use 8000/MULAW instead to compare direct CES
140+
# mu-law output without connector-side conversion.
141+
output_sample_rate_hertz: 24000
142+
output_audio_encoding: "LINEAR16"
140143
suppress_long_leading_audio: true
141144
output_leading_audio_min_ms: 5000
142145
output_speech_rms_threshold: 200
@@ -164,9 +167,10 @@ connectors:
164167
- "My GECX Agent"
165168
```
166169

167-
GECX CHUNK output currently requires `output_sample_rate_hertz: 8000` and
168-
`output_audio_encoding: "MULAW"`. Unsupported output combinations fail during
169-
connector initialization; broader output formats are not silently mislabeled.
170+
GECX accepts two explicit provider-output pairs: `24000`/`LINEAR16` for the
171+
recommended connector-side conversion path, or `8000`/`MULAW` for direct CES
172+
mu-law. WxCC always receives 8 kHz mu-law; unsupported provider-output pairs
173+
fail during connector initialization rather than being silently mislabeled.
170174
The leading-audio guard activates only when the first CES frame is at least
171175
`output_leading_audio_min_ms` and contains no sustained speech. It then retains
172176
`output_speech_preroll_ms` before the first detected speech frames.

config/config.cloudrun.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ connectors:
5050
language_code: "en-US"
5151
input_sample_rate_hertz: 8000
5252
input_audio_encoding: "MULAW"
53-
# Required by the current raw BYOVA CHUNK output path.
54-
output_sample_rate_hertz: 8000
55-
output_audio_encoding: "MULAW"
53+
# Recommended quality path. Use 8000/MULAW for direct CES mu-law.
54+
output_sample_rate_hertz: 24000
55+
output_audio_encoding: "LINEAR16"
5656
suppress_long_leading_audio: true
5757
output_leading_audio_min_ms: 5000
5858
output_speech_rms_threshold: 200

config/config.yaml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,9 @@ connectors:
9696
# language_code: "en-US"
9797
# input_sample_rate_hertz: 8000
9898
# input_audio_encoding: "MULAW"
99-
# output_sample_rate_hertz: 8000
100-
# output_audio_encoding: "MULAW"
99+
# # Recommended quality path. Use 8000/MULAW for direct CES mu-law.
100+
# output_sample_rate_hertz: 24000
101+
# output_audio_encoding: "LINEAR16"
101102
# # Suppress only anomalously long low-energy CES pre-roll while retaining
102103
# # 100ms before detected prompt speech. Normal short frames pass through.
103104
# suppress_long_leading_audio: true

config/gecx_example.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,13 @@ gecx_connector:
2929

3030
language_code: "en-US"
3131

32-
# Input can be normalized from WxCC metadata. The current raw BYOVA CHUNK
33-
# output path requires exactly 8 kHz MULAW; other output combinations fail
34-
# connector initialization until explicit format support is added.
32+
# Input can be normalized from WxCC metadata. For output, request CES's
33+
# highest-quality PCM and let the connector perform the final anti-aliased
34+
# 8 kHz mu-law conversion. Use 8000/MULAW for direct CES mu-law instead.
3535
input_sample_rate_hertz: 8000
3636
input_audio_encoding: "MULAW"
37-
output_sample_rate_hertz: 8000
38-
output_audio_encoding: "MULAW"
37+
output_sample_rate_hertz: 24000
38+
output_audio_encoding: "LINEAR16"
3939

4040
# Guard against a provider returning a multi-second low-energy audio asset
4141
# before synthesized speech. Short CES frames remain on the direct path.

docs/PRODUCTION_READINESS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,9 @@ other obligations that apply to the contact center.
425425
- Build gateway runtime archives with `scripts/build-runtime-release.sh`. Do not deploy the
426426
repository root: local tools, tests, documentation, `requirements-dev.txt`, JavaScript
427427
manifests and lockfiles, and workstation metadata are not runtime components and must not
428-
reach gateway hosts.
428+
reach gateway hosts. The builder must regenerate and validate the Python gRPC modules from
429+
the selected ref's protobuf definitions so the immutable artifact is independently
430+
startable and does not depend on ignored workstation files.
429431
- Run formatting, linting, type checks, unit tests, integration tests, dependency scans,
430432
secret scans, and artifact/image scans on every change.
431433
- Generate gRPC code deterministically and detect incompatible protocol changes.

docs/guides/byova-gecx-setup.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,9 @@ gecx_connector:
8888
language_code: "en-US"
8989
input_sample_rate_hertz: 8000
9090
input_audio_encoding: "MULAW"
91-
output_sample_rate_hertz: 8000
92-
output_audio_encoding: "MULAW"
91+
# Recommended quality path. Use 8000/MULAW for direct CES mu-law.
92+
output_sample_rate_hertz: 24000
93+
output_audio_encoding: "LINEAR16"
9394
suppress_long_leading_audio: true
9495
output_leading_audio_min_ms: 5000
9596
output_speech_rms_threshold: 200
@@ -305,11 +306,11 @@ A terminal turn uses the same audio chunks, followed by one `FINAL` carrying
305306
`TRANSFER_TO_AGENT` or `SESSION_END`. The initial greeting uses the same
306307
`CHUNK`/`FINAL` pipeline.
307308

308-
The current CHUNK path intentionally supports only 8 kHz mu-law output.
309-
`output_audio_encoding` must remain `MULAW` and
310-
`output_sample_rate_hertz` must remain `8000`; unsupported combinations fail
311-
configuration early. Broader output-format support requires explicit
312-
conversion and validation.
309+
The WxCC CHUNK path intentionally remains 8 kHz mu-law. The provider output can
310+
be `LINEAR16` at `24000` Hz, which the connector anti-alias filters, downsamples,
311+
and mu-law encodes once, or `MULAW` at `8000` Hz for direct CES mu-law. The
312+
24 kHz Linear16 path is recommended when CES direct mu-law sounds quiet or
313+
degraded. Unsupported provider-output pairs fail configuration early.
313314

314315
Gateway `START_OF_INPUT` discards already-buffered autonomous prompt output and
315316
isolates CES output until CES recognizes the caller audio. This prevents the
@@ -428,8 +429,8 @@ window for an `EndSession` that follows the final TTS frames.
428429
| `initial_message` | No | Text sent when the CES stream opens (default: `Hello`) |
429430
| `enable_partial_responses` | No | Request CES text streaming for logs, terminal-cue detection, and text-only fallback |
430431
| `barge_in_enabled` | No | Allow interruption of autonomous/no-input prompt playback (default: `false`; greeting and caller-triggered replies always remain non-bargeable) |
431-
| `output_sample_rate_hertz` | No | Must be `8000` for the current raw CHUNK path |
432-
| `output_audio_encoding` | No | Must be `MULAW` for the current raw CHUNK path |
432+
| `output_sample_rate_hertz` | No | CES output rate: `24000` with `LINEAR16` (recommended) or `8000` with `MULAW` (default) |
433+
| `output_audio_encoding` | No | CES output codec: `LINEAR16` for connector-side conversion or `MULAW` for direct CES output |
433434
| `suppress_long_leading_audio` | No | Guard anomalously long low-energy CES output before prompt speech (default: `true`) |
434435
| `output_leading_audio_min_ms` | No | Minimum first-frame duration that can activate the guard (default: `5000`) |
435436
| `output_speech_rms_threshold` | No | 16-bit linear RMS threshold used to identify speech in decoded mu-law frames (default: `200`) |

scripts/build-runtime-release.sh

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,15 @@ runtime_paths=(
6868
src
6969
)
7070

71+
required_generated_paths=(
72+
src/generated/byova_common_pb2.py
73+
src/generated/byova_common_pb2_grpc.py
74+
src/generated/health_pb2.py
75+
src/generated/health_pb2_grpc.py
76+
src/generated/voicevirtualagent_pb2.py
77+
src/generated/voicevirtualagent_pb2_grpc.py
78+
)
79+
7180
for path in "${required_paths[@]}"; do
7281
if ! git cat-file -e "${resolved_ref}:${path}" 2>/dev/null; then
7382
echo "error: required runtime path is missing at ${ref}: ${path}" >&2
@@ -86,10 +95,73 @@ mkdir -p "$(dirname "$output")"
8695
output_dir="$(cd "$(dirname "$output")" && pwd -P)"
8796
output_path="${output_dir}/$(basename "$output")"
8897
temporary_archive="$(mktemp "${TMPDIR:-/tmp}/byova-runtime-release.XXXXXX")"
89-
trap 'rm -f "$temporary_archive"' EXIT
98+
staging_root="$(mktemp -d "${TMPDIR:-/tmp}/byova-runtime-staging.XXXXXX")"
99+
100+
cleanup() {
101+
rm -f "$temporary_archive"
102+
rm -rf "$staging_root"
103+
}
104+
trap cleanup EXIT
90105

91106
git archive --format=tar "$resolved_ref" -- "${archive_paths[@]}" \
92-
| gzip -n > "$temporary_archive"
107+
| tar -xf - -C "$staging_root"
108+
109+
protoc_python=""
110+
protoc_candidates=()
111+
if [[ -n "${PYTHON:-}" ]]; then
112+
protoc_candidates+=("$PYTHON")
113+
fi
114+
protoc_candidates+=(
115+
"$repo_root/.venv/bin/python"
116+
"$repo_root/venv/bin/python"
117+
python3
118+
python
119+
)
120+
121+
for candidate in "${protoc_candidates[@]}"; do
122+
if [[ "$candidate" == */* ]]; then
123+
[[ -x "$candidate" ]] || continue
124+
elif ! command -v "$candidate" >/dev/null 2>&1; then
125+
continue
126+
fi
127+
if "$candidate" -c 'import grpc_tools.protoc' >/dev/null 2>&1; then
128+
protoc_python="$candidate"
129+
break
130+
fi
131+
done
132+
133+
if [[ -z "$protoc_python" ]]; then
134+
echo "error: grpcio-tools is required to build runtime protobuf modules" >&2
135+
exit 1
136+
fi
137+
138+
proto_files=()
139+
while IFS= read -r proto_file; do
140+
proto_files+=("$proto_file")
141+
done < <(find "$staging_root/proto" -maxdepth 1 -type f -name '*.proto' -print | sort)
142+
143+
if [[ "${#proto_files[@]}" -eq 0 ]]; then
144+
echo "error: runtime release contains no protobuf definitions" >&2
145+
exit 1
146+
fi
147+
148+
"$protoc_python" -m grpc_tools.protoc \
149+
-I"$staging_root/proto" \
150+
--python_out="$staging_root/src/generated" \
151+
--grpc_python_out="$staging_root/src/generated" \
152+
"${proto_files[@]}"
153+
154+
for path in "${required_generated_paths[@]}"; do
155+
if [[ ! -f "$staging_root/$path" ]]; then
156+
echo "error: generated runtime module is missing: $path" >&2
157+
exit 1
158+
fi
159+
done
160+
161+
(
162+
cd "$staging_root"
163+
COPYFILE_DISABLE=1 tar -cf - "${archive_paths[@]}"
164+
) | gzip -n > "$temporary_archive"
93165

94166
archive_listing="$(tar -tzf "$temporary_archive")"
95167
for path in "${required_paths[@]}"; do
@@ -98,6 +170,12 @@ for path in "${required_paths[@]}"; do
98170
exit 1
99171
fi
100172
done
173+
for path in "${required_generated_paths[@]}"; do
174+
if ! grep -Eq "^${path}$" <<<"$archive_listing"; then
175+
echo "error: runtime archive is missing generated module: $path" >&2
176+
exit 1
177+
fi
178+
done
101179

102180
forbidden_pattern='(^|/)(tools|tests|docs)(/|$)|(^|/)(requirements-dev.txt|package.json|package-lock.json|npm-shrinkwrap.json|yarn.lock|pnpm-lock.yaml)$|(^|/)\._'
103181
if grep -Eq "$forbidden_pattern" <<<"$archive_listing"; then
@@ -107,6 +185,7 @@ if grep -Eq "$forbidden_pattern" <<<"$archive_listing"; then
107185
fi
108186

109187
mv "$temporary_archive" "$output_path"
188+
cleanup
110189
trap - EXIT
111190

112191
if command -v sha256sum >/dev/null 2>&1; then

src/connectors/README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,8 @@ Experience) through the CES `BidiRunSession` API.
164164
suppressing an overlapping stale no-input prompt without dropping the reply
165165
- Can report current-turn CES recognition to the gateway so an opt-in,
166166
recognition-assisted policy shortens only the cancellable post-VAD grace
167-
- Streams 8 kHz mu-law CES output as BYOVA `CHUNK` responses
167+
- Streams WxCC-compatible 8 kHz mu-law as BYOVA `CHUNK` responses, either
168+
directly from CES or converted once from CES 24 kHz Linear16 output
168169
- Pushes autonomous CES no-input prompts directly to the active WxCC stream and
169170
optionally enables barge-in for those open prompts (disabled by default)
170171
- Suppresses only a long low-energy prefix before detected prompt speech;
@@ -191,8 +192,10 @@ gecx_connector:
191192
# deployment_id: "YOUR_DEPLOYMENT_ID"
192193
input_sample_rate_hertz: 8000
193194
input_audio_encoding: "MULAW"
194-
output_sample_rate_hertz: 8000
195-
output_audio_encoding: "MULAW"
195+
# Recommended quality path: request high-quality CES output, then perform
196+
# the final anti-aliased 8 kHz mu-law conversion in the connector.
197+
output_sample_rate_hertz: 24000
198+
output_audio_encoding: "LINEAR16"
196199
suppress_long_leading_audio: true
197200
output_leading_audio_min_ms: 5000
198201
output_speech_rms_threshold: 200

0 commit comments

Comments
 (0)