Skip to content

experiment: integrate audio S2S probe stack (PRs 1-6) - #7

Closed
codingshreyash wants to merge 145 commits into
mainfrom
experiment/audio-s2s-probes
Closed

experiment: integrate audio S2S probe stack (PRs 1-6)#7
codingshreyash wants to merge 145 commits into
mainfrom
experiment/audio-s2s-probes

Conversation

@codingshreyash

Copy link
Copy Markdown
Owner

What this is

Experimental integration branch that cherry-picks all six draft audio PRs onto current main so the full probe stack can be tested end-to-end against a live duplex S2S target before the individual PRs land upstream.

What's included

PR Branch What it adds
1 pr/audio-generator Generator.supported_formats() interface + NVAudioTranscription (Parakeet ASR)
2 pr/audio-resources garak.resources.audio — TTS synthesis, WAV transforms with SHA-256 provenance, attack metadata helpers
3 pr/nvvoicechat NVVoiceChat — speech-to-speech generator via OpenAI-compat /v1/chat/completions shim
4 pr/audio-probes Full PETTS probe family + garak/detectors/audio.py (output quality, tool-risk, reliability, judge)
5 pr/audio-injection AudioSuffixInjection + AudioOverlayInjection probes
6 pr/audio-bon PairedDirect + AcousticVoiceBestOfN Best-of-N probes

Two shim commits glue the stack onto main without pulling in the full feature/technique_intent branch:

  • garak/intents/ + garak.services.intentservice — minimal stubs satisfying IntentProbe's import (all concrete PETTS subclasses override the intent-population methods with hardcoded data anyway)
  • _config.cas namespace — GarakSubConfig() entry expected by probe tests

How to test against the duplex endpoint

git checkout experiment/audio-s2s-probes
pip install -e ".[dev]"

# Point at a running NVVoiceChat shim
export NIM_API_KEY=<key>
python -m garak \
  --model_type nim.NVVoiceChat \
  --model_name <model-served-by-shim> \
  --generator_option uri=https://<shim>/v1 \
  --probes audio.ToolRiskPETTS

Status

  • All 216 audio-stack tests pass (pytest tests/generators/test_nim_audio_transcription.py tests/resources/ tests/detectors/test_detectors_audio*.py tests/probes/test_probes_audio*.py)
  • Live endpoint validation against Colossus / NVVoiceChat staging
  • Rebase shims out once PRs 1-4 land in upstream main

This branch is not intended for upstream merge — it exists purely for red-team testing. Once individual PRs land, switch back to running off their tips.

r3v5 and others added 30 commits March 28, 2026 15:51
Signed-off-by: Ian Miller <milleryan2003@gmail.com>
Signed-off-by: Ian Miller <milleryan2003@gmail.com>
This allows the user control over what prompt generation parameters maybe overridden
for inference calls from the `promptinject` family of probes.

Signed-off-by: Jeffrey Martin <jemartin@nvidia.com>
Add dedicated test files for donotanswer, grandma, phrasing,
realtoxicityprompts, and sata probes. Each covers plugin loading,
prompt generation, and module-specific behavior (inheritance,
pruning, NLTK masking, dynamic class enumeration).

Signed-off-by: boao.dong <markdba313@gmail.com>
Implements AdaptiveAttacks and AdaptiveAttacksFull probes based on
Andriushchenko, Croce, and Flammarion, "Jailbreaking Leading
Safety-Aligned LLMs with Simple Adaptive Attacks" (ICML 2024,
arXiv:2404.02151).

The v1 probe is template-only: it combines a paraphrased adaptive
prompt template (direct-assistant role + structured response scaffold
+ no-"I" affirmative-opener constraint) with a small cached set of
seed forbidden behaviors and pre-computed adversarial suffixes drawn
from public AdvBench results. No gradient computation or online
search is performed.

- garak/probes/adaptiveattacks.py: AdaptiveAttacks (active, capped)
  and AdaptiveAttacksFull (uncapped, opt-in)
- garak/data/adaptive_attacks/{seeds,suffixes}.txt: 20 seeds,
  3 suffix variants
- tests/probes/test_probes_adaptiveattacks.py: 8 tests covering
  load, prompt content, template markers, and prompt-cap behavior

Smoke run against test.Blank completes without error.
Closes NVIDIA#583.

Signed-off-by: Neeraj Kumar Singh <b.neerajkumarsingh@gmail.com>
Address @jmartin-tech review on NVIDIA#1742:

