Skip to content

feat: add audio + audio-video analysis tools (qwen3.5-omni) - #6

Merged
sommio merged 1 commit into
developfrom
feat/omni-audio-tools
Jul 29, 2026
Merged

feat: add audio + audio-video analysis tools (qwen3.5-omni)#6
sommio merged 1 commit into
developfrom
feat/omni-audio-tools

Conversation

@sommio

@sommio sommio commented Jul 29, 2026

Copy link
Copy Markdown
Owner

What

Adds two MCP tools on qwen3.5-omni-plus, separate from the existing qwen3.7-plus video/image tools:

  • analyze_audio — analyze an audio file (URL or local), mp3/wav/flac/ogg/m4a/aac
  • analyze_audio_video — analyze a video's visuals and its sound track (URL or local)

Both accept a public URL or local file path, with a custom question prompt and max_tokens — same shape as analyze_video/analyze_image.

Key decisions

  • Reuses the non-streaming analyze path. The official Qwen-Omni doc claims stream=True is mandatory, but live testing shows non-streaming calls return HTTP 200 + JSON for text/audio/video. No streaming, no SSE parsing, no test churn on the existing path.
  • input_audio.data must be data:;base64,<b64> + format. Raw base64 is rejected ("The provided URL does not appear to be valid"). Verified live for mp3/wav.
  • Default endpoint serves omni. dashscope.aliyuncs.com/compatible-mode/v1 works for qwen3.5-omni-plus; no workspace-specific URL or new base-URL env needed.
  • 25MB local-file guardrail reused (8.8MB / ~11.7MB base64 body verified on omni).
  • New QWEN_OMNI_MODEL env (default qwen3.5-omni-plus); check_endpoint_status now reports omni_model.

Live verification

LIVE=1 npm run test:live — all 5 pass, including new audio + audio-video against real assets (audio extracted via ffmpeg into /tmp, never into the asset dir):

✓ live: Qwen-Omni audio understanding > analyzes a local mp3 through resolveAudio
✓ live: Qwen-Omni audio-video understanding > analyzes a local video (with audio) via the omni model

Gates

typecheck · lint · format:check · test (76 passed, 5 live-skipped) · build — all green.

Out of scope

No streaming, no progress notifications, no system_prompt field, no voice output (modalities:["text"]). Additive only — existing 3 tools and analyze path untouched.

Fragile assumptions

Updated in AGENTS.md. Notably: doc's stream-mandatory claim is stale; input_audio raw-base64 form is rejected; flac/ogg/m4a/aac formats are implemented by-spec but not live-tested (single-point fallback in src/media.ts).


⚠️ Do not merge — awaiting maintainer review.

Two new MCP tools (analyze_audio, analyze_audio_video) on qwen3.5-omni-plus,
separate from qwen3.7-plus video/image. Reuse the non-streaming analyze path:
live testing shows the doc's 'stream=True mandatory' claim is stale -- omni
non-stream returns 200 for text/audio/video.

Audio uses input_audio{data,format} where data must be data:;base64,<b64>
(raw base64 rejected). Local files validated by ext + magic-byte before
encoding; 25MB guardrail reused (8.8MB / 11.7MB base64 verified on omni).

New QWEN_OMNI_MODEL env, default qwen3.5-omni-plus.
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added audio understanding and audio-video analysis.
    • Added support for common audio formats, remote URLs, and validated local files.
    • Added configurable Omni model support and text-only responses for audio workflows.
    • Expanded endpoint status details to include the configured audio model.
  • Documentation

    • Updated setup instructions, configuration variables, supported tools, formats, and media-size guidance.
  • Tests

    • Added coverage for audio payloads, validation, local-file encoding, tool behavior, and end-to-end audio workflows.
  • Chores

    • Bumped the package version to 0.3.0.

Walkthrough

The MCP server now supports Omni-based audio and audio-video analysis, including configurable models, audio format detection, local-file encoding, new payload shapes, two new tools, documentation, and expanded automated and live tests.

Changes

Omni media analysis

