@qwen-code/arena-agent 0.0.9-dev.8 → 0.0.10-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +54 -19
- package/README.md +218 -45
- package/dist/agent.d.ts +4 -2
- package/dist/agent.js +167 -21
- package/dist/agent.js.map +1 -1
- package/dist/anthropic.d.ts +1 -0
- package/dist/anthropic.js +90 -23
- package/dist/anthropic.js.map +1 -1
- package/dist/attachments.d.ts +1 -1
- package/dist/attachments.js +16 -4
- package/dist/attachments.js.map +1 -1
- package/dist/brave-search.d.ts +10 -0
- package/dist/brave-search.js +238 -0
- package/dist/brave-search.js.map +1 -0
- package/dist/canonical-data.js +2 -2
- package/dist/canonical-data.js.map +1 -1
- package/dist/cli.js +64 -18
- package/dist/cli.js.map +1 -1
- package/dist/code-tools.d.ts +0 -5
- package/dist/code-tools.js +29 -353
- package/dist/code-tools.js.map +1 -1
- package/dist/compaction.js +8 -2
- package/dist/compaction.js.map +1 -1
- package/dist/config.d.ts +30 -0
- package/dist/config.js +57 -3
- package/dist/config.js.map +1 -1
- package/dist/fetch-page.d.ts +1 -0
- package/dist/fetch-page.js +1 -1
- package/dist/fetch-page.js.map +1 -1
- package/dist/history.d.ts +3 -2
- package/dist/history.js +7 -2
- package/dist/history.js.map +1 -1
- package/dist/image-input-fallback.d.ts +9 -0
- package/dist/image-input-fallback.js +47 -0
- package/dist/image-input-fallback.js.map +1 -0
- package/dist/image.d.ts +16 -0
- package/dist/image.js +380 -0
- package/dist/image.js.map +1 -0
- package/dist/mock.d.ts +1 -0
- package/dist/mock.js +38 -0
- package/dist/mock.js.map +1 -1
- package/dist/model-capabilities.js +12 -2
- package/dist/model-capabilities.js.map +1 -1
- package/dist/model-diagnostics.js +16 -5
- package/dist/model-diagnostics.js.map +1 -1
- package/dist/model-router-media.d.ts +26 -0
- package/dist/model-router-media.js +274 -0
- package/dist/model-router-media.js.map +1 -0
- package/dist/openai-responses.d.ts +28 -0
- package/dist/openai-responses.js +473 -0
- package/dist/openai-responses.js.map +1 -0
- package/dist/process-manager.d.ts +38 -0
- package/dist/process-manager.js +486 -0
- package/dist/process-manager.js.map +1 -0
- package/dist/prompts.js +3 -1
- package/dist/prompts.js.map +1 -1
- package/dist/provider.d.ts +7 -0
- package/dist/provider.js +291 -27
- package/dist/provider.js.map +1 -1
- package/dist/routify-web-fetch.d.ts +17 -0
- package/dist/routify-web-fetch.js +188 -0
- package/dist/routify-web-fetch.js.map +1 -0
- package/dist/sdk.d.ts +9 -2
- package/dist/sdk.js +5 -2
- package/dist/sdk.js.map +1 -1
- package/dist/server.d.ts +3 -0
- package/dist/server.js +112 -21
- package/dist/server.js.map +1 -1
- package/dist/spec.js +15 -1
- package/dist/spec.js.map +1 -1
- package/dist/tools.d.ts +2 -1
- package/dist/tools.js +250 -340
- package/dist/tools.js.map +1 -1
- package/dist/tui.d.ts +3 -0
- package/dist/tui.js +18 -11
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +17 -6
- package/dist/voice-registry.d.ts +30 -0
- package/dist/voice-registry.js +82 -0
- package/dist/voice-registry.js.map +1 -0
- package/dist/web/app.js +67 -6
- package/dist/web/app.js.map +1 -1
- package/docs/BINARY_DISTRIBUTION.md +25 -18
- package/docs/MODES_AND_RELEASES.md +8 -4
- package/docs/SDK.md +28 -4
- package/package.json +21 -3
- package/web/styles.css +28 -7
package/dist/tools.js
CHANGED
|
@@ -2,19 +2,26 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { existsSync, mkdirSync, realpathSync, } from "node:fs";
|
|
3
3
|
import { open, readFile, realpath, stat, writeFile, } from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { setTimeout as delay } from "node:timers/promises";
|
|
6
5
|
import { Ajv } from "ajv";
|
|
7
6
|
import addFormats from "ajv-formats";
|
|
8
7
|
import { detect as detectEncoding } from "chardet";
|
|
9
8
|
import iconv from "iconv-lite";
|
|
10
9
|
import mime from "mime/lite";
|
|
11
10
|
import { AskUserHandlerError } from "./ask-user.js";
|
|
11
|
+
import { executeBraveImageSearch, executeBraveWebSearch, } from "./brave-search.js";
|
|
12
12
|
import { CodeToolExecutor } from "./code-tools.js";
|
|
13
13
|
import { supportsImageInput } from "./config.js";
|
|
14
14
|
import { extractStructuredDocument, isStructuredDocument, } from "./document-reader.js";
|
|
15
15
|
import { FetchPageReader } from "./fetch-page.js";
|
|
16
|
+
import { generatedImageBytesForExtension, prepareImageForModel, } from "./image.js";
|
|
17
|
+
import { imageTextFromFallbackModel } from "./image-input-fallback.js";
|
|
16
18
|
import { isCodeArenaMode } from "./mode.js";
|
|
19
|
+
import { generateImageWithModelRouter, generateSpeechWithModelRouter, } from "./model-router-media.js";
|
|
20
|
+
import { processManagerForWorkspace, } from "./process-manager.js";
|
|
21
|
+
import { HttpModelClient } from "./provider.js";
|
|
22
|
+
import { RoutifyFetchPageReader, routifyWebFetchEnabled, } from "./routify-web-fetch.js";
|
|
17
23
|
import { toolDefinitions } from "./spec.js";
|
|
24
|
+
import { voiceRegistryForWorkspace, } from "./voice-registry.js";
|
|
18
25
|
const knownTextApplicationMimeTypes = new Set([
|
|
19
26
|
"application/ecmascript",
|
|
20
27
|
"application/javascript",
|
|
@@ -356,58 +363,6 @@ function record(value) {
|
|
|
356
363
|
function stringValue(value) {
|
|
357
364
|
return typeof value === "string" ? value : "";
|
|
358
365
|
}
|
|
359
|
-
function dashScopeApiKey() {
|
|
360
|
-
const apiKey = process.env.DASHSCOPE_API_KEY;
|
|
361
|
-
if (!apiKey) {
|
|
362
|
-
throw new Error("DashScope tools require DASHSCOPE_API_KEY");
|
|
363
|
-
}
|
|
364
|
-
return apiKey;
|
|
365
|
-
}
|
|
366
|
-
function dashScopeRoot() {
|
|
367
|
-
return (process.env.DASHSCOPE_API_BASE ||
|
|
368
|
-
process.env.DASHSCOPE_BASE_URL ||
|
|
369
|
-
"https://dashscope.aliyuncs.com")
|
|
370
|
-
.replace(/\/+$/, "")
|
|
371
|
-
.replace(/\/(?:api\/v1|compatible-mode\/v1)$/i, "");
|
|
372
|
-
}
|
|
373
|
-
function dashScopeUrl(protocol, pathname) {
|
|
374
|
-
const prefix = protocol === "api" ? "/api/v1" : "/compatible-mode/v1";
|
|
375
|
-
const normalized = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
376
|
-
return `${dashScopeRoot()}${prefix}${normalized}`;
|
|
377
|
-
}
|
|
378
|
-
async function dashScopeFetch(protocol, pathname, init, label) {
|
|
379
|
-
const headers = new Headers(init.headers);
|
|
380
|
-
headers.set("authorization", `Bearer ${dashScopeApiKey()}`);
|
|
381
|
-
const response = await fetch(dashScopeUrl(protocol, pathname), {
|
|
382
|
-
...init,
|
|
383
|
-
headers,
|
|
384
|
-
});
|
|
385
|
-
if (response.ok)
|
|
386
|
-
return response;
|
|
387
|
-
const body = await response.text();
|
|
388
|
-
let detail = body;
|
|
389
|
-
try {
|
|
390
|
-
const payload = record(JSON.parse(body));
|
|
391
|
-
detail =
|
|
392
|
-
stringValue(payload.message) ||
|
|
393
|
-
stringValue(record(payload.error).message) ||
|
|
394
|
-
body;
|
|
395
|
-
}
|
|
396
|
-
catch {
|
|
397
|
-
// Keep the response text as the diagnostic.
|
|
398
|
-
}
|
|
399
|
-
throw new Error(`${label} failed (${response.status} ${response.statusText}): ${detail.slice(0, 2000)}`);
|
|
400
|
-
}
|
|
401
|
-
async function dashScopeJson(protocol, pathname, init, label) {
|
|
402
|
-
const response = await dashScopeFetch(protocol, pathname, init, label);
|
|
403
|
-
return record(await response.json());
|
|
404
|
-
}
|
|
405
|
-
function responseMessage(payload) {
|
|
406
|
-
return (stringValue(payload.message) ||
|
|
407
|
-
stringValue(record(payload.output).message) ||
|
|
408
|
-
stringValue(payload.code) ||
|
|
409
|
-
"unknown provider error");
|
|
410
|
-
}
|
|
411
366
|
async function responseBytes(response, limit = 4 * 1024 * 1024) {
|
|
412
367
|
const bytes = Buffer.from(await response.arrayBuffer());
|
|
413
368
|
if (bytes.length > limit) {
|
|
@@ -422,24 +377,37 @@ class ArenaToolExecutor {
|
|
|
422
377
|
validators = new Map();
|
|
423
378
|
pendingVisualContent = new Map();
|
|
424
379
|
askUserHandler;
|
|
380
|
+
imageInputFallbackClient;
|
|
425
381
|
signal;
|
|
426
382
|
workspaceContext;
|
|
427
383
|
pageReader;
|
|
384
|
+
routifyPageReader;
|
|
385
|
+
processManager;
|
|
386
|
+
voiceRegistry;
|
|
428
387
|
ajv = new Ajv({ allErrors: true, strict: false });
|
|
429
388
|
// ask_user needs a handler: TUI/WebUI collect real user input, while
|
|
430
|
-
// headless `-p` may delegate to a configured remote User Agent.
|
|
431
|
-
//
|
|
389
|
+
// headless `-p` may delegate to a configured remote User Agent. add_voice
|
|
390
|
+
// remains available without one and deterministically registers a fallback.
|
|
432
391
|
askUserEnabled;
|
|
433
392
|
constructor(config, options = {}) {
|
|
434
393
|
this.config = config;
|
|
435
394
|
this.workspace = path.resolve(config.workspace);
|
|
436
395
|
mkdirSync(this.workspace, { recursive: true });
|
|
437
396
|
this.workspaceReal = realpathSync(this.workspace);
|
|
397
|
+
this.processManager = processManagerForWorkspace(this.workspaceReal);
|
|
398
|
+
this.voiceRegistry = voiceRegistryForWorkspace(this.workspaceReal);
|
|
438
399
|
this.askUserHandler = options.askUserHandler;
|
|
439
400
|
this.askUserEnabled = Boolean(options.askUserHandler);
|
|
401
|
+
if (config.imageInputFallbackModel?.trim()) {
|
|
402
|
+
this.imageInputFallbackClient =
|
|
403
|
+
options.modelClient || new HttpModelClient(config);
|
|
404
|
+
}
|
|
440
405
|
this.signal = options.signal;
|
|
441
406
|
this.workspaceContext = options.workspaceContext;
|
|
442
407
|
this.pageReader = new FetchPageReader({ signal: options.signal });
|
|
408
|
+
this.routifyPageReader = new RoutifyFetchPageReader({
|
|
409
|
+
signal: options.signal,
|
|
410
|
+
});
|
|
443
411
|
addFormats(this.ajv);
|
|
444
412
|
for (const definition of toolDefinitions(this.workspaceContext)) {
|
|
445
413
|
this.validators.set(definition.function.name, this.ajv.compile(definition.function.parameters));
|
|
@@ -447,7 +415,7 @@ class ArenaToolExecutor {
|
|
|
447
415
|
}
|
|
448
416
|
// The tool schemas offered to the model for this session. Identical to the
|
|
449
417
|
// canonical trace set, adjusted for the selected workspace runtime and minus
|
|
450
|
-
// ask_user when no handler is wired.
|
|
418
|
+
// ask_user when no handler is wired. add_voice stays available headlessly.
|
|
451
419
|
availableTools() {
|
|
452
420
|
let definitions = toolDefinitions(this.workspaceContext);
|
|
453
421
|
if (!this.askUserEnabled) {
|
|
@@ -543,12 +511,16 @@ class ArenaToolExecutor {
|
|
|
543
511
|
: "/home/user")), Number(values.timeout || 30));
|
|
544
512
|
case "ask_user":
|
|
545
513
|
return this.askUser(values.questions, toolCallId, context);
|
|
514
|
+
case "add_voice":
|
|
515
|
+
return this.addVoice(String(values.text), String(values.language), values.voice_identity, toolCallId, context);
|
|
546
516
|
case "edit_file":
|
|
547
517
|
return this.editFile(String(values.path), String(values.old_text), String(values.new_text));
|
|
548
518
|
case "read_file":
|
|
549
519
|
return this.readWorkspaceFile(String(values.path), toolCallId);
|
|
550
520
|
case "fetch_page":
|
|
551
|
-
return
|
|
521
|
+
return routifyWebFetchEnabled()
|
|
522
|
+
? this.routifyPageReader.read(String(values.url), Number(values.chunkIndex || 0))
|
|
523
|
+
: this.pageReader.read(String(values.url), Number(values.chunkIndex || 0));
|
|
552
524
|
case "web_search":
|
|
553
525
|
return this.webSearch(String(values.query), String(values.depth));
|
|
554
526
|
case "write_file":
|
|
@@ -557,10 +529,28 @@ class ArenaToolExecutor {
|
|
|
557
529
|
return this.imageSearch(String(values.query), Number(values.count));
|
|
558
530
|
case "present_file":
|
|
559
531
|
return this.presentFile(String(values.path));
|
|
532
|
+
case "stop_process":
|
|
533
|
+
return this.processManager.stop(String(values.process_id));
|
|
534
|
+
case "start_process":
|
|
535
|
+
return this.startProcess(String(values.name || ""), String(values.command), String(values.cwd ||
|
|
536
|
+
(this.workspaceContext
|
|
537
|
+
? path.resolve(this.workspaceContext.cwd)
|
|
538
|
+
: "/home/user")), Number(values.startup_wait || 5));
|
|
560
539
|
case "generate_image":
|
|
561
540
|
return this.generateImage(String(values.file_path), String(values.prompt), values.images);
|
|
562
541
|
case "generate_speech":
|
|
563
|
-
return this.generateSpeech(String(values.file_path), String(values.text), String(values.
|
|
542
|
+
return this.generateSpeech(String(values.file_path), String(values.text), String(values.voice_id), values.language === undefined ? undefined : String(values.language));
|
|
543
|
+
case "get_process_output":
|
|
544
|
+
return this.processManager.get({
|
|
545
|
+
processId: String(values.process_id),
|
|
546
|
+
tailLines: Number(values.tail_lines || 200),
|
|
547
|
+
waitFor: values.wait_for,
|
|
548
|
+
waitPattern: values.wait_pattern === undefined
|
|
549
|
+
? undefined
|
|
550
|
+
: String(values.wait_pattern),
|
|
551
|
+
waitTimeoutSeconds: Number(values.wait_timeout || 60),
|
|
552
|
+
signal: this.signal,
|
|
553
|
+
});
|
|
564
554
|
default:
|
|
565
555
|
throw new Error(`unknown tool: ${name}`);
|
|
566
556
|
}
|
|
@@ -569,12 +559,15 @@ class ArenaToolExecutor {
|
|
|
569
559
|
if (!raw)
|
|
570
560
|
throw new Error("path must not be empty");
|
|
571
561
|
let candidate;
|
|
572
|
-
if (
|
|
562
|
+
if (raw === "/home/user") {
|
|
573
563
|
candidate = this.workspace;
|
|
574
564
|
}
|
|
575
|
-
else if (
|
|
565
|
+
else if (raw.startsWith("/home/user/")) {
|
|
576
566
|
candidate = path.join(this.workspace, raw.slice("/home/user/".length));
|
|
577
567
|
}
|
|
568
|
+
else if (!this.workspaceContext && raw === "~") {
|
|
569
|
+
candidate = this.workspace;
|
|
570
|
+
}
|
|
578
571
|
else if (!this.workspaceContext && raw.startsWith("~/")) {
|
|
579
572
|
candidate = path.join(this.workspace, raw.slice(2));
|
|
580
573
|
}
|
|
@@ -620,6 +613,17 @@ class ArenaToolExecutor {
|
|
|
620
613
|
}
|
|
621
614
|
return virtualized;
|
|
622
615
|
}
|
|
616
|
+
sandboxEnvironment() {
|
|
617
|
+
const env = {
|
|
618
|
+
...process.env,
|
|
619
|
+
ARENA_WORKSPACE: this.workspace,
|
|
620
|
+
LANG: "C.UTF-8",
|
|
621
|
+
LC_ALL: "C.UTF-8",
|
|
622
|
+
};
|
|
623
|
+
delete env.FORCE_COLOR;
|
|
624
|
+
delete env.CLICOLOR_FORCE;
|
|
625
|
+
return env;
|
|
626
|
+
}
|
|
623
627
|
async bash(command, cwd, timeoutSeconds) {
|
|
624
628
|
const workdir = await this.confinedPath(cwd);
|
|
625
629
|
if (!(await stat(workdir)).isDirectory()) {
|
|
@@ -631,19 +635,7 @@ class ArenaToolExecutor {
|
|
|
631
635
|
? command
|
|
632
636
|
: command.replaceAll("/home/user", "\"$ARENA_WORKSPACE\"");
|
|
633
637
|
return new Promise((resolve, reject) => {
|
|
634
|
-
const env =
|
|
635
|
-
...process.env,
|
|
636
|
-
HOME: this.workspace,
|
|
637
|
-
ARENA_WORKSPACE: this.workspace,
|
|
638
|
-
// 生产 sandbox 是 C collation(参考 find|sort 为逐字节序),宿主
|
|
639
|
-
// 常见 en_US.UTF-8 会翻转大小写次序;统一成 C.UTF-8 对齐生产。
|
|
640
|
-
LANG: "C.UTF-8",
|
|
641
|
-
LC_ALL: "C.UTF-8",
|
|
642
|
-
};
|
|
643
|
-
// 交互终端(node --test、npm 等)会向子进程注入强制着色变量,
|
|
644
|
-
// 管道输出被 ANSI 色码污染;生产轨迹无色码,剥离之。
|
|
645
|
-
delete env.FORCE_COLOR;
|
|
646
|
-
delete env.CLICOLOR_FORCE;
|
|
638
|
+
const env = this.sandboxEnvironment();
|
|
647
639
|
// detached:管道成员(npm、tail…)与 bash 同进程组。只杀 bash 会留下
|
|
648
640
|
// 孤儿持有 stdout/stderr 管道,close 永不触发,整个 turn 挂死;
|
|
649
641
|
// 必须杀整组,并在宽限期后强制结算兜底。
|
|
@@ -658,7 +650,6 @@ class ArenaToolExecutor {
|
|
|
658
650
|
let stderr = "";
|
|
659
651
|
let timedOut = false;
|
|
660
652
|
let settled = false;
|
|
661
|
-
let killRequested = false;
|
|
662
653
|
let forceSettle;
|
|
663
654
|
const killTree = (signal) => {
|
|
664
655
|
try {
|
|
@@ -677,33 +668,26 @@ class ArenaToolExecutor {
|
|
|
677
668
|
throw error;
|
|
678
669
|
}
|
|
679
670
|
};
|
|
680
|
-
const finish = (code) => {
|
|
671
|
+
const finish = (code, signal) => {
|
|
681
672
|
if (settled)
|
|
682
673
|
return;
|
|
683
674
|
settled = true;
|
|
684
675
|
clearTimeout(timeout);
|
|
685
676
|
if (forceSettle)
|
|
686
677
|
clearTimeout(forceSettle);
|
|
687
|
-
//
|
|
688
|
-
//
|
|
689
|
-
//
|
|
690
|
-
|
|
691
|
-
if (killRequested)
|
|
692
|
-
killTree("SIGKILL");
|
|
678
|
+
// bash is one-shot in v2. Any descendant left in the process group is
|
|
679
|
+
// cleaned up even when the shell itself exited normally; persistent
|
|
680
|
+
// servers belong in start_process.
|
|
681
|
+
killTree("SIGKILL");
|
|
693
682
|
this.signal?.removeEventListener("abort", abort);
|
|
694
683
|
if (this.signal?.aborted) {
|
|
695
684
|
reject(this.signal.reason || new Error("operation cancelled"));
|
|
696
685
|
return;
|
|
697
686
|
}
|
|
698
|
-
// 生产契约:status 恒为 "completed"(包括非零退出码),超时无参考样本,
|
|
699
|
-
// 按最小猜测同样返回 completed + exit_code null,stderr 附超时说明。
|
|
700
|
-
if (timedOut) {
|
|
701
|
-
stderr += (stderr ? "\n" : "") + `command exceeded ${timeoutSeconds}s`;
|
|
702
|
-
}
|
|
703
687
|
const out = capBashOutput(this.virtualizeBashOutput(stdout));
|
|
704
688
|
const err = capBashOutput(this.virtualizeBashOutput(stderr));
|
|
705
689
|
resolve({
|
|
706
|
-
status: "completed",
|
|
690
|
+
status: timedOut ? "timeout" : signal ? "killed" : "completed",
|
|
707
691
|
stderr: err.text,
|
|
708
692
|
stdout: out.text,
|
|
709
693
|
exit_code: timedOut ? null : code,
|
|
@@ -717,17 +701,15 @@ class ArenaToolExecutor {
|
|
|
717
701
|
const armForceSettle = () => {
|
|
718
702
|
forceSettle ??= setTimeout(() => {
|
|
719
703
|
killTree("SIGKILL");
|
|
720
|
-
finish(null);
|
|
704
|
+
finish(null, "SIGKILL");
|
|
721
705
|
}, 2000);
|
|
722
706
|
};
|
|
723
707
|
const timeout = setTimeout(() => {
|
|
724
708
|
timedOut = true;
|
|
725
|
-
killRequested = true;
|
|
726
709
|
killTree("SIGKILL");
|
|
727
710
|
armForceSettle();
|
|
728
711
|
}, timeoutSeconds * 1000);
|
|
729
712
|
const abort = () => {
|
|
730
|
-
killRequested = true;
|
|
731
713
|
killTree("SIGTERM");
|
|
732
714
|
armForceSettle();
|
|
733
715
|
};
|
|
@@ -748,7 +730,24 @@ class ArenaToolExecutor {
|
|
|
748
730
|
this.signal?.removeEventListener("abort", abort);
|
|
749
731
|
reject(error);
|
|
750
732
|
});
|
|
751
|
-
child.once("close", (code) => finish(code));
|
|
733
|
+
child.once("close", (code, signal) => finish(code, signal));
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
async startProcess(name, command, cwd, startupWaitSeconds) {
|
|
737
|
+
const workdir = await this.confinedPath(cwd);
|
|
738
|
+
if (!(await stat(workdir)).isDirectory()) {
|
|
739
|
+
throw new Error(`cwd is not a directory: ${cwd}`);
|
|
740
|
+
}
|
|
741
|
+
const translatedCommand = this.workspaceContext
|
|
742
|
+
? command
|
|
743
|
+
: command.replaceAll("/home/user", '"$ARENA_WORKSPACE"');
|
|
744
|
+
return this.processManager.start({
|
|
745
|
+
name,
|
|
746
|
+
command: translatedCommand,
|
|
747
|
+
cwd: workdir,
|
|
748
|
+
startupWaitSeconds,
|
|
749
|
+
env: this.sandboxEnvironment(),
|
|
750
|
+
signal: this.signal,
|
|
752
751
|
});
|
|
753
752
|
}
|
|
754
753
|
async askUser(questions, toolCallId, context) {
|
|
@@ -768,6 +767,58 @@ class ArenaToolExecutor {
|
|
|
768
767
|
"Proceed with reasonable defaults and state any assumptions you make.",
|
|
769
768
|
};
|
|
770
769
|
}
|
|
770
|
+
async addVoice(text, language, voiceIdentity = {}, toolCallId, context) {
|
|
771
|
+
if (!this.askUserHandler) {
|
|
772
|
+
const selection = this.voiceRegistry.registerVirtual(language, voiceIdentity);
|
|
773
|
+
return {
|
|
774
|
+
status: "success",
|
|
775
|
+
message: `Battle "${selection.battleId}": no interactive voice picker was ` +
|
|
776
|
+
`available, so a default voice was selected. Pass voice_id ` +
|
|
777
|
+
`"${selection.voiceId}" to generate_speech to speak with it.`,
|
|
778
|
+
voice_id: selection.voiceId,
|
|
779
|
+
battle_id: selection.battleId,
|
|
780
|
+
virtual: true,
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
const battle = await this.voiceRegistry.audition(text, language, voiceIdentity, this.signal);
|
|
784
|
+
const response = await this.askUserHandler(toolCallId, [
|
|
785
|
+
{
|
|
786
|
+
id: `voice-battle-${battle.battleId}`,
|
|
787
|
+
question: "Which voice should be used for this speaker?",
|
|
788
|
+
options: battle.candidates.map((candidate) => ({
|
|
789
|
+
id: candidate.id,
|
|
790
|
+
label: candidate.label,
|
|
791
|
+
description: "Play the audition, then select this voice.",
|
|
792
|
+
audioDataUrl: candidate.audioDataUrl,
|
|
793
|
+
})),
|
|
794
|
+
allowCustomResponse: false,
|
|
795
|
+
},
|
|
796
|
+
], context
|
|
797
|
+
? {
|
|
798
|
+
...context,
|
|
799
|
+
signal: this.signal || context.signal,
|
|
800
|
+
}
|
|
801
|
+
: undefined);
|
|
802
|
+
if (response.status === "needs_input")
|
|
803
|
+
return response;
|
|
804
|
+
if (response.skipped === true) {
|
|
805
|
+
return { status: "error", message: "The user skipped voice selection." };
|
|
806
|
+
}
|
|
807
|
+
const answers = Array.isArray(response.answers) ? response.answers : [];
|
|
808
|
+
const selectedOptionId = record(answers[0]).selectedOptionId;
|
|
809
|
+
if (typeof selectedOptionId !== "string") {
|
|
810
|
+
throw new Error("voice selection did not choose a candidate");
|
|
811
|
+
}
|
|
812
|
+
const voiceId = this.voiceRegistry.register(battle, selectedOptionId);
|
|
813
|
+
return {
|
|
814
|
+
status: "success",
|
|
815
|
+
message: `Battle "${battle.battleId}": the user selected a voice. ` +
|
|
816
|
+
`Pass voice_id "${voiceId}" to generate_speech to speak with it. ` +
|
|
817
|
+
"Continue the task now — if you need another voice, call add_voice again immediately.",
|
|
818
|
+
voice_id: voiceId,
|
|
819
|
+
battle_id: battle.battleId,
|
|
820
|
+
};
|
|
821
|
+
}
|
|
771
822
|
async editFile(rawPath, oldText, newText) {
|
|
772
823
|
const target = await this.confinedPath(rawPath);
|
|
773
824
|
const source = await readFile(target, "utf8");
|
|
@@ -827,7 +878,11 @@ class ArenaToolExecutor {
|
|
|
827
878
|
if (kind === "image" && fileSize > 20 * 1024 * 1024) {
|
|
828
879
|
throw new Error(`image exceeds 20 MB: ${rawPath}`);
|
|
829
880
|
}
|
|
830
|
-
|
|
881
|
+
const mainModelSupportsImages = kind === "image" ? supportsImageInput(this.config) : false;
|
|
882
|
+
const fallbackModel = mainModelSupportsImages
|
|
883
|
+
? undefined
|
|
884
|
+
: this.config.imageInputFallbackModel?.trim();
|
|
885
|
+
if (kind === "image" && !mainModelSupportsImages && !fallbackModel) {
|
|
831
886
|
return unsupportedImageResult(path.basename(target));
|
|
832
887
|
}
|
|
833
888
|
let data;
|
|
@@ -877,9 +932,37 @@ class ArenaToolExecutor {
|
|
|
877
932
|
mimeTypes[extension] ||
|
|
878
933
|
"application/octet-stream";
|
|
879
934
|
if (kind === "image") {
|
|
935
|
+
const prepared = await prepareImageForModel(data, mimeType);
|
|
936
|
+
if (!prepared.dataUri) {
|
|
937
|
+
return {
|
|
938
|
+
status: "error",
|
|
939
|
+
message: `Image could not be provided to the model: ${rawPath} ` +
|
|
940
|
+
`(${prepared.omittedReason || "exceeds the inline image limits"}).`,
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
if (fallbackModel) {
|
|
944
|
+
if (!this.imageInputFallbackClient) {
|
|
945
|
+
throw new Error("fallback image model client is unavailable");
|
|
946
|
+
}
|
|
947
|
+
const content = await imageTextFromFallbackModel(this.imageInputFallbackClient, {
|
|
948
|
+
model: fallbackModel,
|
|
949
|
+
fileName: this.relativePath(target),
|
|
950
|
+
mimeType: prepared.mimeType || mimeType,
|
|
951
|
+
dataUri: prepared.dataUri,
|
|
952
|
+
signal: this.signal,
|
|
953
|
+
});
|
|
954
|
+
return {
|
|
955
|
+
kind: "text",
|
|
956
|
+
source_kind: "image",
|
|
957
|
+
path: this.relativePath(target),
|
|
958
|
+
lines: content.split("\n").length,
|
|
959
|
+
size: Buffer.byteLength(content, "utf8"),
|
|
960
|
+
content,
|
|
961
|
+
};
|
|
962
|
+
}
|
|
880
963
|
this.pendingVisualContent.set(toolCallId, [
|
|
881
964
|
{
|
|
882
|
-
image:
|
|
965
|
+
image: prepared.dataUri,
|
|
883
966
|
},
|
|
884
967
|
]);
|
|
885
968
|
return [{ type: "text", text: imageResultPlaceholder }];
|
|
@@ -898,82 +981,13 @@ class ArenaToolExecutor {
|
|
|
898
981
|
return { status: "success" };
|
|
899
982
|
}
|
|
900
983
|
async webSearch(query, depth) {
|
|
901
|
-
|
|
902
|
-
const searchStrategy = depth === "1" ? "turbo" : "max";
|
|
903
|
-
const payload = await dashScopeJson("api", "/services/aigc/text-generation/generation", {
|
|
904
|
-
method: "POST",
|
|
905
|
-
headers: { "content-type": "application/json" },
|
|
906
|
-
body: JSON.stringify({
|
|
907
|
-
model,
|
|
908
|
-
input: {
|
|
909
|
-
messages: [{ role: "user", content: query }],
|
|
910
|
-
},
|
|
911
|
-
parameters: {
|
|
912
|
-
enable_search: true,
|
|
913
|
-
result_format: "message",
|
|
914
|
-
search_options: {
|
|
915
|
-
forced_search: true,
|
|
916
|
-
enable_source: true,
|
|
917
|
-
enable_citation: true,
|
|
918
|
-
citation_format: "[<number>]",
|
|
919
|
-
search_strategy: searchStrategy,
|
|
920
|
-
},
|
|
921
|
-
},
|
|
922
|
-
}),
|
|
923
|
-
signal: this.signal,
|
|
924
|
-
}, "web_search");
|
|
925
|
-
const output = record(payload.output);
|
|
926
|
-
const searchInfo = record(output.search_info);
|
|
927
|
-
const searchResults = Array.isArray(searchInfo.search_results)
|
|
928
|
-
? searchInfo.search_results
|
|
929
|
-
: [];
|
|
930
|
-
const limit = { "1": 3, "2": 5, "3": 8 }[depth] || 5;
|
|
931
|
-
// 参考轨迹条目:{id, url, title, description[, pageAge]};
|
|
932
|
-
// DashScope 无 pageAge,按可选键省略。
|
|
933
|
-
return {
|
|
934
|
-
status: "success",
|
|
935
|
-
results: searchResults.slice(0, limit).map((value, index) => {
|
|
936
|
-
const item = record(value);
|
|
937
|
-
return {
|
|
938
|
-
id: Number(item.index) || index + 1,
|
|
939
|
-
url: stringValue(item.url),
|
|
940
|
-
title: stringValue(item.title),
|
|
941
|
-
description: stringValue(item.content) ||
|
|
942
|
-
stringValue(item.snippet) ||
|
|
943
|
-
stringValue(item.summary),
|
|
944
|
-
};
|
|
945
|
-
}),
|
|
946
|
-
};
|
|
984
|
+
return executeBraveWebSearch(query, depth, { signal: this.signal });
|
|
947
985
|
}
|
|
948
986
|
async imageSearch(query, count) {
|
|
949
|
-
const
|
|
950
|
-
const payload = await dashScopeJson("compatible", "/responses", {
|
|
951
|
-
method: "POST",
|
|
952
|
-
headers: { "content-type": "application/json" },
|
|
953
|
-
body: JSON.stringify({
|
|
954
|
-
model,
|
|
955
|
-
input: query,
|
|
956
|
-
tools: [{ type: "web_search_image" }],
|
|
957
|
-
}),
|
|
987
|
+
const payload = await executeBraveImageSearch(query, count, {
|
|
958
988
|
signal: this.signal,
|
|
959
|
-
}
|
|
960
|
-
const
|
|
961
|
-
const toolCall = output
|
|
962
|
-
.map(record)
|
|
963
|
-
.find((item) => item.type === "web_search_image_call");
|
|
964
|
-
let results = [];
|
|
965
|
-
if (toolCall?.output) {
|
|
966
|
-
try {
|
|
967
|
-
const parsed = typeof toolCall.output === "string"
|
|
968
|
-
? JSON.parse(toolCall.output)
|
|
969
|
-
: toolCall.output;
|
|
970
|
-
if (Array.isArray(parsed))
|
|
971
|
-
results = parsed;
|
|
972
|
-
}
|
|
973
|
-
catch {
|
|
974
|
-
throw new Error("image_search returned malformed image results");
|
|
975
|
-
}
|
|
976
|
-
}
|
|
989
|
+
});
|
|
990
|
+
const results = Array.isArray(payload.results) ? payload.results : [];
|
|
977
991
|
const saved = [];
|
|
978
992
|
// 参考轨迹命名:image-search/<query slug 前 40 字符>-<序号>.<扩展名>。
|
|
979
993
|
const slug = query
|
|
@@ -984,35 +998,44 @@ class ArenaToolExecutor {
|
|
|
984
998
|
if (saved.length >= count)
|
|
985
999
|
break;
|
|
986
1000
|
const item = record(value);
|
|
987
|
-
const
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1001
|
+
const properties = record(item.properties);
|
|
1002
|
+
const thumbnail = record(item.thumbnail);
|
|
1003
|
+
const sourceUrl = stringValue(properties.url) || stringValue(thumbnail.src);
|
|
1004
|
+
const thumbnailUrl = stringValue(thumbnail.src) || sourceUrl;
|
|
991
1005
|
if (!sourceUrl)
|
|
992
1006
|
continue;
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1007
|
+
const downloadUrls = [...new Set([sourceUrl, thumbnailUrl])];
|
|
1008
|
+
for (const downloadUrl of downloadUrls) {
|
|
1009
|
+
try {
|
|
1010
|
+
const imageResponse = await fetch(downloadUrl, {
|
|
1011
|
+
signal: this.signal,
|
|
1012
|
+
});
|
|
1013
|
+
if (!imageResponse.ok)
|
|
1014
|
+
continue;
|
|
1015
|
+
const contentType = imageResponse.headers.get("content-type") || "image/jpeg";
|
|
1016
|
+
if (!contentType.toLowerCase().startsWith("image/"))
|
|
1017
|
+
continue;
|
|
1018
|
+
const normalizedType = contentType
|
|
1019
|
+
.split(";")[0]
|
|
1020
|
+
?.trim()
|
|
1021
|
+
.toLowerCase();
|
|
1022
|
+
const extension = Object.entries(mimeTypes).find(([, mime]) => mime === normalizedType)?.[0] || ".jpg";
|
|
1023
|
+
const target = await this.confinedPath(`image-search/${slug}-${saved.length + 1}${extension}`);
|
|
1024
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
1025
|
+
await writeFile(target, await responseBytes(imageResponse));
|
|
1026
|
+
saved.push({
|
|
1027
|
+
title: stringValue(item.title),
|
|
1028
|
+
file_path: this.relativePath(target),
|
|
1029
|
+
source_url: sourceUrl,
|
|
1030
|
+
thumbnail_url: thumbnailUrl,
|
|
1031
|
+
});
|
|
1032
|
+
break;
|
|
1033
|
+
}
|
|
1034
|
+
catch (error) {
|
|
1035
|
+
if (this.signal?.aborted)
|
|
1036
|
+
throw error;
|
|
1037
|
+
// Try the Brave proxy thumbnail when the original is inaccessible.
|
|
1038
|
+
}
|
|
1016
1039
|
}
|
|
1017
1040
|
}
|
|
1018
1041
|
return { status: "success", results: saved };
|
|
@@ -1025,13 +1048,13 @@ class ArenaToolExecutor {
|
|
|
1025
1048
|
}
|
|
1026
1049
|
async generateImage(rawPath, prompt, images) {
|
|
1027
1050
|
const target = await this.confinedPath(rawPath);
|
|
1028
|
-
|
|
1051
|
+
const extension = path.extname(target).toLowerCase();
|
|
1052
|
+
if (![".jpg", ".jpeg", ".png"].includes(extension)) {
|
|
1029
1053
|
throw new Error("file_path must end in .jpg, .jpeg, or .png");
|
|
1030
1054
|
}
|
|
1031
1055
|
mkdirSync(path.dirname(target), { recursive: true });
|
|
1032
|
-
const model = process.env.ARENA_IMAGE_MODEL || "
|
|
1033
|
-
const
|
|
1034
|
-
const content = [];
|
|
1056
|
+
const model = process.env.ARENA_IMAGE_MODEL || "gemini-2.5-flash-image";
|
|
1057
|
+
const references = [];
|
|
1035
1058
|
for (const imagePath of images || []) {
|
|
1036
1059
|
const source = await this.confinedPath(imagePath);
|
|
1037
1060
|
const extension = path.extname(source).toLowerCase();
|
|
@@ -1043,169 +1066,56 @@ class ArenaToolExecutor {
|
|
|
1043
1066
|
if (bytes.length > 20 * 1024 * 1024) {
|
|
1044
1067
|
throw new Error(`reference image exceeds 20 MB: ${imagePath}`);
|
|
1045
1068
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1069
|
+
references.push({
|
|
1070
|
+
bytes,
|
|
1071
|
+
fileName: path.basename(source),
|
|
1072
|
+
mimeType,
|
|
1048
1073
|
});
|
|
1049
1074
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
watermark: false,
|
|
1055
|
-
};
|
|
1056
|
-
if (isQwenImage2) {
|
|
1057
|
-
parameters.prompt_extend = true;
|
|
1058
|
-
}
|
|
1059
|
-
else {
|
|
1060
|
-
parameters.n = 1;
|
|
1061
|
-
if (!images?.length)
|
|
1062
|
-
parameters.thinking_mode = true;
|
|
1063
|
-
}
|
|
1064
|
-
const headers = {
|
|
1065
|
-
"content-type": "application/json",
|
|
1066
|
-
};
|
|
1067
|
-
if (!isQwenImage2)
|
|
1068
|
-
headers["x-dashscope-async"] = "enable";
|
|
1069
|
-
const submitted = await dashScopeJson("api", isQwenImage2
|
|
1070
|
-
? "/services/aigc/multimodal-generation/generation"
|
|
1071
|
-
: "/services/aigc/image-generation/generation", {
|
|
1072
|
-
method: "POST",
|
|
1073
|
-
headers,
|
|
1074
|
-
body: JSON.stringify({
|
|
1075
|
-
model,
|
|
1076
|
-
input: {
|
|
1077
|
-
messages: [{ role: "user", content }],
|
|
1078
|
-
},
|
|
1079
|
-
parameters,
|
|
1080
|
-
}),
|
|
1075
|
+
const generated = await generateImageWithModelRouter({
|
|
1076
|
+
images: references,
|
|
1077
|
+
model,
|
|
1078
|
+
prompt,
|
|
1081
1079
|
signal: this.signal,
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
: undefined;
|
|
1086
|
-
if (!isQwenImage2) {
|
|
1087
|
-
const taskId = stringValue(record(submitted.output).task_id);
|
|
1088
|
-
if (!taskId) {
|
|
1089
|
-
throw new Error(`generate_image returned no task_id: ${responseMessage(submitted)}`);
|
|
1090
|
-
}
|
|
1091
|
-
const timeoutMs = Number(process.env.DASHSCOPE_TASK_TIMEOUT_MS || 5 * 60 * 1000);
|
|
1092
|
-
const pollIntervalMs = Number(process.env.DASHSCOPE_TASK_POLL_INTERVAL_MS || 1000);
|
|
1093
|
-
const deadline = Date.now() + timeoutMs;
|
|
1094
|
-
while (Date.now() <= deadline) {
|
|
1095
|
-
const task = await dashScopeJson("api", `/tasks/${encodeURIComponent(taskId)}`, { method: "GET", signal: this.signal }, "generate_image status");
|
|
1096
|
-
const output = record(task.output);
|
|
1097
|
-
const taskStatus = stringValue(output.task_status).toUpperCase();
|
|
1098
|
-
if (taskStatus === "SUCCEEDED") {
|
|
1099
|
-
completed = task;
|
|
1100
|
-
break;
|
|
1101
|
-
}
|
|
1102
|
-
if (["FAILED", "CANCELED", "UNKNOWN"].includes(taskStatus)) {
|
|
1103
|
-
throw new Error(`generate_image task ${taskStatus}: ${responseMessage(task)}`);
|
|
1104
|
-
}
|
|
1105
|
-
if (Date.now() + pollIntervalMs > deadline)
|
|
1106
|
-
break;
|
|
1107
|
-
await delay(pollIntervalMs, undefined, { signal: this.signal });
|
|
1108
|
-
}
|
|
1109
|
-
if (!completed) {
|
|
1110
|
-
throw new Error(`generate_image timed out waiting for task ${taskId}`);
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
if (!completed) {
|
|
1114
|
-
throw new Error("generate_image returned no response");
|
|
1115
|
-
}
|
|
1116
|
-
const output = record(completed.output);
|
|
1117
|
-
const choices = Array.isArray(output.choices) ? output.choices : [];
|
|
1118
|
-
const message = record(record(choices[0]).message);
|
|
1119
|
-
const generatedContent = Array.isArray(message.content)
|
|
1120
|
-
? message.content
|
|
1121
|
-
: [];
|
|
1122
|
-
const generated = generatedContent
|
|
1123
|
-
.map(record)
|
|
1124
|
-
.find((item) => typeof item.image === "string");
|
|
1125
|
-
const legacyResults = Array.isArray(output.results) ? output.results : [];
|
|
1126
|
-
const imageUrl = stringValue(generated?.image) ||
|
|
1127
|
-
stringValue(record(legacyResults[0]).url);
|
|
1128
|
-
if (!imageUrl) {
|
|
1129
|
-
throw new Error("generate_image response did not include an image URL");
|
|
1130
|
-
}
|
|
1131
|
-
const imageResponse = await fetch(imageUrl, { signal: this.signal });
|
|
1132
|
-
if (!imageResponse.ok) {
|
|
1133
|
-
throw new Error(`image download failed (${imageResponse.status})`);
|
|
1134
|
-
}
|
|
1135
|
-
await writeFile(target, await responseBytes(imageResponse, 25 * 1024 * 1024));
|
|
1080
|
+
size: process.env.ARENA_IMAGE_SIZE || "1024*1024",
|
|
1081
|
+
});
|
|
1082
|
+
await writeFile(target, await generatedImageBytesForExtension(generated.bytes, generated.mimeType, extension));
|
|
1136
1083
|
return {
|
|
1137
1084
|
status: "success",
|
|
1138
1085
|
file_path: this.relativePath(target),
|
|
1139
1086
|
};
|
|
1140
1087
|
}
|
|
1141
|
-
async generateSpeech(rawPath, text,
|
|
1088
|
+
async generateSpeech(rawPath, text, voiceId, language) {
|
|
1142
1089
|
const target = await this.confinedPath(rawPath);
|
|
1143
1090
|
const extension = path.extname(target).toLowerCase();
|
|
1144
1091
|
const format = {
|
|
1145
1092
|
".mp3": "mp3",
|
|
1146
1093
|
".wav": "wav",
|
|
1094
|
+
".ogg": "opus",
|
|
1147
1095
|
".opus": "opus",
|
|
1148
1096
|
}[extension];
|
|
1149
1097
|
if (!format) {
|
|
1150
|
-
throw new Error("
|
|
1098
|
+
throw new Error("ModelRouter Doubao TTS output supports .mp3, .wav, .ogg, or .opus");
|
|
1151
1099
|
}
|
|
1152
1100
|
mkdirSync(path.dirname(target), { recursive: true });
|
|
1153
|
-
const
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
const voices = primaryLanguage === "en"
|
|
1157
|
-
? feminine
|
|
1158
|
-
? ["loongeva_v3.6", "longanhuan_v3.6"]
|
|
1159
|
-
: ["loongjohn", "longjielidou_v3.6"]
|
|
1160
|
-
: feminine
|
|
1161
|
-
? ["longanhuan_v3.6"]
|
|
1162
|
-
: ["longjielidou_v3.6"];
|
|
1163
|
-
const requestedIndex = Math.max(0, Math.trunc(Number(voiceIdentity.index || 0)));
|
|
1164
|
-
const voice = process.env.DASHSCOPE_SPEECH_VOICE ||
|
|
1165
|
-
voices[requestedIndex % voices.length] ||
|
|
1166
|
-
voices[0];
|
|
1167
|
-
const supportedHints = new Set([
|
|
1168
|
-
"ar",
|
|
1169
|
-
"de",
|
|
1170
|
-
"en",
|
|
1171
|
-
"es",
|
|
1172
|
-
"fil",
|
|
1173
|
-
"fr",
|
|
1174
|
-
"id",
|
|
1175
|
-
"it",
|
|
1176
|
-
"ja",
|
|
1177
|
-
"ko",
|
|
1178
|
-
"ms",
|
|
1179
|
-
"pt",
|
|
1180
|
-
"ru",
|
|
1181
|
-
"th",
|
|
1182
|
-
"vi",
|
|
1183
|
-
"zh",
|
|
1184
|
-
]);
|
|
1185
|
-
const input = {
|
|
1186
|
-
text,
|
|
1187
|
-
voice,
|
|
1188
|
-
format,
|
|
1189
|
-
sample_rate: 24000,
|
|
1190
|
-
};
|
|
1191
|
-
if (supportedHints.has(primaryLanguage)) {
|
|
1192
|
-
input.language_hints = [primaryLanguage];
|
|
1101
|
+
const registered = this.voiceRegistry.resolve(voiceId);
|
|
1102
|
+
if (!registered) {
|
|
1103
|
+
throw new Error(`unknown voice_id: ${voiceId}. Call add_voice and use the returned voice_id.`);
|
|
1193
1104
|
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
}, "generate_speech");
|
|
1200
|
-
const audioUrl = stringValue(record(record(payload.output).audio).url);
|
|
1201
|
-
if (!audioUrl) {
|
|
1202
|
-
throw new Error(`generate_speech returned no audio URL: ${responseMessage(payload)}`);
|
|
1203
|
-
}
|
|
1204
|
-
const audioResponse = await fetch(audioUrl, { signal: this.signal });
|
|
1205
|
-
if (!audioResponse.ok) {
|
|
1206
|
-
throw new Error(`audio download failed (${audioResponse.status})`);
|
|
1105
|
+
if (language &&
|
|
1106
|
+
language.toLowerCase().split(/[-_]/)[0] !==
|
|
1107
|
+
registered.language.toLowerCase().split(/[-_]/)[0]) {
|
|
1108
|
+
throw new Error(`voice ${voiceId} was registered for ${registered.language}; ` +
|
|
1109
|
+
`register a voice for ${language} with add_voice`);
|
|
1207
1110
|
}
|
|
1208
|
-
|
|
1111
|
+
const voice = process.env.ARENA_SPEECH_VOICE || registered.providerVoice;
|
|
1112
|
+
await writeFile(target, await generateSpeechWithModelRouter({
|
|
1113
|
+
format,
|
|
1114
|
+
model: process.env.ARENA_SPEECH_MODEL || "doubao-tts",
|
|
1115
|
+
signal: this.signal,
|
|
1116
|
+
text,
|
|
1117
|
+
voice,
|
|
1118
|
+
}));
|
|
1209
1119
|
return {
|
|
1210
1120
|
status: "success",
|
|
1211
1121
|
file_path: this.relativePath(target),
|