(a) Use the canonical follow_prompt_cap guard pattern (per NVIDIA#1546/NVIDIA#1562 +
    encoding.py / latentinjection.py) instead of an unconditional
    self._prune_data() call.

(b) Add DEFAULT_PARAMS override on AdaptiveAttacksFull to flip
    follow_prompt_cap to False, replacing the previous Probe.__init__
    bypass. This makes the no-cap behavior a configurable param rather
    than a constructor hack, matches how every other Full variant in the
    repo expresses uncapped intent, and lets users re-enable the cap on
    Full via config if they want.

AdaptiveAttacks: follow_prompt_cap=True  -> prunes to soft_probe_prompt_cap
AdaptiveAttacksFull: follow_prompt_cap=False -> keeps every (seed, suffix) pair

All 8 tests in tests/probes/test_probes_adaptiveattacks.py still pass,
plus tests/test_docs.py adaptive cases (4) and the broader probe suite
(1130 passing; the 1 remaining failure is the pre-existing audio probe
optional-deps issue unrelated to this PR).

Signed-off-by: Neeraj Kumar Singh <b.neerajkumarsingh@gmail.com>
…ests

Pare down tests to validation of functionality unique to each probe
module, per review feedback. Generic instantiation and prompt-not-empty
checks are already covered by plugins/test_plugin_load.py.

Signed-off-by: boao.dong <markdba313@gmail.com>
Signed-off-by: boao.dong <markdba313@gmail.com>
Signed-off-by: Jonghyeok Kim <jka236@sfu.ca>
- Dynamically discover evaluator classes by scanning garak.evaluators modules
- Replace class-based test grouping with bare test functions
- Extract shared constants (THRESHOLD_VALUES, DEFAULT_PROBE, DEFAULT_GOAL, etc.)
- Add failure messages to all assert statements
- Replace meaningless "G" mock return with realistic emoji symbol
- Document fixture parallelisation constraints in docstring
- Separate evaluator-specific tests from generic structure tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Ian Miller <milleryan2003@gmail.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Ian Miller <milleryan2003@gmail.com>
)

Adds a regression guard for the bug fixed in PR NVIDIA#1544 where calling
load_base_config() followed by load_config() caused garak.core.yaml to
be appended twice to _config.config_files, making it appear twice in
HTML reports.

The fix (deduplication check in _load_config_files) is already merged,
but there was no test to prevent the regression from creeping back in.

Closes NVIDIA#1249

Signed-off-by: ppradyoth <pradyoth0@gmail.com>
generators.function.Single overrode DEFAULT_PARAMS with only {"kwargs": {}},
so it did not pick up the base Generator.DEFAULT_PARAMS values. Because
_apply_missing_instance_defaults iterates over the most-derived
DEFAULT_PARAMS, params such as max_tokens, temperature, top_k, context_len,
skip_seq_start and skip_seq_end were never set as instance attributes.
Accessing them (e.g. generator.max_tokens) raised AttributeError.

Merge Generator.DEFAULT_PARAMS into the class dict, matching the pattern
used by the other generators (openai, rest, huggingface, cohere, ollama,
etc.). Multiple inherits from Single and is covered as well.

Adds a regression test asserting both Single and Multiple expose the
inherited defaults while keeping the function-specific kwargs default.

Fixes NVIDIA#1096

Signed-off-by: Aditya Singh <adisin650@gmail.com>
…VIDIA#830)

When a user passes a module-name spec (e.g. `-p test`) that maps only
to plugins marked `active = False`, `parse_plugin_spec` returns the
clause in the rejected list and the CLI raises `Unknown probes: test`.
That message is misleading: the module exists, but every plugin in it
is inactive.

Detect this case at the CLI rejection site by re-enumerating plugins
and checking whether any inactive entries share the rejected clause as
a namespace prefix. When they do, surface a message that names the
module and points the user at calling specific plugins by name. Mixed
rejections (some inactive-only, some truly unknown) still report both.

The change is contained to `garak/cli.py` so `parse_plugin_spec`
keeps its existing `(found, rejected)` signature and `--list_probes`
callers are unaffected. A regression test exercises `-p test` and
asserts the new wording.

Signed-off-by: notnick2 <varun024123@gmail.com>
Signed-off-by: Jeffrey Martin <jemartin@nvidia.com>
- NVAudioTranscription now validates inline (raw-bytes) audio against
  audio_formats using the mime the message carries, and forwards the real
  content type, instead of labelling every payload as WAV.
