mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-14 21:43:22 +08:00
重构主控编排与运行时预热链路,统一工作区提示词/专家调度协议并补齐 wiki 记忆注入与写回闭环。
同时收敛启动与运维脚本默认行为(含 wiki worker)、更新 Admin 可观测性与相关测试,降低首轮时延并提高运行稳定性。 Made-with: Cursor
This commit is contained in:
parent
4a23b715a2
commit
dbbe3add6a
14438 changed files with 2693620 additions and 2546 deletions
152
openclaw/extensions/vydra/speech-provider.ts
Normal file
152
openclaw/extensions/vydra/speech-provider.ts
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import {
|
||||
assertOkOrThrowHttpError,
|
||||
postJsonRequest,
|
||||
resolveProviderHttpRequestConfig,
|
||||
} from "openclaw/plugin-sdk/provider-http";
|
||||
import { normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
|
||||
import type {
|
||||
SpeechProviderConfig,
|
||||
SpeechProviderOverrides,
|
||||
SpeechProviderPlugin,
|
||||
} from "openclaw/plugin-sdk/speech-core";
|
||||
import { asObject } from "openclaw/plugin-sdk/speech-core";
|
||||
import {
|
||||
DEFAULT_VYDRA_BASE_URL,
|
||||
DEFAULT_VYDRA_SPEECH_MODEL,
|
||||
DEFAULT_VYDRA_VOICE_ID,
|
||||
downloadVydraAsset,
|
||||
extractVydraResultUrls,
|
||||
normalizeVydraBaseUrl,
|
||||
trimToUndefined,
|
||||
} from "./shared.js";
|
||||
|
||||
type VydraSpeechConfig = {
|
||||
apiKey?: string;
|
||||
baseUrl: string;
|
||||
model: string;
|
||||
voiceId: string;
|
||||
};
|
||||
|
||||
const VYDRA_SPEECH_VOICES = [
|
||||
{
|
||||
id: DEFAULT_VYDRA_VOICE_ID,
|
||||
name: "Rachel",
|
||||
},
|
||||
] as const;
|
||||
|
||||
function normalizeVydraSpeechConfig(rawConfig: Record<string, unknown>): VydraSpeechConfig {
|
||||
const providers = asObject(rawConfig.providers);
|
||||
const raw = asObject(providers?.vydra) ?? asObject(rawConfig.vydra);
|
||||
return {
|
||||
apiKey: normalizeResolvedSecretInputString({
|
||||
value: raw?.apiKey,
|
||||
path: "messages.tts.providers.vydra.apiKey",
|
||||
}),
|
||||
baseUrl: normalizeVydraBaseUrl(
|
||||
trimToUndefined(raw?.baseUrl) ?? trimToUndefined(process.env.VYDRA_BASE_URL),
|
||||
),
|
||||
model:
|
||||
trimToUndefined(raw?.model) ??
|
||||
trimToUndefined(process.env.VYDRA_TTS_MODEL) ??
|
||||
DEFAULT_VYDRA_SPEECH_MODEL,
|
||||
voiceId:
|
||||
trimToUndefined(raw?.voiceId) ??
|
||||
trimToUndefined(process.env.VYDRA_TTS_VOICE_ID) ??
|
||||
DEFAULT_VYDRA_VOICE_ID,
|
||||
};
|
||||
}
|
||||
|
||||
function readVydraSpeechConfig(config: SpeechProviderConfig): VydraSpeechConfig {
|
||||
const normalized = normalizeVydraSpeechConfig({});
|
||||
return {
|
||||
apiKey: trimToUndefined(config.apiKey) ?? normalized.apiKey,
|
||||
baseUrl: normalizeVydraBaseUrl(trimToUndefined(config.baseUrl) ?? normalized.baseUrl),
|
||||
model: trimToUndefined(config.model) ?? normalized.model,
|
||||
voiceId: trimToUndefined(config.voiceId) ?? normalized.voiceId,
|
||||
};
|
||||
}
|
||||
|
||||
function readVydraOverrides(overrides: SpeechProviderOverrides | undefined): {
|
||||
model?: string;
|
||||
voiceId?: string;
|
||||
} {
|
||||
if (!overrides) {
|
||||
return {};
|
||||
}
|
||||
return {
|
||||
model: trimToUndefined(overrides.model),
|
||||
voiceId: trimToUndefined(overrides.voiceId),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildVydraSpeechProvider(): SpeechProviderPlugin {
|
||||
return {
|
||||
id: "vydra",
|
||||
label: "Vydra",
|
||||
models: [DEFAULT_VYDRA_SPEECH_MODEL],
|
||||
voices: VYDRA_SPEECH_VOICES.map((voice) => voice.id),
|
||||
resolveConfig: ({ rawConfig }) => normalizeVydraSpeechConfig(rawConfig),
|
||||
listVoices: async () => VYDRA_SPEECH_VOICES.map((voice) => Object.assign({}, voice)),
|
||||
isConfigured: ({ providerConfig }) =>
|
||||
Boolean(readVydraSpeechConfig(providerConfig).apiKey || process.env.VYDRA_API_KEY),
|
||||
synthesize: async (req) => {
|
||||
const config = readVydraSpeechConfig(req.providerConfig);
|
||||
const overrides = readVydraOverrides(req.providerOverrides);
|
||||
const apiKey = config.apiKey || process.env.VYDRA_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error("Vydra API key missing");
|
||||
}
|
||||
|
||||
const fetchFn = fetch;
|
||||
const { baseUrl, allowPrivateNetwork, headers, dispatcherPolicy } =
|
||||
resolveProviderHttpRequestConfig({
|
||||
baseUrl: config.baseUrl,
|
||||
defaultBaseUrl: DEFAULT_VYDRA_BASE_URL,
|
||||
allowPrivateNetwork: false,
|
||||
defaultHeaders: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
provider: "vydra",
|
||||
capability: "audio",
|
||||
transport: "http",
|
||||
});
|
||||
|
||||
const { response, release } = await postJsonRequest({
|
||||
url: `${baseUrl}/models/${overrides.model ?? config.model}`,
|
||||
headers,
|
||||
body: {
|
||||
text: req.text,
|
||||
voice_id: overrides.voiceId ?? config.voiceId,
|
||||
},
|
||||
timeoutMs: req.timeoutMs,
|
||||
fetchFn,
|
||||
allowPrivateNetwork,
|
||||
dispatcherPolicy,
|
||||
});
|
||||
|
||||
try {
|
||||
await assertOkOrThrowHttpError(response, "Vydra speech synthesis failed");
|
||||
const payload = await response.json();
|
||||
const audioUrl = extractVydraResultUrls(payload, "audio")[0];
|
||||
if (!audioUrl) {
|
||||
throw new Error("Vydra speech synthesis response missing audio URL");
|
||||
}
|
||||
const audio = await downloadVydraAsset({
|
||||
url: audioUrl,
|
||||
kind: "audio",
|
||||
timeoutMs: req.timeoutMs,
|
||||
fetchFn,
|
||||
});
|
||||
return {
|
||||
audioBuffer: audio.buffer,
|
||||
outputFormat: audio.mimeType.includes("wav") ? "wav" : "mp3",
|
||||
fileExtension: audio.fileName.endsWith(".wav") ? ".wav" : ".mp3",
|
||||
voiceCompatible: false,
|
||||
};
|
||||
} finally {
|
||||
await release();
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue