Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
71
packages/media-understanding-common/package.json
Normal file
71
packages/media-understanding-common/package.json
Normal file
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"name": "@openclaw/media-understanding-common",
|
||||
"version": "0.0.0-private",
|
||||
"private": true,
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"type": "module",
|
||||
"main": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.mts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"import": "./dist/index.mjs",
|
||||
"default": "./dist/index.mjs"
|
||||
},
|
||||
"./active-model": {
|
||||
"types": "./dist/active-model.d.mts",
|
||||
"import": "./dist/active-model.mjs",
|
||||
"default": "./dist/active-model.mjs"
|
||||
},
|
||||
"./defaults": {
|
||||
"types": "./dist/defaults.d.mts",
|
||||
"import": "./dist/defaults.mjs",
|
||||
"default": "./dist/defaults.mjs"
|
||||
},
|
||||
"./errors": {
|
||||
"types": "./dist/errors.d.mts",
|
||||
"import": "./dist/errors.mjs",
|
||||
"default": "./dist/errors.mjs"
|
||||
},
|
||||
"./format": {
|
||||
"types": "./dist/format.d.mts",
|
||||
"import": "./dist/format.mjs",
|
||||
"default": "./dist/format.mjs"
|
||||
},
|
||||
"./openai-compatible-video": {
|
||||
"types": "./dist/openai-compatible-video.d.mts",
|
||||
"import": "./dist/openai-compatible-video.mjs",
|
||||
"default": "./dist/openai-compatible-video.mjs"
|
||||
},
|
||||
"./output-extract": {
|
||||
"types": "./dist/output-extract.d.mts",
|
||||
"import": "./dist/output-extract.mjs",
|
||||
"default": "./dist/output-extract.mjs"
|
||||
},
|
||||
"./provider-id": {
|
||||
"types": "./dist/provider-id.d.mts",
|
||||
"import": "./dist/provider-id.mjs",
|
||||
"default": "./dist/provider-id.mjs"
|
||||
},
|
||||
"./provider-supports": {
|
||||
"types": "./dist/provider-supports.d.mts",
|
||||
"import": "./dist/provider-supports.mjs",
|
||||
"default": "./dist/provider-supports.mjs"
|
||||
},
|
||||
"./types": {
|
||||
"types": "./dist/types.d.mts",
|
||||
"import": "./dist/types.mjs",
|
||||
"default": "./dist/types.mjs"
|
||||
},
|
||||
"./video": {
|
||||
"types": "./dist/video.d.mts",
|
||||
"import": "./dist/video.mjs",
|
||||
"default": "./dist/video.mjs"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsdown src/index.ts src/active-model.ts src/defaults.ts src/errors.ts src/format.ts src/openai-compatible-video.ts src/output-extract.ts src/provider-id.ts src/provider-supports.ts src/types.ts src/video.ts --no-config --platform node --format esm --dts --out-dir dist --clean"
|
||||
}
|
||||
}
|
||||
7
packages/media-understanding-common/src/active-model.ts
Normal file
7
packages/media-understanding-common/src/active-model.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// Active media-understanding model selection contract.
|
||||
|
||||
/** Provider/model pair selected for one media-understanding request. */
|
||||
export type ActiveMediaModel = {
|
||||
provider: string;
|
||||
model?: string;
|
||||
};
|
||||
44
packages/media-understanding-common/src/defaults.ts
Normal file
44
packages/media-understanding-common/src/defaults.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
// Media Understanding Common module implements defaults behavior.
|
||||
import type { MediaUnderstandingCapability } from "./types.js";
|
||||
|
||||
// Shared defaults for media-understanding limits, prompts, and concurrency.
|
||||
|
||||
const MB = 1024 * 1024;
|
||||
|
||||
/** Default max response characters for bounded text outputs. */
|
||||
export const DEFAULT_MAX_CHARS = 500;
|
||||
/** Default max response characters by capability. */
|
||||
export const DEFAULT_MAX_CHARS_BY_CAPABILITY: Record<
|
||||
MediaUnderstandingCapability,
|
||||
number | undefined
|
||||
> = {
|
||||
image: DEFAULT_MAX_CHARS,
|
||||
audio: undefined,
|
||||
video: DEFAULT_MAX_CHARS,
|
||||
};
|
||||
/** Default input byte limits by capability. */
|
||||
export const DEFAULT_MAX_BYTES: Record<MediaUnderstandingCapability, number> = {
|
||||
image: 10 * MB,
|
||||
audio: 20 * MB,
|
||||
video: 50 * MB,
|
||||
};
|
||||
/** Default request timeout by capability. */
|
||||
export const DEFAULT_TIMEOUT_SECONDS: Record<MediaUnderstandingCapability, number> = {
|
||||
image: 60,
|
||||
audio: 60,
|
||||
video: 120,
|
||||
};
|
||||
/** Default prompts by capability. */
|
||||
export const DEFAULT_PROMPT: Record<MediaUnderstandingCapability, string> = {
|
||||
image: "Describe the image.",
|
||||
audio: "Transcribe the audio.",
|
||||
video: "Describe the video.",
|
||||
};
|
||||
/** Upper bound for base64-expanded video payloads. */
|
||||
export const DEFAULT_VIDEO_MAX_BASE64_BYTES = 70 * MB;
|
||||
/** CLI output buffer used by provider child processes. */
|
||||
export const CLI_OUTPUT_MAX_BUFFER = 5 * MB;
|
||||
/** Default parallel media-understanding request count. */
|
||||
export const DEFAULT_MEDIA_CONCURRENCY = 2;
|
||||
/** Minimum bytes for audio files before transcription is attempted. */
|
||||
export const MIN_AUDIO_FILE_BYTES = 1024;
|
||||
26
packages/media-understanding-common/src/errors.ts
Normal file
26
packages/media-understanding-common/src/errors.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
// Media-understanding skip error used for non-fatal attachment omissions.
|
||||
|
||||
/** Reason a media-understanding attachment was skipped. */
|
||||
type MediaUnderstandingSkipReason =
|
||||
| "maxBytes"
|
||||
| "timeout"
|
||||
| "unsupported"
|
||||
| "empty"
|
||||
| "blocked"
|
||||
| "tooSmall";
|
||||
|
||||
/** Error used when a media attachment should be skipped without failing the whole request. */
|
||||
export class MediaUnderstandingSkipError extends Error {
|
||||
readonly reason: MediaUnderstandingSkipReason;
|
||||
|
||||
constructor(reason: MediaUnderstandingSkipReason, message: string) {
|
||||
super(message);
|
||||
this.reason = reason;
|
||||
this.name = "MediaUnderstandingSkipError";
|
||||
}
|
||||
}
|
||||
|
||||
/** Narrow unknown errors to media-understanding skip errors. */
|
||||
export function isMediaUnderstandingSkipError(err: unknown): err is MediaUnderstandingSkipError {
|
||||
return err instanceof MediaUnderstandingSkipError;
|
||||
}
|
||||
147
packages/media-understanding-common/src/format.test.ts
Normal file
147
packages/media-understanding-common/src/format.test.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
// Media Understanding Common tests cover format behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formatMediaUnderstandingBody } from "./format.js";
|
||||
|
||||
describe("formatMediaUnderstandingBody", () => {
|
||||
it("replaces placeholder body with transcript", () => {
|
||||
const body = formatMediaUnderstandingBody({
|
||||
body: "<media:audio>",
|
||||
outputs: [
|
||||
{
|
||||
kind: "audio.transcription",
|
||||
attachmentIndex: 0,
|
||||
text: "hello world",
|
||||
provider: "groq",
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(body).toBe("[Audio]\nTranscript:\nhello world");
|
||||
});
|
||||
|
||||
it("includes user text when body is meaningful", () => {
|
||||
const body = formatMediaUnderstandingBody({
|
||||
body: "caption here",
|
||||
outputs: [
|
||||
{
|
||||
kind: "audio.transcription",
|
||||
attachmentIndex: 0,
|
||||
text: "transcribed",
|
||||
provider: "groq",
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(body).toBe("[Audio]\nUser text:\ncaption here\nTranscript:\ntranscribed");
|
||||
});
|
||||
|
||||
it("strips leading media placeholders from user text", () => {
|
||||
const body = formatMediaUnderstandingBody({
|
||||
body: "<media:audio> caption here",
|
||||
outputs: [
|
||||
{
|
||||
kind: "audio.transcription",
|
||||
attachmentIndex: 0,
|
||||
text: "transcribed",
|
||||
provider: "groq",
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(body).toBe("[Audio]\nUser text:\ncaption here\nTranscript:\ntranscribed");
|
||||
});
|
||||
|
||||
it("strips repeated leading media placeholders from user text", () => {
|
||||
const body = formatMediaUnderstandingBody({
|
||||
body: "<media:image> <media:audio> caption here",
|
||||
outputs: [
|
||||
{
|
||||
kind: "audio.transcription",
|
||||
attachmentIndex: 0,
|
||||
text: "transcribed",
|
||||
provider: "groq",
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(body).toBe("[Audio]\nUser text:\ncaption here\nTranscript:\ntranscribed");
|
||||
});
|
||||
|
||||
it("treats repeated media placeholders without captions as synthetic text", () => {
|
||||
const body = formatMediaUnderstandingBody({
|
||||
body: "<media:image> <media:audio>",
|
||||
outputs: [
|
||||
{
|
||||
kind: "image.description",
|
||||
attachmentIndex: 0,
|
||||
text: "a chart",
|
||||
provider: "openai",
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(body).toBe("[Image]\nDescription:\na chart");
|
||||
});
|
||||
|
||||
it("keeps user text once when multiple outputs exist", () => {
|
||||
const body = formatMediaUnderstandingBody({
|
||||
body: "caption here",
|
||||
outputs: [
|
||||
{
|
||||
kind: "audio.transcription",
|
||||
attachmentIndex: 0,
|
||||
text: "audio text",
|
||||
provider: "groq",
|
||||
},
|
||||
{
|
||||
kind: "video.description",
|
||||
attachmentIndex: 1,
|
||||
text: "video text",
|
||||
provider: "google",
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(body).toBe(
|
||||
[
|
||||
"User text:\ncaption here",
|
||||
"[Audio]\nTranscript:\naudio text",
|
||||
"[Video]\nDescription:\nvideo text",
|
||||
].join("\n\n"),
|
||||
);
|
||||
});
|
||||
|
||||
it("formats image outputs", () => {
|
||||
const body = formatMediaUnderstandingBody({
|
||||
body: "<media:image>",
|
||||
outputs: [
|
||||
{
|
||||
kind: "image.description",
|
||||
attachmentIndex: 0,
|
||||
text: "a cat",
|
||||
provider: "openai",
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(body).toBe("[Image]\nDescription:\na cat");
|
||||
});
|
||||
|
||||
it("labels audio transcripts by their attachment order", () => {
|
||||
const body = formatMediaUnderstandingBody({
|
||||
outputs: [
|
||||
{
|
||||
kind: "audio.transcription",
|
||||
attachmentIndex: 0,
|
||||
text: "first clip was silent",
|
||||
provider: "openclaw",
|
||||
},
|
||||
{
|
||||
kind: "audio.transcription",
|
||||
attachmentIndex: 1,
|
||||
text: "second clip has speech",
|
||||
provider: "groq",
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(body).toBe(
|
||||
[
|
||||
"[Audio 1/2]\nTranscript:\nfirst clip was silent",
|
||||
"[Audio 2/2]\nTranscript:\nsecond clip has speech",
|
||||
].join("\n\n"),
|
||||
);
|
||||
});
|
||||
});
|
||||
103
packages/media-understanding-common/src/format.ts
Normal file
103
packages/media-understanding-common/src/format.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
// Media Understanding Common helper module supports format behavior.
|
||||
import type { MediaUnderstandingOutput } from "./types.js";
|
||||
|
||||
const MEDIA_PLACEHOLDER_TOKEN = String.raw`<media:[^>]+>(?:\s*\([^)]*\))?`;
|
||||
const MEDIA_PLACEHOLDER_RE = new RegExp(String.raw`^(?:${MEDIA_PLACEHOLDER_TOKEN}\s*)+$`, "i");
|
||||
const MEDIA_PLACEHOLDER_TOKEN_RE = new RegExp(String.raw`^(?:${MEDIA_PLACEHOLDER_TOKEN}\s*)+`, "i");
|
||||
|
||||
/** Extracts user-authored text while ignoring synthetic media placeholder tokens. */
|
||||
export function extractMediaUserText(body?: string): string | undefined {
|
||||
const trimmed = body?.trim() ?? "";
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
if (MEDIA_PLACEHOLDER_RE.test(trimmed)) {
|
||||
return undefined;
|
||||
}
|
||||
const cleaned = trimmed.replace(MEDIA_PLACEHOLDER_TOKEN_RE, "").trim();
|
||||
return cleaned || undefined;
|
||||
}
|
||||
|
||||
function formatSection(
|
||||
title: string,
|
||||
kind: "Transcript" | "Description",
|
||||
text: string,
|
||||
userText?: string,
|
||||
): string {
|
||||
const lines = [`[${title}]`];
|
||||
if (userText) {
|
||||
lines.push(`User text:\n${userText}`);
|
||||
}
|
||||
lines.push(`${kind}:\n${text}`);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/** Formats media-understanding outputs into the chat body sent back to the model. */
|
||||
export function formatMediaUnderstandingBody(params: {
|
||||
body?: string;
|
||||
outputs: MediaUnderstandingOutput[];
|
||||
}): string {
|
||||
const outputs = params.outputs.filter((output) => output.text.trim());
|
||||
if (outputs.length === 0) {
|
||||
return params.body ?? "";
|
||||
}
|
||||
|
||||
const userText = extractMediaUserText(params.body);
|
||||
const sections: string[] = [];
|
||||
if (userText && outputs.length > 1) {
|
||||
sections.push(`User text:\n${userText}`);
|
||||
}
|
||||
|
||||
const counts = new Map<MediaUnderstandingOutput["kind"], number>();
|
||||
for (const output of outputs) {
|
||||
counts.set(output.kind, (counts.get(output.kind) ?? 0) + 1);
|
||||
}
|
||||
const seen = new Map<MediaUnderstandingOutput["kind"], number>();
|
||||
|
||||
for (const output of outputs) {
|
||||
const count = counts.get(output.kind) ?? 1;
|
||||
const next = (seen.get(output.kind) ?? 0) + 1;
|
||||
seen.set(output.kind, next);
|
||||
const suffix = count > 1 ? ` ${next}/${count}` : "";
|
||||
if (output.kind === "audio.transcription") {
|
||||
sections.push(
|
||||
formatSection(
|
||||
`Audio${suffix}`,
|
||||
"Transcript",
|
||||
output.text,
|
||||
outputs.length === 1 ? userText : undefined,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (output.kind === "image.description") {
|
||||
sections.push(
|
||||
formatSection(
|
||||
`Image${suffix}`,
|
||||
"Description",
|
||||
output.text,
|
||||
outputs.length === 1 ? userText : undefined,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
sections.push(
|
||||
formatSection(
|
||||
`Video${suffix}`,
|
||||
"Description",
|
||||
output.text,
|
||||
outputs.length === 1 ? userText : undefined,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return sections.join("\n\n").trim();
|
||||
}
|
||||
|
||||
/** Formats one or more audio transcript outputs for legacy transcript-only callers. */
|
||||
export function formatAudioTranscripts(outputs: MediaUnderstandingOutput[]): string {
|
||||
if (outputs.length === 1) {
|
||||
return outputs[0].text;
|
||||
}
|
||||
return outputs.map((output, index) => `Audio ${index + 1}:\n${output.text}`).join("\n\n");
|
||||
}
|
||||
12
packages/media-understanding-common/src/index.ts
Normal file
12
packages/media-understanding-common/src/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// Public barrel for media-understanding shared contracts and helpers.
|
||||
|
||||
export * from "./active-model.js";
|
||||
export * from "./defaults.js";
|
||||
export * from "./errors.js";
|
||||
export * from "./format.js";
|
||||
export * from "./openai-compatible-video.js";
|
||||
export * from "./output-extract.js";
|
||||
export * from "./provider-id.js";
|
||||
export * from "./provider-supports.js";
|
||||
export * from "./types.js";
|
||||
export * from "./video.js";
|
||||
@@ -0,0 +1,72 @@
|
||||
// OpenAI-compatible video request/response helpers.
|
||||
|
||||
/** Minimal OpenAI-compatible video response payload shape. */
|
||||
export type OpenAiCompatibleVideoPayload = {
|
||||
choices?: Array<{
|
||||
message?: {
|
||||
content?: string | Array<{ text?: string }>;
|
||||
reasoning_content?: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
|
||||
/** Trim optional strings, falling back when empty. */
|
||||
export function resolveMediaUnderstandingString(
|
||||
value: string | undefined,
|
||||
fallback: string,
|
||||
): string {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed || fallback;
|
||||
}
|
||||
|
||||
/** Coerce text from OpenAI-compatible content or reasoning fields. */
|
||||
export function coerceOpenAiCompatibleVideoText(
|
||||
payload: OpenAiCompatibleVideoPayload,
|
||||
): string | null {
|
||||
const message = payload.choices?.[0]?.message;
|
||||
if (!message) {
|
||||
return null;
|
||||
}
|
||||
if (typeof message.content === "string" && message.content.trim()) {
|
||||
return message.content.trim();
|
||||
}
|
||||
if (Array.isArray(message.content)) {
|
||||
const text = message.content
|
||||
.map((part) => part.text?.trim() ?? "")
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
if (typeof message.reasoning_content === "string" && message.reasoning_content.trim()) {
|
||||
return message.reasoning_content.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Build an OpenAI-compatible request body with an inline data URL video. */
|
||||
export function buildOpenAiCompatibleVideoRequestBody(params: {
|
||||
model: string;
|
||||
prompt: string;
|
||||
mime: string;
|
||||
buffer: Buffer;
|
||||
}) {
|
||||
return {
|
||||
model: params.model,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: params.prompt },
|
||||
{
|
||||
type: "video_url",
|
||||
video_url: {
|
||||
url: `data:${params.mime};base64,${params.buffer.toString("base64")}`,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
100
packages/media-understanding-common/src/output-extract.test.ts
Normal file
100
packages/media-understanding-common/src/output-extract.test.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
// Media Understanding Common tests cover provider output extraction behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { extractGeminiResponse } from "./output-extract.js";
|
||||
|
||||
describe("extractGeminiResponse", () => {
|
||||
it("extracts the response from noisy output with nested JSON objects", () => {
|
||||
expect(
|
||||
extractGeminiResponse(
|
||||
[
|
||||
"debug: invoking gemini",
|
||||
JSON.stringify({
|
||||
response: "a useful description",
|
||||
usage: {
|
||||
inputTokens: 12,
|
||||
outputTokens: 4,
|
||||
},
|
||||
}),
|
||||
].join("\n"),
|
||||
),
|
||||
).toBe("a useful description");
|
||||
});
|
||||
|
||||
it("returns null for an incomplete JSON object", () => {
|
||||
expect(extractGeminiResponse("{")).toBeNull();
|
||||
});
|
||||
|
||||
it("ignores unmatched quotes in noisy output before the JSON object", () => {
|
||||
expect(extractGeminiResponse('debug: model said "hello\n{"response":"ok"}')).toBe("ok");
|
||||
});
|
||||
|
||||
it("ignores braces inside quoted noisy output", () => {
|
||||
expect(extractGeminiResponse('debug: "hello { world" {"response":"ok"}')).toBe("ok");
|
||||
});
|
||||
|
||||
it("ignores shell-quoted JSON-like noisy output", () => {
|
||||
expect(extractGeminiResponse('debug: \'{"response":"fake"}\'')).toBeNull();
|
||||
});
|
||||
|
||||
it("does not treat apostrophes inside noisy words as quote delimiters", () => {
|
||||
expect(extractGeminiResponse('debug: it\'s done {"response":"ok"}')).toBe("ok");
|
||||
});
|
||||
|
||||
it("resynchronizes after an unmatched brace in noisy output", () => {
|
||||
expect(extractGeminiResponse('debug: generated {\n{"response":"ok"}')).toBe("ok");
|
||||
});
|
||||
|
||||
it("preserves brace-heavy response text", () => {
|
||||
const response = "{".repeat(33);
|
||||
expect(extractGeminiResponse(JSON.stringify({ response }))).toBe(response);
|
||||
});
|
||||
|
||||
it("extracts pretty-printed JSON output", () => {
|
||||
expect(
|
||||
extractGeminiResponse(
|
||||
JSON.stringify(
|
||||
{
|
||||
response: "pretty response",
|
||||
usage: { inputTokens: 12 },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
),
|
||||
).toBe("pretty response");
|
||||
});
|
||||
|
||||
it("preserves pretty-printed object elements inside arrays", () => {
|
||||
expect(
|
||||
extractGeminiResponse(
|
||||
JSON.stringify(
|
||||
{
|
||||
response: "array response",
|
||||
items: [{ id: 1 }, { id: 2 }],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
),
|
||||
).toBe("array response");
|
||||
});
|
||||
|
||||
it("does not accept an inner response from a malformed trailing object", () => {
|
||||
expect(extractGeminiResponse('{"response":"good"} {"meta":{"response":"bad"} broken}')).toBe(
|
||||
"good",
|
||||
);
|
||||
expect(extractGeminiResponse('{"response":"good"} {"meta":{"response":"bad"}')).toBe("good");
|
||||
});
|
||||
|
||||
it("ignores a nested response inside an unfinished outer object", () => {
|
||||
expect(extractGeminiResponse('noise {"meta":{"response":"bad"}')).toBeNull();
|
||||
});
|
||||
|
||||
it("does not promote a child from a malformed outer object", () => {
|
||||
expect(extractGeminiResponse('{"response":"good"} {"meta" {"response":"bad"}}')).toBe("good");
|
||||
expect(extractGeminiResponse('noise {broken {"response":"bad"}}')).toBeNull();
|
||||
expect(extractGeminiResponse('{"response":"good"}\nnoise {broken\n{"response":"bad"}}')).toBe(
|
||||
"good",
|
||||
);
|
||||
});
|
||||
});
|
||||
133
packages/media-understanding-common/src/output-extract.ts
Normal file
133
packages/media-understanding-common/src/output-extract.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
// Output extractors for media-understanding provider CLI responses.
|
||||
|
||||
/** Parse the last JSON object in a noisy provider output string. */
|
||||
function extractLastJsonObject(raw: string): unknown {
|
||||
const trimmed = raw.trim();
|
||||
const ranges: Array<{ end: number; start: number }> = [];
|
||||
const starts: number[] = [];
|
||||
let inString = false;
|
||||
let escaped = false;
|
||||
let preambleQuote: string | undefined;
|
||||
let preambleEscaped = false;
|
||||
let previousSignificant: string | undefined;
|
||||
let lineHasNonWhitespace = false;
|
||||
let arrayDepth = 0;
|
||||
let candidateHasContent = false;
|
||||
|
||||
for (let index = 0; index < trimmed.length; index += 1) {
|
||||
const character = trimmed[index];
|
||||
if (inString) {
|
||||
if (character === "\n" || character === "\r") {
|
||||
starts.length = 0;
|
||||
inString = false;
|
||||
escaped = false;
|
||||
} else if (escaped) {
|
||||
escaped = false;
|
||||
} else if (character === "\\") {
|
||||
escaped = true;
|
||||
} else if (character === '"') {
|
||||
inString = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (starts.length === 0) {
|
||||
if (preambleQuote !== undefined) {
|
||||
if (character === "\n" || character === "\r") {
|
||||
preambleQuote = undefined;
|
||||
preambleEscaped = false;
|
||||
} else if (preambleEscaped) {
|
||||
preambleEscaped = false;
|
||||
} else if (character === "\\") {
|
||||
preambleEscaped = true;
|
||||
} else if (character === preambleQuote) {
|
||||
preambleQuote = undefined;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (character === '"' || character === "'" || character === "`") {
|
||||
const previous = trimmed[index - 1];
|
||||
if (previous === undefined || /[\s:([{]/.test(previous)) {
|
||||
preambleQuote = character;
|
||||
preambleEscaped = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (character === "{") {
|
||||
arrayDepth = 0;
|
||||
candidateHasContent = false;
|
||||
starts.push(index);
|
||||
}
|
||||
if (!/\s/.test(character)) {
|
||||
previousSignificant = character;
|
||||
lineHasNonWhitespace = true;
|
||||
} else if (character === "\n" || character === "\r") {
|
||||
lineHasNonWhitespace = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const hadCandidateContent = candidateHasContent;
|
||||
if (character === '"') {
|
||||
inString = true;
|
||||
} else if (character === "{") {
|
||||
if (
|
||||
previousSignificant === ":" ||
|
||||
previousSignificant === "[" ||
|
||||
previousSignificant === '"' ||
|
||||
(previousSignificant === "," && (lineHasNonWhitespace || arrayDepth > 0))
|
||||
) {
|
||||
starts.push(index);
|
||||
} else if (!lineHasNonWhitespace && !hadCandidateContent) {
|
||||
// Only resync at a clean record boundary; otherwise keep malformed
|
||||
// outer objects from promoting diagnostic payloads as valid results.
|
||||
starts.length = 1;
|
||||
starts[0] = index;
|
||||
arrayDepth = 0;
|
||||
candidateHasContent = false;
|
||||
}
|
||||
} else if (character === "}" && starts.length > 0) {
|
||||
const start = starts.pop();
|
||||
if (start !== undefined && starts.length === 0) {
|
||||
ranges.push({ start, end: index });
|
||||
}
|
||||
} else if (character === "[") {
|
||||
arrayDepth += 1;
|
||||
} else if (character === "]" && arrayDepth > 0) {
|
||||
arrayDepth -= 1;
|
||||
}
|
||||
|
||||
if (!/\s/.test(character)) {
|
||||
candidateHasContent = true;
|
||||
previousSignificant = character;
|
||||
lineHasNonWhitespace = true;
|
||||
} else if (character === "\n" || character === "\r") {
|
||||
lineHasNonWhitespace = false;
|
||||
}
|
||||
}
|
||||
|
||||
for (let index = ranges.length - 1; index >= 0; index -= 1) {
|
||||
const range = ranges[index];
|
||||
try {
|
||||
return JSON.parse(trimmed.slice(range.start, range.end + 1));
|
||||
} catch {
|
||||
// Ignore malformed objects and try the previous completed range.
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Extract Gemini CLI-style response text from the last JSON object in output. */
|
||||
export function extractGeminiResponse(raw: string): string | null {
|
||||
const payload = extractLastJsonObject(raw);
|
||||
if (!payload || typeof payload !== "object") {
|
||||
return null;
|
||||
}
|
||||
const response = (payload as { response?: unknown }).response;
|
||||
if (typeof response !== "string") {
|
||||
return null;
|
||||
}
|
||||
const trimmed = response.trim();
|
||||
return trimmed || null;
|
||||
}
|
||||
30
packages/media-understanding-common/src/provider-id.ts
Normal file
30
packages/media-understanding-common/src/provider-id.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
// Provider id normalization for media-understanding config and execution.
|
||||
|
||||
/** Normalize a provider id for comparison. */
|
||||
function normalizeProviderId(provider: string): string {
|
||||
return provider.trim().toLowerCase();
|
||||
}
|
||||
|
||||
/** Normalize provider aliases to canonical config provider ids. */
|
||||
export function normalizeMediaProviderId(id: string): string {
|
||||
const normalized = normalizeProviderId(id);
|
||||
if (normalized === "gemini") {
|
||||
return "google";
|
||||
}
|
||||
if (normalized === "minimax-cn") {
|
||||
return "minimax";
|
||||
}
|
||||
if (normalized === "minimax-portal-cn") {
|
||||
return "minimax-portal";
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/** Normalize provider ids while preserving execution-specific regional aliases. */
|
||||
export function normalizeMediaExecutionProviderId(id: string): string {
|
||||
const normalized = normalizeProviderId(id);
|
||||
if (normalized === "minimax-cn" || normalized === "minimax-portal-cn") {
|
||||
return normalized;
|
||||
}
|
||||
return normalizeMediaProviderId(normalized);
|
||||
}
|
||||
21
packages/media-understanding-common/src/provider-supports.ts
Normal file
21
packages/media-understanding-common/src/provider-supports.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// Media Understanding Common module implements provider supports behavior.
|
||||
import type { MediaUnderstandingCapability, MediaUnderstandingProvider } from "./types.js";
|
||||
|
||||
// Capability checks for media-understanding provider objects.
|
||||
|
||||
/** Return true when a provider exposes the method for a media capability. */
|
||||
export function providerSupportsCapability(
|
||||
provider: MediaUnderstandingProvider | undefined,
|
||||
capability: MediaUnderstandingCapability,
|
||||
): boolean {
|
||||
if (!provider) {
|
||||
return false;
|
||||
}
|
||||
if (capability === "audio") {
|
||||
return Boolean(provider.transcribeAudio);
|
||||
}
|
||||
if (capability === "image") {
|
||||
return Boolean(provider.describeImage);
|
||||
}
|
||||
return Boolean(provider.describeVideo);
|
||||
}
|
||||
47
packages/media-understanding-common/src/types.ts
Normal file
47
packages/media-understanding-common/src/types.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// Shared media-understanding provider, attachment, output, and capability contracts.
|
||||
|
||||
/** Kind of media-understanding output produced for an attachment. */
|
||||
export type MediaUnderstandingKind =
|
||||
| "audio.transcription"
|
||||
| "video.description"
|
||||
| "image.description";
|
||||
|
||||
/** Capability exposed by a media-understanding provider. */
|
||||
export type MediaUnderstandingCapability = "image" | "audio" | "video";
|
||||
|
||||
/** Capability registry keyed by provider id. */
|
||||
export type MediaUnderstandingCapabilityRegistry = Map<
|
||||
string,
|
||||
{
|
||||
capabilities?: MediaUnderstandingCapability[];
|
||||
}
|
||||
>;
|
||||
|
||||
/** Media attachment passed to understanding providers. */
|
||||
export type MediaAttachment = {
|
||||
path?: string;
|
||||
url?: string;
|
||||
mime?: string;
|
||||
index: number;
|
||||
alreadyTranscribed?: boolean;
|
||||
};
|
||||
|
||||
/** Normalized text output produced by media understanding. */
|
||||
export type MediaUnderstandingOutput = {
|
||||
kind: MediaUnderstandingKind;
|
||||
attachmentIndex: number;
|
||||
text: string;
|
||||
provider: string;
|
||||
model?: string;
|
||||
};
|
||||
|
||||
/** Provider shape used for capability discovery and dispatch. */
|
||||
export type MediaUnderstandingProvider = {
|
||||
id: string;
|
||||
capabilities?: MediaUnderstandingCapability[];
|
||||
transcribeAudio?: unknown;
|
||||
describeVideo?: unknown;
|
||||
describeImage?: unknown;
|
||||
describeImages?: unknown;
|
||||
extractStructured?: unknown;
|
||||
};
|
||||
28
packages/media-understanding-common/src/video.test.ts
Normal file
28
packages/media-understanding-common/src/video.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// Media Understanding Common tests cover video payload sizing behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { DEFAULT_VIDEO_MAX_BASE64_BYTES } from "./defaults.js";
|
||||
import { estimateBase64Size, resolveVideoMaxBase64Bytes } from "./video.js";
|
||||
|
||||
describe("estimateBase64Size", () => {
|
||||
it("rounds byte counts to base64 quanta", () => {
|
||||
expect(estimateBase64Size(1)).toBe(4);
|
||||
expect(estimateBase64Size(2)).toBe(4);
|
||||
expect(estimateBase64Size(3)).toBe(4);
|
||||
expect(estimateBase64Size(4)).toBe(8);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveVideoMaxBase64Bytes", () => {
|
||||
it("allows raw byte limits that expand to valid base64 boundaries", () => {
|
||||
expect(resolveVideoMaxBase64Bytes(1)).toBe(4);
|
||||
expect(resolveVideoMaxBase64Bytes(2)).toBe(4);
|
||||
expect(resolveVideoMaxBase64Bytes(3)).toBe(4);
|
||||
expect(resolveVideoMaxBase64Bytes(4)).toBe(8);
|
||||
});
|
||||
|
||||
it("keeps the shared maximum base64 payload cap", () => {
|
||||
expect(resolveVideoMaxBase64Bytes(DEFAULT_VIDEO_MAX_BASE64_BYTES)).toBe(
|
||||
DEFAULT_VIDEO_MAX_BASE64_BYTES,
|
||||
);
|
||||
});
|
||||
});
|
||||
15
packages/media-understanding-common/src/video.ts
Normal file
15
packages/media-understanding-common/src/video.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// Media Understanding Common module implements video behavior.
|
||||
import { DEFAULT_VIDEO_MAX_BASE64_BYTES } from "./defaults.js";
|
||||
|
||||
// Video payload size helpers for base64-expanded request bodies.
|
||||
|
||||
/** Estimate base64 size for a byte count. */
|
||||
export function estimateBase64Size(bytes: number): number {
|
||||
return Math.ceil(bytes / 3) * 4;
|
||||
}
|
||||
|
||||
/** Resolve video base64 byte limit from raw byte limit and global cap. */
|
||||
export function resolveVideoMaxBase64Bytes(maxBytes: number): number {
|
||||
const expanded = estimateBase64Size(maxBytes);
|
||||
return Math.min(expanded, DEFAULT_VIDEO_MAX_BASE64_BYTES);
|
||||
}
|
||||
Reference in New Issue
Block a user