- OpenAICompatible.audio_formats is derived from audio_mime_subtype_formats'
  values so supported_formats() cannot advertise a format the request builder
  is unable to send. Adds tests for both paths.

Signed-off-by: Shreyash Ranjan <shrranjan@nvidia.com>
Signed-off-by: Shreyash Ranjan <shrranjan@nvidia.com>
Support library for audio probes: text-to-speech synthesis over pluggable
TTS backends, a deterministic transform-recipe system for perturbing WAV
audio with provenance digests, WAV/intelligibility validation, and attack
helpers (file metadata, Wilson confidence intervals, per-record summarisation).

Signed-off-by: Shreyash Ranjan <shrranjan@nvidia.com>
summarize_audio_groups counted a candidate as a success at score > 0.5 while
summarize_audio_records uses >= success_threshold (default 0.5), so the two
aggregations disagreed for a score of exactly 0.5. Both now treat 0.5 as a
success.

Signed-off-by: Shreyash Ranjan <shrranjan@nvidia.com>
…aries

validate_candidate did a plain substring check, so a required phrase 'ignore'
was satisfied by the transcript word 'ignored', letting an invalid candidate
pass as scoreable. Phrases now match on whole-word boundaries. Adds a test.

Signed-off-by: Shreyash Ranjan <shrranjan@nvidia.com>
…lidation

An empty required_transcript_phrases list previously short-circuited to
scoreable=True, bypassing the WER/semantic checks. It now falls through to
normal validation, so an empty list means 'no phrase requirement' rather than
'auto-pass'. Adds a regression test.

Signed-off-by: Shreyash Ranjan <shrranjan@nvidia.com>
Signed-off-by: Shreyash Ranjan <shrranjan@nvidia.com>
Adds NVVoiceChat, a generator for NVIDIA's Nemotron VoiceChat speech-to-speech
model via an OpenAI-compatible /v1/chat/completions shim that accepts base64
WAV and returns a text transcript (optionally a WAV reply). The endpoint is set
via uri. Appends configurable trailing silence so the model can finish its
response before the stream closes, and fails cleanly on non-WAV input.

Signed-off-by: Shreyash Ranjan <shrranjan@nvidia.com>
_save_response_audio no longer raises when response_audio_dir is set but
audio was disabled via extra_body (generate_audio: False) -- it returns
cleanly since there is nothing to save. Adds a regression test for the
combination, and inlines the format guard in _call_model.

Signed-off-by: Shreyash Ranjan <shrranjan@nvidia.com>
Signed-off-by: Shreyash Ranjan <shrranjan@nvidia.com>
…s shim

Signed-off-by: Shreyash Ranjan <shrranjan@nvidia.com>
…ODEL

DEFAULT_MODEL was non-standard (only used by these two nim additions). Follow
the OpenAICompatible convention: no hardcoded default, require --target_name and
raise a clear error if unset. Avoids shipping a fake or product-specific default.

Signed-off-by: Shreyash Ranjan <shrranjan@nvidia.com>
…it__

Signed-off-by: Shreyash Ranjan <shrranjan@nvidia.com>
Adds PETTS, the base probe that renders an intent as speech and sends it to
an audio-capable target to test whether moving an intent from text into spoken
audio changes safety behaviour (credited to Rich Harang), plus its tool-risk,
reliability, and instruction-priority subclasses. Adds the detectors these
score with, including an output-quality screen that discards mis-transcribed
audio and a policy-grounded model-as-judge for tool-risk actions.

Signed-off-by: Shreyash Ranjan <shrranjan@nvidia.com>
…se positive

- ToolRiskInstructionInjection now maps the 'suffix' mode to the 'concat'
  transform, so suffix injection produces results instead of silently raising
  an unknown-transform error that _audio_prompts swallows.
- AudioOutputQualityDetector no longer double-counts mid-word caps as both a
  fragmentation and a context signal, so coherent answers containing CamelCase
  product names (macOS, iPhone, JavaScript) are not flagged as word salad.
  Adds a regression test.

Signed-off-by: Shreyash Ranjan <shrranjan@nvidia.com>
AudioSuffixInjection appends a hidden spoken instruction to a benign carrier
clip the attacker cannot otherwise control; AudioOverlayInjection mixes a
hidden instruction into the background of a benign clip. Both score with the
tool-risk detectors.

