Add native OpenClaw Athena Talk plugin
This commit is contained in:
+24
-7
@@ -6,13 +6,25 @@ function record(value) {
|
||||
? value
|
||||
: {};
|
||||
}
|
||||
function resolveModelProvider(cfg, requested) {
|
||||
const providers = record(record(cfg).models).providers;
|
||||
const requestedId = typeof requested === "string" ? requested.trim() : "";
|
||||
if (requestedId)
|
||||
return record(record(providers)[requestedId]);
|
||||
for (const id of ["athena", "llama-cpp", "openai"]) {
|
||||
const candidate = record(record(providers)[id]);
|
||||
if (typeof candidate.baseUrl === "string" && candidate.baseUrl.trim())
|
||||
return candidate;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
function resolveConfig(req) {
|
||||
const raw = record(req.providerConfig);
|
||||
const modelProviders = record(record(req.cfg).models).providers;
|
||||
const athena = record(record(modelProviders).athena);
|
||||
const modelProvider = resolveModelProvider(req.cfg, raw.modelProvider);
|
||||
return {
|
||||
baseUrl: String(raw.baseUrl || athena.baseUrl || "http://192.168.1.212:8081/v1").replace(/\/$/, ""),
|
||||
apiKey: String(raw.apiKey || athena.apiKey || ""),
|
||||
modelProvider: String(raw.modelProvider || ""),
|
||||
baseUrl: String(raw.baseUrl || modelProvider.baseUrl || "http://192.168.1.212:8081/v1").replace(/\/$/, ""),
|
||||
apiKey: String(raw.apiKey || modelProvider.apiKey || ""),
|
||||
voice: String(raw.voice || req.voice || "alloy"),
|
||||
language: String(raw.language || req.language || "de"),
|
||||
vadThreshold: Number(raw.vadThreshold ?? 0.018),
|
||||
@@ -145,6 +157,10 @@ class AthenaTalkBridge {
|
||||
sendAudio(chunk) {
|
||||
if (!this.isConnected() || chunk.length === 0)
|
||||
return;
|
||||
// Half-duplex by design: while Athena is transcribing, consulting the
|
||||
// agent, synthesizing, or playing a reply, microphone input is ignored.
|
||||
// This prevents speaker feedback and background noise from cancelling the
|
||||
// response that is currently being delivered.
|
||||
if (this.transcribing || this.active)
|
||||
return;
|
||||
const voiced = pcmRms(chunk) >= this.cfg.vadThreshold;
|
||||
@@ -225,7 +241,8 @@ class AthenaTalkBridge {
|
||||
}
|
||||
async transcribe(pcm) {
|
||||
const form = new FormData();
|
||||
form.append("file", new Blob([wavFromPcm16(pcm)], { type: "audio/wav" }), "talk.wav");
|
||||
const wavBytes = Uint8Array.from(wavFromPcm16(pcm));
|
||||
form.append("file", new Blob([wavBytes], { type: "audio/wav" }), "talk.wav");
|
||||
form.append("model", "whisper-1");
|
||||
form.append("language", this.cfg.language);
|
||||
const response = await fetch(`${this.cfg.baseUrl}/audio/transcriptions`, {
|
||||
@@ -303,6 +320,7 @@ class AthenaTalkBridge {
|
||||
const response = await fetch(`${this.cfg.baseUrl}/audio/speech`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", ...this.authHeaders() },
|
||||
// Athena normalizes the request and serves it through Qwen3-TTS.
|
||||
body: JSON.stringify({ voice: this.cfg.voice, input: text, response_format: "wav" }),
|
||||
signal,
|
||||
});
|
||||
@@ -338,8 +356,7 @@ export default definePluginEntry({
|
||||
resolveConfig: ({ rawConfig }) => record(rawConfig),
|
||||
isConfigured: ({ providerConfig, cfg }) => {
|
||||
const raw = record(providerConfig);
|
||||
const athena = record(record(record(cfg).models).providers).athena;
|
||||
return Boolean(raw.baseUrl || record(athena).baseUrl);
|
||||
return Boolean(raw.baseUrl || resolveModelProvider(cfg, raw.modelProvider).baseUrl);
|
||||
},
|
||||
createBridge: (req) => new AthenaTalkBridge(req, resolveConfig(req)),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user