Layer / File(s) Summary
Configuration and payload contracts
.env.example, src/config.ts, src/bailian.ts, test/config.test.ts, test/bailian.test.ts
Adds the configurable Omni model and supports audio payloads, per-call models, audio formats, and text-only modalities.
Audio resolution and validation
src/media.ts, test/media.test.ts
Adds audio extension mapping, signature checks, local base64 encoding, remote URL resolution, and size/type validation.
Omni orchestration and MCP tools
src/server.ts, test/tools.test.ts, test/live.test.ts
Adds analyze_audio and analyze_audio_video, routes requests through the Omni model, and validates request shapes and errors.
Documentation and release metadata
AGENTS.md, README.md, package.json
Documents the expanded tool surface, model behavior, media constraints, configuration, and updates package metadata.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant MCPServer
  participant MediaResolver
  participant DashScope
  Client->>MCPServer: analyze_audio(audio_url, question)
  MCPServer->>MediaResolver: resolveAudio(audio_url)
  MediaResolver-->>MCPServer: data and format
  MCPServer->>DashScope: Omni request with input_audio and text modality
  DashScope-->>MCPServer: answer
  MCPServer-->>Client: tool result
Loading

Possibly related PRs

  • sommio/qwen-omni-mcp#1: Introduced the Bailian client and original video/image MCP tool wiring extended by this change.
  • sommio/qwen-omni-mcp#3: Established local media resolution patterns extended here for audio validation and encoding.

Poem

A bunny hears the audio flow,
With Omni models set to go.
Base64 hops through every byte,
Five MCP tools now shine bright.
“Text-only answers!” sings the hare.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 69.23% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: adding audio and audio-video analysis tools backed by qwen3.5-omni.
Description check ✅ Passed The description is directly related to the changeset and accurately summarizes the new tools, config, and implementation choices.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/omni-audio-tools
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/omni-audio-tools

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sommio
sommio merged commit 4a30952 into develop Jul 29, 2026
3 of 4 checks passed
@sommio
sommio deleted the feat/omni-audio-tools branch July 29, 2026 09:49

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

🟡 Other comments (1)
src/media.ts-40-47 (1)

40-47: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Send .m4a audio as format: "m4a".

DashScope supports m4a as a distinct input_audio.format value, so .m4a files should not be mapped to aac; update AUDIO_FORMAT[".m4a"] and the docs to avoid sending the wrong audio format label.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/media.ts` around lines 40 - 47, Update the AUDIO_FORMAT mapping so the
".m4a" extension resolves to "m4a" instead of "aac", and update the related
documentation to use the same format label.
🧹 Nitpick comments (2)
src/bailian.ts (1)

5-16: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Audio format isn't enforced at the type level; contentBlock's audio branch is dead/duplicate code.

AnalyzeParams.audioFormat is documented as "Required for audio" (Line 12) but typed optional, and both contentBlock (Line 54, audioBlock(url, "")) and buildPayload (Line 70, params.audioFormat ?? "") silently fall back to an empty format string instead of erroring. contentBlock's own audio branch is unreachable in practice — buildPayload never calls it for kind: "audio", it branches independently — so the two places duplicate the kind === "audio" check with different (and one wrong) results, cutting against the stated intent to centralize content-block shapes in contentBlock/audioBlock.

Model AnalyzeParams as a discriminated union so audioFormat is required when kind: "audio", and drop the dead audio fallback in contentBlock.

♻️ Suggested discriminated-union fix
-export interface AnalyzeParams {
-  kind: MediaKind;
-  url: string;
-  prompt: string;
-  maxTokens: number;
-  /** Per-call model override. Falls back to `cfg.model` when omitted. */
-  model?: string;
-  /** Audio format for `kind: "audio"` (e.g. "mp3", "wav"). Required for audio. */
-  audioFormat?: string;
-  /** Output modalities. Omni calls send `["text"]` to force text-only output. */
-  modalities?: string[];
-}
+interface CommonAnalyzeParams {
+  url: string;
+  prompt: string;
+  maxTokens: number;
+  /** Per-call model override. Falls back to `cfg.model` when omitted. */
+  model?: string;
+  /** Output modalities. Omni calls send `["text"]` to force text-only output. */
+  modalities?: string[];
+}
+export type AnalyzeParams =
+  | (CommonAnalyzeParams & { kind: "audio"; audioFormat: string })
+  | (CommonAnalyzeParams & { kind: "video" | "image" });
 export function contentBlock(kind: MediaKind, url: string): Record<string, unknown> {
   if (kind === "video") {
     return { type: "video_url", video_url: { url } };
   }
-  if (kind === "image") {
-    return { type: "image_url", image_url: { url } };
-  }
-  // audio uses input_audio with {data, format}; handled in buildPayload via audioBlock.
-  return audioBlock(url, "");
+  return { type: "image_url", image_url: { url } };
 }

Also applies to: 46-55, 67-86

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bailian.ts` around lines 5 - 16, Model AnalyzeParams as a discriminated
union so the audio variant requires audioFormat while non-audio variants do not
accept or require it. Update contentBlock and buildPayload to use the union’s
narrowing and preserve the centralized audioBlock/content-block construction,
removing empty-string fallbacks and the duplicate unreachable audio branch.
src/media.ts (1)