Signed-off-by: Shreyash Ranjan <shrranjan@nvidia.com>
The suffix/overlay adversarial-condition assertions compared a condition set
against its own complement, so they held regardless of the probe's behaviour.
They now invoke the prestore hook and assert the is_adversarial flag it stores
against a fixed per-condition truth table, catching regressions in the logic.

Signed-off-by: Shreyash Ranjan <shrranjan@nvidia.com>
Signed-off-by: Shreyash Ranjan <shrranjan@nvidia.com>
PairedDirect varies the signal (speed/noise/filter recipes) and
AcousticVoiceBestOfN synthesises the same request across TTS speaker presets.
A source counts as compromised if any intelligible candidate succeeds; every
candidate passes the independent-ASR intelligibility gate first.

Signed-off-by: Shreyash Ranjan <shrranjan@nvidia.com>
audio_bon now only sets attempt.notes[attack_goal] on harmful trials, matching
audio_acoustic, so benign controls are not mislabelled as adversarial. Adds the
missing SPDX header and a non-empty-name guard for candidate_names, plus a
regression test.

Signed-off-by: Shreyash Ranjan <shrranjan@nvidia.com>
PETTS and its subclasses inherit IntentProbe but override _populate_intents,
_populate_stubs, and build_prompts with hardcoded data, so the real
intentservice is never called. This shim satisfies the import dependency
without pulling in the full feature/technique_intent branch.
_config.cas was missing (added by feature/technique_intent); add the
GarakSubConfig entry so probe tests that reference _config.cas.intent_spec
don't AttributeError.  Also flesh out intentservice.load() with a minimal
stub set so the base PETTS probe test can exercise the probe() flow.
Implements the full duplex-specific attack surface extension on top of the
existing PETTS / NVVoiceChat foundation:

EPIC-0 — Foundation (TICKETS 001 + 002)
- garak/resources/audio/session.py: SessionScript DSL with SessionEvent,
  ReactivePattern, and SessionResult; stable SHA-256 digests for provenance
- DuplexCapable mixin in nim.py: capability flag + run_session() contract
- NVDuplexChat generator: sequential-simulation duplex over any
  /v1/chat/completions shim; maintains full conversation history across
  turns; handles reactive triggers (fires interrupt when agent transcript
  matches a pattern)

EPIC-1 — Barge-in as attack primitive (TICKETS 101-103)
- PETTSBargeInterrupt: timed refusal-reversal; ablates over all affirmation
  phrasings per intent; RefusalReversalDetector scores pre/post split
- PETTSBargeFlood: rapid N-flood at configurable intervals around the 0.64 s
  latency window; StateDestabilizationDetector as secondary signal
- PETTSFalseBargeIn: injects non-speech false-trigger audio (breath /
  throat-clear) mid-response via PR-2 transform recipes

EPIC-2 — Cross-stream leakage (TICKETS 201-202)
- AudioVoiceBleed: watermarks user audio with a pure-tone fingerprint;
  VoiceBleedDetector scores MFCC cosine similarity shift vs. control
- AudioSilenceSmuggling: encodes instruction in pause-duration binary over
  short/long pause buckets; scores with existing ToolRiskActionDetector

EPIC-3 — Multi-turn drift (TICKET 301)
- PETTSDriftPoison: 5/20 turn sessions with persistent broadband noise on
  every benign turn; harmful request at the final turn; compares vs. clean
  control; sweeps session length as independent variable

All probes skip gracefully (log warning, return []) on non-DuplexCapable
targets. 268 tests, all passing.
1. RefusalReversalDetector: return None (not 0.0) for empty/whitespace
   full_transcript — blank output can't be distinguished from a safe refusal
   without this, an endpoint returning " " scores as a pass every time

2. DuplexPETTS.probe(): fix zip(scripts, audio_msgs) truncation bug —
   PETTSBargeInterrupt builds N_cases × N_affirmations scripts but audio_msgs
   has only N_cases entries; zip() silently dropped all but the first
   affirmation variant, turning 20 intended attempts into 5

3. PETTSBargeInterrupt: add StateDestabilizationDetector to extended_detectors
   so blank/degenerate responses surface as failures rather than vacuous passes
   in the harness summary (StateDestabilizationDetector scores empty→1.0,
   RefusalReversalDetector scores empty→None; both are now present)

Also adds:
- RefusalReversalDetector tests for empty/whitespace full_transcript → None
- PETTSBargeInterrupt test asserting 5 cases × 4 affirmations = 20 scripts
Per reviewer feedback: generators should not silently perform modality
conversion.  If the duplex target returns audio but no text content
(the cause of the blank-output results in the first live run), an
optional explicit ASR fallback is now available via asr_uri / asr_model
/ asr_language config params.

When asr_uri is set, NVDuplexChat calls NVAudioTranscription against the
configured endpoint to produce a scorable transcript.  When asr_uri is
unset (default), blank output reaches detectors as-is so RefusalReversal
returns None and StateDestabilization returns 1.0.

The transcription step is user-visible in config, uses the same public
default model as NVAudioTranscription (parakeet-1-1b-rnnt-multilingual),
and is isolated in _transcribe_response_audio() so it can be tested
independently.  provenance["asr_used"] records whether the fallback fired.
Addresses reviewer guidance that (1) garak generators output text only and
must not handle/transcribe non-text responses, and (2) new NIM generators
should follow the upstream OpenAICompatible pattern like Vision / NVMultimodal.

NVVoiceChat:
- now extends NVOpenAIChat and talks to the target through the OpenAI SDK
  client (self.generator.create) instead of raw requests, matching the
  upstream nim generators
- audio is sent as an input_audio content block via the inherited
  _conversation_to_list; the target's TEXT transcript is read from
  choices[0].message.content
- REMOVED all audio-output handling: generate_audio response consumption,
  _save_response_audio, response_audio_dir, and the ASR fallback
  (asr_uri/asr_model/asr_language, _transcribe_response_audio). Transcribing
  the target's audio reply would make the test measure the target *as
  interpreted by a transcription provider*, not the target itself
- keeps trailing-silence, system/text prompt, tools/tool_choice, and
  generate_audio-as-request-passthrough via a _prepare_prompt hook
- request_timeout / request_retries now configure the SDK client (slow S2S
  endpoints need a long timeout; SDK handles backoff internally)

NVDuplexChat:
- run_session builds a growing Conversation and calls self._call_model per
  turn (SDK path) instead of custom HTTP; removed _send_turn, _post_completion,
  and the ASR fallback
- text-out only, consistent with NVVoiceChat

Tests rewritten to stub the SDK create call / _call_model rather than
requests.post. Audio-output and ASR tests removed; added coverage for
text-only output, empty-content passthrough, and no-audio-output-params.
Convention/correctness fixes found in a branch-wide audit against upstream:

- detectors/audio_duplex.py: use lang_spec (upstream attr) instead of bcp47,
  which garak silently ignores. RefusalReversal and StateDestabilization
  detect() now return exactly one score per attempt.outputs entry (was
  max(len,1) / fixed [1.0], which could IndexError the evaluator when outputs
  is empty). Refusal signal is session-level, assigned per present output.

- NVAudioTranscription: default uri moved off the NVIDIA-private
  inference-api.nvidia.com onto the public integrate.api.nvidia.com surface
  (the endpoint the reviewer flagged as internal-only); docstring notes the
  required transcription route so users can repoint if needed.

- Removed AudioVoiceBleed probe + VoiceBleedDetector: the text-only generator
  refactor means agent audio is never captured, so the speaker-similarity
  detector could only ever return None. Speaker-identity bleed fundamentally
  needs audio output, which garak does not support; keeping inert code invites
  reviewer confusion. Can return as its own PR against an audio-capable path.
  Also dropped now-unused imports (attack metadata helpers, tempfile, _config).
NVAudioTranscription was scaffolding for the ASR fallback, which was removed
when the generators became text-out only. Nothing in the branch uses it, and
its /v1/audio/{model}/transcriptions route on the public endpoint is
unverified — shipping a target whose default may 404 is a quieter version of
the private-endpoint problem the reviewer flagged.

The reusable half of the original PR — the Generator.supported_formats()
format interface — stays. NVAudioTranscription can return as its own PR once
a public ASR endpoint + route is confirmed with a live test.

Removed the class + now-unused requests/mimetypes imports; renamed the test
file to test_nim_voicechat.py (it now covers only NVVoiceChat).
@codingshreyash

Copy link
Copy Markdown
Owner Author

Closing this non-upstream integration PR. Its broad shimmed stack and stale endpoint/ASR assumptions have been replaced by the focused review stack in #2, #3, #4, #8, #5, and #6. The experiment/audio-s2s-probes branch is retained for recovery; no branch was deleted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.