141-177: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

toDataUrl and toAudioData duplicate the stat/size/read/signature-check pipeline.

Both functions repeat the same stat → isFile → size-guardrail → read → post-read size re-check → signature-check sequence, differing only in the extension validator (mimeFromExt vs audioFormatFromExt) and the final data-URL prefix. Extracting the shared steps into one helper would prevent the two guardrails (e.g. the TOCTOU re-check comment at Line 168) from drifting apart if one is updated later.

Also applies to: 196-230

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/media.ts` around lines 141 - 177, Extract the shared
stat/read/size/signature-validation pipeline from toDataUrl and toAudioData into
a private helper, preserving both pre-read and post-read MAX_LOCAL_FILE_BYTES
checks and the existing file/signature errors. Parameterize only the extension
validator or resulting media metadata needed for each caller, then keep
toDataUrl and toAudioData responsible for their distinct data-URL prefixes and
format handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Other comments:
In `@src/media.ts`:
- Around line 40-47: Update the AUDIO_FORMAT mapping so the ".m4a" extension
resolves to "m4a" instead of "aac", and update the related documentation to use
the same format label.

---

Nitpick comments:
In `@src/bailian.ts`:
- Around line 5-16: Model AnalyzeParams as a discriminated union so the audio
variant requires audioFormat while non-audio variants do not accept or require
it. Update contentBlock and buildPayload to use the union’s narrowing and
preserve the centralized audioBlock/content-block construction, removing
empty-string fallbacks and the duplicate unreachable audio branch.

In `@src/media.ts`:
- Around line 141-177: Extract the shared stat/read/size/signature-validation
pipeline from toDataUrl and toAudioData into a private helper, preserving both
pre-read and post-read MAX_LOCAL_FILE_BYTES checks and the existing
file/signature errors. Parameterize only the extension validator or resulting
media metadata needed for each caller, then keep toDataUrl and toAudioData
responsible for their distinct data-URL prefixes and format handling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: QUIET

Plan: Pro Plus

Run ID: a69fef9c-432f-4379-83e1-757e247db1e8

📥 Commits

Reviewing files that changed from the base of the PR and between 6979d18 and 08c8602.

📒 Files selected for processing (13)
  • .env.example
  • AGENTS.md
  • README.md
  • package.json
  • src/bailian.ts
  • src/config.ts
  • src/media.ts
  • src/server.ts
  • test/bailian.test.ts
  • test/config.test.ts
  • test/live.test.ts
  • test/media.test.ts
  • test/tools.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (10)
**/*.{ts,js,json,md,env}

📄 CodeRabbit inference engine (AGENTS.md)

Never commit, hardcode, or document real secrets, API keys, tokens, or .env files. Read DASHSCOPE_API_KEY from environment variables via src/config.ts; use dummy values such as sk-test in fixtures.

Files:

  • package.json
  • test/config.test.ts
  • test/media.test.ts
  • test/live.test.ts
  • src/config.ts
  • test/bailian.test.ts
  • test/tools.test.ts
  • README.md
  • src/server.ts
  • src/media.ts
  • AGENTS.md
  • src/bailian.ts
package.json

📄 CodeRabbit inference engine (AGENTS.md)

package.json: After cloning, run npm install so the prepare script installs Husky hooks, and verify core.hooksPath is .husky.
Do not add a new runtime, language, or heavy dependency without explicit maintainer approval.

Files:

  • package.json
**/*

📄 CodeRabbit inference engine (AGENTS.md)

Delete files with trash, never rm.

Files:

  • package.json
  • test/config.test.ts
  • test/media.test.ts
  • test/live.test.ts
  • src/config.ts
  • test/bailian.test.ts
  • test/tools.test.ts
  • README.md
  • src/server.ts
  • src/media.ts
  • AGENTS.md
  • src/bailian.ts
test/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

test/**/*.ts: any may be used sparingly for fixture typing in tests, but production TypeScript must remain strict.
Unit and mocked end-to-end tests must use MSW to mock fetch; they must not make real API calls.
Every new tool or logic branch must have a test, and overall coverage must remain at least 85%.

Files:

  • test/config.test.ts
  • test/media.test.ts
  • test/live.test.ts
  • test/bailian.test.ts
  • test/tools.test.ts
test/live.test.ts

📄 CodeRabbit inference engine (AGENTS.md)

Live tests run only with LIVE=1 and a real DASHSCOPE_API_KEY; never include them in the default npm test suite.

Files:

  • test/live.test.ts
src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.ts: Use strict TypeScript. Do not use any, @ts-ignore, or non-null assertions in src/; prefer narrow types and unknown when parsing external JSON.
Use the Bailian OpenAI-compatible endpoint ${DASHSCOPE_BASE_URL}/chat/completions, defaulting to https://dashscope.aliyuncs.com/compatible-mode/v1.
Use qwen3.7-plus for video and image analysis with native video support; do not perform client-side frame extraction.
Use qwen3.5-omni-plus, configurable through QWEN_OMNI_MODEL, for audio and audio-video analysis, and send modalities: ["text"].
Do not switch multimodal tools to the Anthropic-compatible /apps/anthropic endpoint because it does not support video input.
Use data:;base64,<b64> plus format for input_audio.data, not raw base64; preserve non-streaming Omni calls unless the endpoint later requires streaming.

Files:

  • src/config.ts
  • src/server.ts
  • src/media.ts
  • src/bailian.ts
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Preserve existing code style and use Prettier and ESLint auto-fix for formatting.

Files:

  • src/config.ts
  • src/server.ts
  • src/media.ts
  • src/bailian.ts
src/server.ts

📄 CodeRabbit inference engine (AGENTS.md)

src/server.ts: Preserve the names and argument schemas of the five MCP tools: analyze_video, analyze_image, analyze_audio, analyze_audio_video, and check_endpoint_status; add new tools instead of renaming existing ones.
check_endpoint_status must redact the API key using redactKey; keep the no-key-leak test passing.

Files:

  • src/server.ts
src/media.ts

📄 CodeRabbit inference engine (AGENTS.md)

Enforce the 25MB local-file guardrail and validate local media by extension and magic-byte signature before encoding; centralize audio data URL changes in toAudioData().

Files:

  • src/media.ts
src/bailian.ts

📄 CodeRabbit inference engine (AGENTS.md)

src/bailian.ts: Keep the DashScope payload builder injectable. Centralize changes to video_url, image_url, and input_audio content-block shapes in contentBlock and audioBlock.
Keep endpoint, model, content-block, and streaming fallbacks centralized in src/bailian.ts; verify model IDs and endpoint compatibility before changing these assumptions.

Files:

  • src/bailian.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: sommio/qwen-omni-mcp

Timestamp: 2026-07-29T09:45:28.641Z
Learning: If a secret is accidentally staged, unstage it, rotate the key immediately, and notify the maintainer.
Learnt from: CR
Repo: sommio/qwen-omni-mcp

Timestamp: 2026-07-29T09:45:28.641Z
Learning: Never bypass Git hooks with `git commit --no-verify` or `git push --no-verify`; fix hook failures instead.
Learnt from: CR
Repo: sommio/qwen-omni-mcp

Timestamp: 2026-07-29T09:45:28.641Z
Learning: Before pushing, all quality gates must pass: typecheck, lint, format check, tests, and build; CI runs them on Node 20 and 22.
🔇 Additional comments (11)
package.json (1)

3-4: LGTM!

AGENTS.md (1)

36-39: LGTM!

Also applies to: 40-45, 46-50, 68-72

README.md (1)

3-13: LGTM!

Also applies to: 38-44, 84-91, 92-94

.env.example (1)

8-9: LGTM!

src/config.ts (1)

3-14: LGTM!

Also applies to: 40-48

test/bailian.test.ts (1)

10-10: LGTM!

Also applies to: 57-87

test/config.test.ts (1)

19-29: LGTM!

Also applies to: 31-40

test/media.test.ts (1)

220-288: LGTM!

src/server.ts (1)

66-105: LGTM!

Also applies to: 157-199, 214-214

test/tools.test.ts (1)

79-89: LGTM!

Also applies to: 144-190, 289-324

test/live.test.ts (1)

71-109: LGTM!

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.

1 participant