@lingjingai/scriptctl 0.63.4 → 0.63.5

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.
@@ -2320,6 +2320,12 @@ function run(bin, args) {
2320
2320
  // packages/review-agent/src/media/watch.ts
2321
2321
  import * as crypto from "node:crypto";
2322
2322
  import * as fs3 from "node:fs";
2323
+ import { z as z3 } from "zod";
2324
+
2325
+ // packages/review-agent/src/models.ts
2326
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
2327
+ import { APICallError } from "@ai-sdk/provider";
2328
+ import { z as z2 } from "zod";
2323
2329
 
2324
2330
  // packages/review-agent/src/media/net.ts
2325
2331
  import { ProxyAgent } from "undici";
@@ -2338,6 +2344,10 @@ function dispatcher() {
2338
2344
  }
2339
2345
  return cached ?? void 0;
2340
2346
  }
2347
+ var proxyFetch = (input, init) => {
2348
+ const proxy = dispatcher();
2349
+ return globalThis.fetch(input, { ...init, ...proxy ? { dispatcher: proxy } : {} });
2350
+ };
2341
2351
  async function post(url, body, timeoutMs = 18e4, headers = {}) {
2342
2352
  return fetch(url, {
2343
2353
  method: "POST",
@@ -2348,72 +2358,238 @@ async function post(url, body, timeoutMs = 18e4, headers = {}) {
2348
2358
  });
2349
2359
  }
2350
2360
 
2361
+ // packages/review-agent/src/models.ts
2362
+ function reviewRoutes(config) {
2363
+ const prefix = config.model.slice(0, config.model.lastIndexOf("/") + 1);
2364
+ const names = [.../* @__PURE__ */ new Set([config.model, ...["gemini-3.8-flash", "gemini-3.7-flash", "gemini-3.6-flash"].map((m) => prefix + m)])];
2365
+ const base = (config.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta").replace(/\/+$/, "");
2366
+ const routes = names.map((model) => ({
2367
+ provider: "google-gemini",
2368
+ model,
2369
+ apiKey: config.apiKey,
2370
+ baseUrl: /\/v1(beta)?$/.test(base) ? base : `${base}/v1beta`,
2371
+ maxAttempts: config.maxAttempts ?? 3
2372
+ }));
2373
+ routes.push({
2374
+ provider: "doubao",
2375
+ model: process.env.ARK_MODEL?.trim() || "ep-20260721150136-98dj9",
2376
+ apiKey: process.env.ARK_API_KEY?.trim() ?? "",
2377
+ baseUrl: "https://ark.cn-beijing.volces.com/api/v3",
2378
+ maxAttempts: 3
2379
+ });
2380
+ return routes;
2381
+ }
2382
+ function agentModels(config, onFailure, onFilteredUsage) {
2383
+ let filteredCalls = 0;
2384
+ return reviewRoutes(config).filter((route) => route.apiKey).map((route) => ({
2385
+ id: `${route.provider}/${route.model}`,
2386
+ model: route.provider === "google-gemini" ? (() => {
2387
+ const model = createGoogleGenerativeAI({ apiKey: route.apiKey, baseURL: route.baseUrl, fetch: proxyFetch })(route.model);
2388
+ const generate = model.doGenerate.bind(model);
2389
+ model.doGenerate = async (options) => {
2390
+ try {
2391
+ const result = await generate(options);
2392
+ if (["content-filter", "error"].includes(result.finishReason.unified) || ["PROHIBITED_CONTENT", "SAFETY", "RECITATION", "BLOCKLIST", "MALFORMED_FUNCTION_CALL"].includes(result.finishReason.raw ?? "")) {
2393
+ const u = result.usage;
2394
+ onFilteredUsage?.({
2395
+ call_key: `filtered:${++filteredCalls}`,
2396
+ provider: route.provider,
2397
+ model: route.model,
2398
+ input_tokens: u.inputTokens.total ?? 0,
2399
+ cached_input_tokens: u.inputTokens.cacheRead ?? 0,
2400
+ output_tokens: u.outputTokens.total ?? 0,
2401
+ reasoning_tokens: u.outputTokens.reasoning ?? 0
2402
+ });
2403
+ throw new APICallError({
2404
+ message: `Gemini finishReason=${result.finishReason.raw ?? result.finishReason.unified}`,
2405
+ url: `${route.baseUrl}/models/${route.model}:generateContent`,
2406
+ requestBodyValues: {},
2407
+ isRetryable: false
2408
+ });
2409
+ }
2410
+ return result;
2411
+ } catch (error) {
2412
+ onFailure?.(route, error);
2413
+ throw error;
2414
+ }
2415
+ };
2416
+ return model;
2417
+ })() : { providerId: "doubao", modelId: route.model, url: route.baseUrl, apiKey: route.apiKey },
2418
+ maxRetries: route.maxAttempts - 1
2419
+ }));
2420
+ }
2421
+ var geminiResponse = z2.object({
2422
+ candidates: z2.array(z2.object({
2423
+ finishReason: z2.string().optional(),
2424
+ content: z2.object({ parts: z2.array(z2.object({ text: z2.string().optional(), thought: z2.boolean().optional() })) }).optional()
2425
+ })).optional(),
2426
+ promptFeedback: z2.object({ blockReason: z2.string().optional() }).optional(),
2427
+ usageMetadata: z2.object({
2428
+ promptTokenCount: z2.number().optional(),
2429
+ cachedContentTokenCount: z2.number().optional(),
2430
+ candidatesTokenCount: z2.number().optional(),
2431
+ thoughtsTokenCount: z2.number().optional()
2432
+ }).optional()
2433
+ });
2434
+ var arkResponse = z2.object({
2435
+ status: z2.string().optional(),
2436
+ incomplete_details: z2.object({ reason: z2.string().optional() }).nullish(),
2437
+ output: z2.array(z2.object({ type: z2.string(), content: z2.array(z2.object({ type: z2.string(), text: z2.string().optional() })).optional() })).optional(),
2438
+ usage: z2.object({
2439
+ input_tokens: z2.number().optional(),
2440
+ output_tokens: z2.number().optional(),
2441
+ input_tokens_details: z2.object({ cached_tokens: z2.number().optional() }).optional(),
2442
+ output_tokens_details: z2.object({ reasoning_tokens: z2.number().optional() }).optional()
2443
+ }).optional()
2444
+ });
2445
+ var ResponseFailure = class extends Error {
2446
+ constructor(message, retryable) {
2447
+ super(message);
2448
+ this.retryable = retryable;
2449
+ }
2450
+ retryable;
2451
+ };
2452
+ var ReviewModelError = class extends Error {
2453
+ constructor(failures, usage) {
2454
+ super(`All review model routes failed:
2455
+ ${failures.join("\n")}`);
2456
+ this.usage = usage;
2457
+ }
2458
+ usage;
2459
+ };
2460
+ function transientNetwork(error) {
2461
+ return error instanceof Error && (error.name === "TimeoutError" || error instanceof TypeError && error.message === "fetch failed" || error.cause instanceof Error && "code" in error.cause && ["ECONNRESET", "ECONNREFUSED", "ETIMEDOUT", "EAI_AGAIN", "UND_ERR_SOCKET", "UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT"].includes(String(error.cause.code)));
2462
+ }
2463
+ async function generateReviewJson(request) {
2464
+ const failures = [];
2465
+ const usage = [];
2466
+ const schema = z2.toJSONSchema(request.schema);
2467
+ for (const route of reviewRoutes(request.config)) {
2468
+ if (!route.apiKey) {
2469
+ failures.push(`${route.provider}/${route.model}: ARK_API_KEY is not set`);
2470
+ continue;
2471
+ }
2472
+ const google = route.provider === "google-gemini";
2473
+ const body = google ? {
2474
+ contents: [{ role: "user", parts: [
2475
+ ...request.video ? [{ inline_data: { mime_type: "video/mp4", data: request.video.data }, video_metadata: { fps: request.video.fps } }] : [],
2476
+ { text: request.prompt }
2477
+ ] }],
2478
+ ...request.system ? { systemInstruction: { parts: [{ text: request.system }] } } : {},
2479
+ generationConfig: {
2480
+ temperature: 0,
2481
+ responseMimeType: "application/json",
2482
+ responseJsonSchema: schema,
2483
+ ...request.maxOutputTokens ? { maxOutputTokens: request.maxOutputTokens } : {}
2484
+ }
2485
+ } : {
2486
+ model: route.model,
2487
+ input: [{ role: "user", content: [
2488
+ ...request.video ? [{ type: "input_video", video_url: `data:video/mp4;base64,${request.video.data}`, fps: request.video.fps }] : [],
2489
+ { type: "input_text", text: request.prompt }
2490
+ ] }],
2491
+ ...request.system ? { instructions: request.system } : {},
2492
+ ...request.maxOutputTokens ? { max_output_tokens: request.maxOutputTokens } : {},
2493
+ text: { format: { type: "json_schema", name: "review", schema } }
2494
+ };
2495
+ for (let attempt = 1; attempt <= route.maxAttempts; attempt++) {
2496
+ let response;
2497
+ let raw;
2498
+ try {
2499
+ response = await post(
2500
+ google ? `${route.baseUrl}/models/${route.model}:generateContent` : `${route.baseUrl}/responses`,
2501
+ body,
2502
+ request.timeoutMs,
2503
+ google ? { "x-goog-api-key": route.apiKey } : { Authorization: `Bearer ${route.apiKey}` }
2504
+ );
2505
+ raw = await response.text();
2506
+ } catch (error) {
2507
+ if (!transientNetwork(error)) throw error;
2508
+ failures.push(`${route.provider}/${route.model} attempt ${attempt}: ${String(error)}`);
2509
+ if (attempt < route.maxAttempts) await new Promise((resolve2) => setTimeout(resolve2, attempt * 1e3));
2510
+ continue;
2511
+ }
2512
+ await request.onResponse?.(route, attempt, raw);
2513
+ try {
2514
+ if (!response.ok) throw new ResponseFailure(
2515
+ `HTTP ${response.status}: ${raw.slice(0, 400)}`,
2516
+ [408, 429].includes(response.status) || response.status >= 500
2517
+ );
2518
+ const payload = JSON.parse(raw);
2519
+ let text;
2520
+ const row = {
2521
+ call_key: `${request.callKey}:${route.provider}:${route.model}:${attempt}`,
2522
+ provider: route.provider,
2523
+ model: route.model,
2524
+ input_tokens: 0,
2525
+ output_tokens: 0,
2526
+ cached_input_tokens: 0,
2527
+ reasoning_tokens: 0
2528
+ };
2529
+ if (google) {
2530
+ const parsed = geminiResponse.parse(payload);
2531
+ const u = parsed.usageMetadata;
2532
+ Object.assign(row, {
2533
+ input_tokens: u?.promptTokenCount ?? 0,
2534
+ output_tokens: u?.candidatesTokenCount ?? 0,
2535
+ cached_input_tokens: u?.cachedContentTokenCount ?? 0,
2536
+ reasoning_tokens: u?.thoughtsTokenCount ?? 0
2537
+ });
2538
+ usage.push(row);
2539
+ const candidate = parsed.candidates?.[0];
2540
+ const reason = parsed.promptFeedback?.blockReason ?? candidate?.finishReason;
2541
+ if (reason && reason !== "STOP") throw new ResponseFailure(`finishReason=${reason}`, false);
2542
+ text = candidate?.content?.parts.filter((p) => !p.thought).map((p) => p.text ?? "").join("") ?? "";
2543
+ } else {
2544
+ const parsed = arkResponse.parse(payload);
2545
+ const u = parsed.usage;
2546
+ Object.assign(row, {
2547
+ input_tokens: u?.input_tokens ?? 0,
2548
+ output_tokens: u?.output_tokens ?? 0,
2549
+ cached_input_tokens: u?.input_tokens_details?.cached_tokens ?? 0,
2550
+ reasoning_tokens: u?.output_tokens_details?.reasoning_tokens ?? 0
2551
+ });
2552
+ usage.push(row);
2553
+ if (parsed.status && parsed.status !== "completed") throw new ResponseFailure(`status=${parsed.status}; ${parsed.incomplete_details?.reason ?? ""}`, false);
2554
+ text = parsed.output?.filter((o) => o.type === "message").flatMap((o) => o.content ?? []).filter((c) => c.type === "output_text").map((c) => c.text ?? "").join("") ?? "";
2555
+ }
2556
+ return { value: request.schema.parse(JSON.parse(text)), usage, route };
2557
+ } catch (error) {
2558
+ if (!(error instanceof ResponseFailure || error instanceof SyntaxError || error instanceof z2.ZodError)) throw error;
2559
+ failures.push(`${route.provider}/${route.model} attempt ${attempt}: ${error.message}`);
2560
+ if (!(error instanceof ResponseFailure) || !error.retryable) break;
2561
+ if (attempt < route.maxAttempts) await new Promise((resolve2) => setTimeout(resolve2, attempt * 1e3));
2562
+ }
2563
+ }
2564
+ }
2565
+ throw new ReviewModelError(failures, usage);
2566
+ }
2567
+
2351
2568
  // packages/review-agent/src/media/watch.ts
2352
2569
  var SYSTEM = "\u4F60\u662F\u5267\u672C\u5BA1\u6838\u7684\u89C6\u9891\u8BC1\u636E\u6838\u5BF9\u5668\u3002\u53EA\u62A5\u544A\u89C6\u9891\u7A97\u53E3\u4E2D\u53EF\u76F4\u63A5\u89C2\u5BDF\u6216\u542C\u5230\u7684\u4E8B\u5B9E\uFF1B\u65E0\u6CD5\u786E\u8BA4\u65F6\u660E\u786E\u8BF4\u660E\uFF0C\u4E0D\u63A8\u6D4B\u5267\u60C5\u3002";
2353
2570
  function traceKey(video, window, model, question) {
2354
2571
  return "watch:" + crypto.createHash("sha256").update([video, window, model, question].join("\n")).digest("hex");
2355
2572
  }
2356
2573
  async function watchWindow(video, from, to, question, opts) {
2357
- const model = opts.model ?? "gemini-3.7-flash";
2358
- const base = opts.baseUrl ?? "https://generativelanguage.googleapis.com";
2359
2574
  const clip2 = await cutClip(video, from, to);
2360
2575
  const started = Date.now();
2361
2576
  try {
2362
- const body = {
2363
- contents: [{
2364
- role: "user",
2365
- parts: [
2366
- {
2367
- inline_data: { mime_type: "video/mp4", data: fs3.readFileSync(clip2.path, "base64") },
2368
- video_metadata: { fps: CLIP_FPS }
2369
- },
2370
- { text: question }
2371
- ]
2372
- }],
2373
- systemInstruction: { parts: [{ text: SYSTEM }] },
2374
- generationConfig: {
2375
- temperature: 0.1,
2376
- maxOutputTokens: 2048,
2377
- responseMimeType: "application/json",
2378
- responseSchema: {
2379
- type: "object",
2380
- properties: { answer: { type: "string" } },
2381
- required: ["answer"]
2382
- }
2383
- }
2384
- };
2385
- const response = await post(
2386
- `${base}/v1beta/models/${model}:generateContent?key=${opts.apiKey}`,
2387
- body
2388
- );
2389
- const payload = await response.json().catch(() => ({}));
2390
- if (!response.ok || payload?.error) {
2391
- throw new Error(`media check failed (${response.status}): ` + String(payload?.error?.message ?? "unknown").slice(0, 200));
2392
- }
2393
- const text = payload?.candidates?.[0]?.content?.parts?.map((p) => p?.text).filter(Boolean).join("") ?? "";
2394
- let answer = text.trim();
2395
- try {
2396
- const parsed = JSON.parse(answer);
2397
- if (typeof parsed?.answer === "string") answer = parsed.answer;
2398
- } catch {
2399
- }
2400
- if (!answer) throw new Error("media check returned no answer");
2401
- const meta = payload?.usageMetadata ?? {};
2402
- return {
2403
- answer,
2404
- usage: {
2405
- call_key: opts.callKey,
2406
- trace_key: traceKey(video, `${from}..${to}`, model, question),
2407
- provider: "google-gemini",
2408
- model,
2409
- input_tokens: Number(meta.promptTokenCount ?? 0),
2410
- cached_input_tokens: Number(meta.cachedContentTokenCount ?? 0),
2411
- output_tokens: Number(meta.candidatesTokenCount ?? 0),
2412
- reasoning_tokens: Number(meta.thoughtsTokenCount ?? 0),
2413
- clip_bytes: clip2.bytes,
2414
- wall_ms: Date.now() - started
2415
- }
2416
- };
2577
+ const result = await generateReviewJson({
2578
+ config: opts,
2579
+ prompt: question,
2580
+ system: SYSTEM,
2581
+ video: { data: fs3.readFileSync(clip2.path, "base64"), fps: CLIP_FPS },
2582
+ schema: z3.object({ answer: z3.string().min(1) }),
2583
+ timeoutMs: 18e4,
2584
+ maxOutputTokens: 2048,
2585
+ callKey: opts.callKey
2586
+ });
2587
+ return { answer: result.value.answer, usage: result.usage.map((row) => ({
2588
+ ...row,
2589
+ trace_key: traceKey(video, `${from}..${to}`, row.model, question),
2590
+ clip_bytes: clip2.bytes,
2591
+ wall_ms: Date.now() - started
2592
+ })) };
2417
2593
  } finally {
2418
2594
  clip2.cleanup();
2419
2595
  }
@@ -2425,20 +2601,20 @@ import { promisify } from "node:util";
2425
2601
  import { createHash as createHash2, randomUUID } from "node:crypto";
2426
2602
  import * as fs4 from "node:fs/promises";
2427
2603
  import * as path2 from "node:path";
2428
- import { z as z2 } from "zod";
2604
+ import { z as z4 } from "zod";
2429
2605
  var exec = promisify(execFile);
2430
- var timedTextSchema = z2.object({
2431
- text: z2.string().min(1),
2432
- start_time: z2.number().int().nonnegative(),
2433
- end_time: z2.number().int().nonnegative()
2606
+ var timedTextSchema = z4.object({
2607
+ text: z4.string().min(1),
2608
+ start_time: z4.number().int().nonnegative(),
2609
+ end_time: z4.number().int().nonnegative()
2434
2610
  }).refine((u) => u.end_time >= u.start_time, "ASR \u7ED3\u675F\u65F6\u95F4\u65E9\u4E8E\u5F00\u59CB\u65F6\u95F4");
2435
- var wordsSchema = z2.array(z2.object({
2436
- text: z2.string(),
2437
- start_time: z2.number(),
2438
- end_time: z2.number()
2439
- })).transform((words) => words.filter((word) => word.text.trim().length > 0)).pipe(z2.array(timedTextSchema));
2611
+ var wordsSchema = z4.array(z4.object({
2612
+ text: z4.string(),
2613
+ start_time: z4.number(),
2614
+ end_time: z4.number()
2615
+ })).transform((words) => words.filter((word) => word.text.trim().length > 0)).pipe(z4.array(timedTextSchema));
2440
2616
  var utteranceSchema = timedTextSchema.safeExtend({ words: wordsSchema.optional() });
2441
- var responseSchema = z2.object({ result: z2.object({ utterances: z2.array(utteranceSchema) }) });
2617
+ var responseSchema = z4.object({ result: z4.object({ utterances: z4.array(utteranceSchema) }) });
2442
2618
  function parseAsr(raw) {
2443
2619
  return responseSchema.parse(raw).result.utterances;
2444
2620
  }
@@ -2671,19 +2847,19 @@ import * as fs5 from "node:fs";
2671
2847
  import * as os2 from "node:os";
2672
2848
  import * as path3 from "node:path";
2673
2849
  import { createTool } from "@mastra/core/tools";
2674
- import { z as z3 } from "zod";
2850
+ import { z as z5 } from "zod";
2675
2851
  var ADDR = "\u5730\u5740\uFF1A\u96C6 `3`\u3001\u573A `3-2`\u3001\u5355\u6761 `3-2-5`\u3001\u6B63\u5F0F ID\uFF08`acB4xR8p`/`scnQ7mN2v`\uFF09\u3001\u8D44\u4EA7\u540D\u3001\u6216 `<\u8D44\u4EA7>/<\u72B6\u6001>`";
2676
2852
  function createRefsTool(doc) {
2677
2853
  return createTool({
2678
2854
  id: "refs",
2679
2855
  description: "\u53EA\u8BFB\u5B9A\u4F4D\u4EBA\u7269/\u573A\u666F/\u9053\u5177\u6216\u5177\u4F53\u9020\u578B\u7684\u5F15\u7528\u3002\u8FD4\u56DE\u573A\u666F\u7ED1\u5B9A\u3001\u8BF4\u8BDD\u4F4D\u7F6E\u3001\u6B63\u6587\u63D0\u53CA\u53CA\u524D\u540E\u6587\uFF1B\u6B63\u6587\u63D0\u53CA\u4E0D\u662F\u51FA\u573A\u8BC1\u636E\u3002\u53EF\u7528\u8FD4\u56DE\u5730\u5740\u7EE7\u7EED read\uFF0C\u6216 span \u5B9A\u4F4D\u539F\u7247\u7A97\u53E3\u3002",
2680
- inputSchema: z3.object({
2681
- addr: z3.string().min(1).describe("\u8D44\u4EA7 ID/\u540D\u5B57/\u522B\u540D\uFF0C\u6216 <\u8D44\u4EA7>/<\u9020\u578B>"),
2682
- offset: z3.number().int().min(0).optional().describe("\u4ECE\u7B2C\u51E0\u6761\u5F15\u7528\u7EED\u8BFB\uFF0C\u4ECE 0 \u5F00\u59CB"),
2683
- limit: z3.number().int().min(1).max(200).optional().describe("\u672C\u6279\u5F15\u7528\u6761\u6570\uFF0C\u9ED8\u8BA4 20\uFF1B\u6309\u8FD4\u56DE\u7684 offset \u63D0\u793A\u7EED\u8BFB"),
2684
- context: z3.number().int().min(0).max(20).optional().describe("\u6B63\u6587\u547D\u4E2D\u524D\u540E\u5404\u51E0\u6761 action\uFF0C\u9ED8\u8BA4 3")
2856
+ inputSchema: z5.object({
2857
+ addr: z5.string().min(1).describe("\u8D44\u4EA7 ID/\u540D\u5B57/\u522B\u540D\uFF0C\u6216 <\u8D44\u4EA7>/<\u9020\u578B>"),
2858
+ offset: z5.number().int().min(0).optional().describe("\u4ECE\u7B2C\u51E0\u6761\u5F15\u7528\u7EED\u8BFB\uFF0C\u4ECE 0 \u5F00\u59CB"),
2859
+ limit: z5.number().int().min(1).max(200).optional().describe("\u672C\u6279\u5F15\u7528\u6761\u6570\uFF0C\u9ED8\u8BA4 20\uFF1B\u6309\u8FD4\u56DE\u7684 offset \u63D0\u793A\u7EED\u8BFB"),
2860
+ context: z5.number().int().min(0).max(20).optional().describe("\u6B63\u6587\u547D\u4E2D\u524D\u540E\u5404\u51E0\u6761 action\uFF0C\u9ED8\u8BA4 3")
2685
2861
  }),
2686
- outputSchema: z3.string(),
2862
+ outputSchema: z5.string(),
2687
2863
  execute: async ({ addr, ...options }) => formatReferences(references(doc, addr, options))
2688
2864
  });
2689
2865
  }
@@ -2731,27 +2907,27 @@ function buildTools(ctx) {
2731
2907
  tools.scan = createTool({
2732
2908
  id: "scan",
2733
2909
  description: "\u8BFB\u4E00\u96C6\u7684\u5B8C\u6574\u4E0A\u4E0B\u6587\uFF1A\u573A\u7ED3\u6784 + \u5168\u90E8\u6B63\u6587 + \u5F53\u524D\u6821\u9A8C\u95EE\u9898\u3002\u5F00\u59CB\u5BA1\u6838\u4E00\u96C6\u65F6\u5148\u8C03\u7528\u5B83\u3002",
2734
- inputSchema: z3.object({ episode: z3.number().int().min(1).describe("\u96C6\u53F7\uFF0C\u4ECE 1 \u5F00\u59CB") }),
2910
+ inputSchema: z5.object({ episode: z5.number().int().min(1).describe("\u96C6\u53F7\uFF0C\u4ECE 1 \u5F00\u59CB") }),
2735
2911
  execute: async ({ episode }) => scanEpisode(ctx.doc, episode)
2736
2912
  });
2737
2913
  tools.read = createTool({
2738
2914
  id: "read",
2739
2915
  description: `\u8BFB\u4EFB\u610F\u8282\u70B9\u3002${ADDR}\u3002\u4E0D\u4F20\u5730\u5740\u8FD4\u56DE\u5267\u672C\u5361\uFF08\u9898\u6750/\u8D44\u4EA7\u8868/\u6BCF\u96C6\u4E00\u884C\uFF09\u3002\u8D44\u4EA7\u5730\u5740\u8FD4\u56DE\u4EBA\u7269\u5361\uFF08\u63CF\u8FF0 + \u5168\u90E8\u72B6\u6001 + \u51FA\u573A\u7EDF\u8BA1 + \u672A\u4F7F\u7528\u72B6\u6001\uFF09\u3002`,
2740
- inputSchema: z3.object({
2741
- addr: z3.string().optional().describe(ADDR),
2742
- context: z3.number().int().min(0).max(20).optional().describe("\u5355\u6761 action \u524D\u540E\u5404\u5E26\u51E0\u6761\uFF0C\u9ED8\u8BA4 3")
2916
+ inputSchema: z5.object({
2917
+ addr: z5.string().optional().describe(ADDR),
2918
+ context: z5.number().int().min(0).max(20).optional().describe("\u5355\u6761 action \u524D\u540E\u5404\u5E26\u51E0\u6761\uFF0C\u9ED8\u8BA4 3")
2743
2919
  }),
2744
2920
  execute: async ({ addr, context }) => read(ctx.doc, addr ?? "", context === void 0 ? {} : { context })
2745
2921
  });
2746
2922
  tools.find = createTool({
2747
2923
  id: "find",
2748
2924
  description: "\u6309\u6B63\u6587\u6B63\u5219 / \u8BF4\u8BDD\u4EBA / \u7C7B\u578B\u5B9A\u4F4D action\uFF0C\u8FD4\u56DE\u951A\u70B9 + \u4F4D\u7F6E + \u547D\u4E2D\u7A97\u53E3\u3002",
2749
- inputSchema: z3.object({
2750
- query: z3.string().describe("\u6B63\u5219\uFF1B\u53EA\u6309 speaker/type \u8FC7\u6EE4\u65F6\u4F20\u7A7A\u4E32"),
2751
- in: z3.string().optional().describe(`\u9650\u5236\u8303\u56F4\uFF0C${ADDR}`),
2752
- speaker: z3.string().optional().describe("\u4EBA\u7269\u540D\u6216 actor id"),
2753
- type: z3.enum(["action", "dialogue", "inner_thought", "transition"]).optional(),
2754
- limit: z3.number().int().min(1).max(200).optional()
2925
+ inputSchema: z5.object({
2926
+ query: z5.string().describe("\u6B63\u5219\uFF1B\u53EA\u6309 speaker/type \u8FC7\u6EE4\u65F6\u4F20\u7A7A\u4E32"),
2927
+ in: z5.string().optional().describe(`\u9650\u5236\u8303\u56F4\uFF0C${ADDR}`),
2928
+ speaker: z5.string().optional().describe("\u4EBA\u7269\u540D\u6216 actor id"),
2929
+ type: z5.enum(["action", "dialogue", "inner_thought", "transition"]).optional(),
2930
+ limit: z5.number().int().min(1).max(200).optional()
2755
2931
  }),
2756
2932
  execute: async (input) => find(ctx.doc, input.query, {
2757
2933
  ...input.in ? { in: input.in } : {},
@@ -2764,44 +2940,44 @@ function buildTools(ctx) {
2764
2940
  tools.span = createTool({
2765
2941
  id: "span",
2766
2942
  description: "\u628A\u4E00\u6BB5 action \u6362\u7B97\u6210\u96C6\u5185\u89C6\u9891\u65F6\u95F4\u7A97\u53E3\uFF08\u534A\u5F00\u533A\u95F4\uFF09\uFF0C\u9009 watch/frames \u7684\u7A97\u53E3\u9760\u5B83\u3002",
2767
- inputSchema: z3.object({ range: z3.string().describe(`${ADDR}\uFF0C\u6216 \`\u8D77ID..\u6B62ID\``) }),
2943
+ inputSchema: z5.object({ range: z5.string().describe(`${ADDR}\uFF0C\u6216 \`\u8D77ID..\u6B62ID\``) }),
2768
2944
  execute: async ({ range }) => span(ctx.doc, range)
2769
2945
  });
2770
2946
  tools.issues = createTool({
2771
2947
  id: "issues",
2772
2948
  description: "\u5217\u5F53\u524D\u6821\u9A8C\u95EE\u9898\uFF0C\u53EF\u6309 severity / code \u8FC7\u6EE4\u3002",
2773
- inputSchema: z3.object({
2774
- severity: z3.enum(["error", "warning"]).optional(),
2775
- code: z3.string().optional()
2949
+ inputSchema: z5.object({
2950
+ severity: z5.enum(["error", "warning"]).optional(),
2951
+ code: z5.string().optional()
2776
2952
  }),
2777
2953
  execute: async (input) => issues(ctx.doc, input)
2778
2954
  });
2779
2955
  tools.validate = createTool({
2780
2956
  id: "validate",
2781
2957
  description: "\u8DD1\u6574\u672C\u6821\u9A8C\u3002\u6BCF\u6279\u4FEE\u6539\u4E4B\u540E\u5FC5\u987B\u8C03\u7528\u4E00\u6B21\u3002",
2782
- inputSchema: z3.object({}),
2958
+ inputSchema: z5.object({}),
2783
2959
  execute: async () => validate(ctx.doc)
2784
2960
  });
2785
2961
  const editFields = {
2786
- content: z3.string().optional().describe("**\u6574\u6761\u8986\u5199** action \u6B63\u6587\uFF1A\u4F20\u5165\u7684\u5B57\u7B26\u4E32\u4F1A\u6210\u4E3A\u8FD9\u6761 action \u7684\u5168\u90E8\u5185\u5BB9\uFF0C\u539F\u6587\u88AB\u4E22\u5F03\u3002\u53EA\u60F3\u6539\u5176\u4E2D\u4E00\u4E2A\u8BCD\u6216\u4E00\u6BB5\uFF0C\u7528 `replace`\uFF0C\u4E0D\u8981\u7528\u8FD9\u4E2A\u5B57\u6BB5\u3002"),
2787
- paren: z3.string().nullable().optional().describe("\u8868\u6F14\u63D0\u793A\uFF1Bnull \u5220\u9664\u8BE5\u5B57\u6BB5"),
2788
- speaker: z3.string().optional().describe("\u4EBA\u7269\u540D\u6216 `\u540D\u5B57\xB7\u72B6\u6001`\uFF1B\u65C1\u767D/\u5E7F\u64AD/\u753B\u5916/\u7FA4\u58F0\u4EA6\u53EF"),
2789
- ts: z3.string().nullable().optional().describe("\u65F6\u95F4\u6233\uFF1Bnull \u5220\u9664"),
2790
- type: z3.enum(["action", "dialogue", "inner_thought", "transition"]).optional(),
2791
- space: z3.enum(["interior", "exterior"]).optional().describe("\u573A\u5730\u5740\uFF1A\u5185\u5916\u666F"),
2792
- time: z3.enum(["day", "night", "dawn", "dusk"]).optional().describe("\u573A\u5730\u5740\uFF1A\u65E5\u591C")
2962
+ content: z5.string().optional().describe("**\u6574\u6761\u8986\u5199** action \u6B63\u6587\uFF1A\u4F20\u5165\u7684\u5B57\u7B26\u4E32\u4F1A\u6210\u4E3A\u8FD9\u6761 action \u7684\u5168\u90E8\u5185\u5BB9\uFF0C\u539F\u6587\u88AB\u4E22\u5F03\u3002\u53EA\u60F3\u6539\u5176\u4E2D\u4E00\u4E2A\u8BCD\u6216\u4E00\u6BB5\uFF0C\u7528 `replace`\uFF0C\u4E0D\u8981\u7528\u8FD9\u4E2A\u5B57\u6BB5\u3002"),
2963
+ paren: z5.string().nullable().optional().describe("\u8868\u6F14\u63D0\u793A\uFF1Bnull \u5220\u9664\u8BE5\u5B57\u6BB5"),
2964
+ speaker: z5.string().optional().describe("\u4EBA\u7269\u540D\u6216 `\u540D\u5B57\xB7\u72B6\u6001`\uFF1B\u65C1\u767D/\u5E7F\u64AD/\u753B\u5916/\u7FA4\u58F0\u4EA6\u53EF"),
2965
+ ts: z5.string().nullable().optional().describe("\u65F6\u95F4\u6233\uFF1Bnull \u5220\u9664"),
2966
+ type: z5.enum(["action", "dialogue", "inner_thought", "transition"]).optional(),
2967
+ space: z5.enum(["interior", "exterior"]).optional().describe("\u573A\u5730\u5740\uFF1A\u5185\u5916\u666F"),
2968
+ time: z5.enum(["day", "night", "dawn", "dusk"]).optional().describe("\u573A\u5730\u5740\uFF1A\u65E5\u591C")
2793
2969
  };
2794
2970
  const assetFields = {
2795
- name: z3.string().optional(),
2796
- desc: z3.string().optional(),
2797
- importance: z3.enum(["featured", "background"]).optional(),
2798
- role: z3.enum(["\u4E3B\u89D2", "\u914D\u89D2"]).optional()
2971
+ name: z5.string().optional(),
2972
+ desc: z5.string().optional(),
2973
+ importance: z5.enum(["featured", "background"]).optional(),
2974
+ role: z5.enum(["\u4E3B\u89D2", "\u914D\u89D2"]).optional()
2799
2975
  };
2800
2976
  tools.edit = createTool({
2801
2977
  id: "edit",
2802
2978
  description: ctx.phase === "converge" ? `\u6539\u4E00\u4E2A\u8282\u70B9\u7684\u5B57\u6BB5\u3002${ADDR}\uFF1B\u5730\u5740\u51B3\u5B9A\u54EA\u4E9B\u5B57\u6BB5\u5408\u6CD5\uFF08action / \u573A / \u8D44\u4EA7 / \u72B6\u6001\uFF09\u3002` : `\u6539\u4E00\u6761 action \u6216\u4E00\u4E2A\u573A\u7684\u5B57\u6BB5\u3002${ADDR}\u3002\u8D44\u4EA7\u548C\u72B6\u6001\u672C\u8EAB\u7559\u7ED9\u5168\u5267\u6536\u655B\u9636\u6BB5\u3002`,
2803
- inputSchema: z3.object({
2804
- at: z3.string().describe(ADDR),
2979
+ inputSchema: z5.object({
2980
+ at: z5.string().describe(ADDR),
2805
2981
  ...editFields,
2806
2982
  ...ctx.phase === "converge" ? assetFields : {}
2807
2983
  }),
@@ -2813,13 +2989,13 @@ function buildTools(ctx) {
2813
2989
  tools.replace = createTool({
2814
2990
  id: "replace",
2815
2991
  description: "\u628A\u4E00\u6BB5\u6587\u672C\u6362\u6210\u53E6\u4E00\u6BB5\uFF0C\u53EA\u52A8\u5339\u914D\u5230\u7684\u90E8\u5206\uFF0C\u6B63\u6587\u5176\u4F59\u5185\u5BB9\u4E0D\u53D8\u3002\u6539\u4EBA\u540D\u3001\u5730\u540D\u3001\u79F0\u8C13\u8FD9\u7C7B\u300E\u4E00\u4E2A\u8BCD\u9519\u4E86\u300F\u7684\u95EE\u9898\u7528\u5B83\uFF0C\u4E0D\u8981\u7528 edit \u6574\u6761\u8986\u5199\u3002 \u8FC7\u6EE4\u53C2\u6570\u4E0E find \u76F8\u540C\uFF0C\u53EF\u7528 in \u9650\u5B9A\u5230\u67D0\u96C6\u6216\u67D0\u573A\u3002",
2816
- inputSchema: z3.object({
2817
- pattern: z3.string().describe("\u8981\u88AB\u66FF\u6362\u6389\u7684\u6587\u672C\uFF08\u6B63\u5219\uFF09"),
2818
- to: z3.string().describe("\u66FF\u6362\u6210\u4EC0\u4E48"),
2819
- in: z3.string().optional().describe(`\u9650\u5236\u8303\u56F4\uFF0C${ADDR}\uFF1B\u4E0D\u4F20\u5219\u6574\u672C`),
2820
- speaker: z3.string().optional().describe("\u53EA\u6539\u67D0\u4EBA\u7269\u7684\u53F0\u8BCD"),
2821
- type: z3.enum(["action", "dialogue", "inner_thought", "transition"]).optional(),
2822
- apply: z3.boolean().optional().describe("false \u6216\u7701\u7565\u53EA\u8FD4\u56DE\u9010\u884C before/after \u9884\u89C8\uFF1Btrue \u624D\u771F\u7684\u5199\u5165")
2992
+ inputSchema: z5.object({
2993
+ pattern: z5.string().describe("\u8981\u88AB\u66FF\u6362\u6389\u7684\u6587\u672C\uFF08\u6B63\u5219\uFF09"),
2994
+ to: z5.string().describe("\u66FF\u6362\u6210\u4EC0\u4E48"),
2995
+ in: z5.string().optional().describe(`\u9650\u5236\u8303\u56F4\uFF0C${ADDR}\uFF1B\u4E0D\u4F20\u5219\u6574\u672C`),
2996
+ speaker: z5.string().optional().describe("\u53EA\u6539\u67D0\u4EBA\u7269\u7684\u53F0\u8BCD"),
2997
+ type: z5.enum(["action", "dialogue", "inner_thought", "transition"]).optional(),
2998
+ apply: z5.boolean().optional().describe("false \u6216\u7701\u7565\u53EA\u8FD4\u56DE\u9010\u884C before/after \u9884\u89C8\uFF1Btrue \u624D\u771F\u7684\u5199\u5165")
2823
2999
  }),
2824
3000
  execute: async ({ pattern, to, apply, ...filters }) => replace(ctx.doc, pattern, to, {
2825
3001
  ...Object.fromEntries(Object.entries(filters).filter(([, v]) => v !== void 0)),
@@ -2829,25 +3005,25 @@ function buildTools(ctx) {
2829
3005
  tools.ref = createTool({
2830
3006
  id: "ref",
2831
3007
  description: "\u6539\u4E00\u4E2A\u573A\u5BF9\u67D0\u8D44\u4EA7\u7684\u5F15\u7528\u3002states \u662F\u5B8C\u6574\u72B6\u6001\u96C6\u5408\u800C\u975E\u8FFD\u52A0\uFF0C`default` \u4E5F\u8981\u663E\u5F0F\u5199\u3002",
2832
- inputSchema: z3.object({
2833
- scene: z3.string().describe("\u573A\u5730\u5740"),
2834
- asset: z3.string().describe("\u4EBA\u7269 / \u5730\u70B9 / \u9053\u5177\u7684\u540D\u5B57\u6216 ID"),
2835
- states: z3.array(z3.string()).optional().describe("\u5B8C\u6574\u72B6\u6001\u96C6\u5408\uFF08\u540D\u5B57\u6216 ID\uFF09"),
2836
- remove: z3.boolean().optional().describe("true \u5220\u9664\u6574\u6761\u5F15\u7528")
3008
+ inputSchema: z5.object({
3009
+ scene: z5.string().describe("\u573A\u5730\u5740"),
3010
+ asset: z5.string().describe("\u4EBA\u7269 / \u5730\u70B9 / \u9053\u5177\u7684\u540D\u5B57\u6216 ID"),
3011
+ states: z5.array(z5.string()).optional().describe("\u5B8C\u6574\u72B6\u6001\u96C6\u5408\uFF08\u540D\u5B57\u6216 ID\uFF09"),
3012
+ remove: z5.boolean().optional().describe("true \u5220\u9664\u6574\u6761\u5F15\u7528")
2837
3013
  }),
2838
3014
  execute: async ({ scene, asset, states, remove }) => ref(ctx.doc, scene, asset, states ?? [], remove ?? false)
2839
3015
  });
2840
3016
  tools.insert = createTool({
2841
3017
  id: "insert",
2842
3018
  description: "\u5728\u67D0\u6761 action \u524D/\u540E\u63D2\u5165\u4E00\u6761\u65B0 action\uFF0C\u7528\u4E8E\u8865\u539F\u7247\u6709\u4F46\u5267\u672C\u6F0F\u6389\u7684\u5185\u5BB9\u3002",
2843
- inputSchema: z3.object({
2844
- at: z3.string().describe("\u76F8\u90BB action \u7684\u5730\u5740"),
2845
- where: z3.enum(["before", "after"]),
2846
- type: z3.enum(["action", "dialogue", "inner_thought", "transition"]),
2847
- content: z3.string(),
2848
- speaker: z3.string().optional(),
2849
- paren: z3.string().optional(),
2850
- ts: z3.string().optional()
3019
+ inputSchema: z5.object({
3020
+ at: z5.string().describe("\u76F8\u90BB action \u7684\u5730\u5740"),
3021
+ where: z5.enum(["before", "after"]),
3022
+ type: z5.enum(["action", "dialogue", "inner_thought", "transition"]),
3023
+ content: z5.string(),
3024
+ speaker: z5.string().optional(),
3025
+ paren: z5.string().optional(),
3026
+ ts: z5.string().optional()
2851
3027
  }),
2852
3028
  execute: async ({ at, where, type, content, speaker, paren, ts }) => {
2853
3029
  const out = insert(ctx.doc, at, where, type, content, speaker, paren);
@@ -2861,20 +3037,20 @@ function buildTools(ctx) {
2861
3037
  tools.move = createTool({
2862
3038
  id: "move",
2863
3039
  description: "\u628A\u4E00\u6761 action \u632A\u5230\u53E6\u4E00\u6761\u65C1\u8FB9\uFF1B\u8DE8\u573A\u4E5F\u4FDD\u6301\u5B83\u7684 action_id\u3002",
2864
- inputSchema: z3.object({
2865
- at: z3.string(),
2866
- to: z3.string().describe("\u76EE\u6807\u90BB\u5C45 action"),
2867
- where: z3.enum(["before", "after"]).optional()
3040
+ inputSchema: z5.object({
3041
+ at: z5.string(),
3042
+ to: z5.string().describe("\u76EE\u6807\u90BB\u5C45 action"),
3043
+ where: z5.enum(["before", "after"]).optional()
2868
3044
  }),
2869
3045
  execute: async ({ at, to, where }) => move(ctx.doc, at, to, where ?? "after")
2870
3046
  });
2871
3047
  tools.del = createTool({
2872
3048
  id: "del",
2873
3049
  description: ctx.phase === "converge" ? "\u5220\u9664\u8282\u70B9\uFF1Aaction / \u573A / \u8D44\u4EA7 / \u72B6\u6001\u3002\u88AB\u5F15\u7528\u7684\u8D44\u4EA7\u6216\u72B6\u6001\u9700\u8981 replacement \u6539\u5199\u6216 force\u3002" : "\u5220\u9664\u4E00\u6761 action\uFF0C\u6216\u4E00\u4E2A\u7A7A\u573A\u3002\u8D44\u4EA7\u548C\u72B6\u6001\u7684\u5220\u9664\u7559\u7ED9\u5168\u5267\u6536\u655B\u9636\u6BB5\u3002",
2874
- inputSchema: z3.object({
2875
- addr: z3.string().describe(ADDR),
2876
- replacement: ctx.phase === "converge" ? z3.string().optional().describe("\u5220\u72B6\u6001\u65F6\u628A\u5F15\u7528\u6539\u5199\u5230\u8FD9\u4E2A\u72B6\u6001") : z3.undefined().optional(),
2877
- force: z3.boolean().optional()
3050
+ inputSchema: z5.object({
3051
+ addr: z5.string().describe(ADDR),
3052
+ replacement: ctx.phase === "converge" ? z5.string().optional().describe("\u5220\u72B6\u6001\u65F6\u628A\u5F15\u7528\u6539\u5199\u5230\u8FD9\u4E2A\u72B6\u6001") : z5.undefined().optional(),
3053
+ force: z5.boolean().optional()
2878
3054
  }),
2879
3055
  execute: async ({ addr, replacement, force }) => {
2880
3056
  if (ctx.phase !== "converge") {
@@ -2893,13 +3069,13 @@ function buildTools(ctx) {
2893
3069
  tools.merge = createTool({
2894
3070
  id: "merge",
2895
3071
  description: "\u628A\u4E00\u4E2A\u8D44\u4EA7\u5E76\u5165\u53E6\u4E00\u4E2A\uFF1A\u5168\u90E8\u573A\u666F\u5F15\u7528\u4E0E speaker \u6539\u5199\u8FC7\u53BB\uFF0C\u540C\u540D\u72B6\u6001\u81EA\u52A8\u5BF9\u9F50\u3002",
2896
- inputSchema: z3.object({ src: z3.string(), dst: z3.string() }),
3072
+ inputSchema: z5.object({ src: z5.string(), dst: z5.string() }),
2897
3073
  execute: async ({ src, dst }) => merge(ctx.doc, src, dst)
2898
3074
  });
2899
3075
  tools.state = createTool({
2900
3076
  id: "state",
2901
3077
  description: "\u7ED9\u5DF2\u6709\u8D44\u4EA7\u52A0\u4E00\u4E2A\u72B6\u6001\uFF08\u9020\u578B\u6863\u4F4D\uFF09\u3002",
2902
- inputSchema: z3.object({ asset: z3.string(), name: z3.string(), desc: z3.string() }),
3078
+ inputSchema: z5.object({ asset: z5.string(), name: z5.string(), desc: z5.string() }),
2903
3079
  execute: async ({ asset, name, desc }) => state(ctx.doc, asset, name, desc)
2904
3080
  });
2905
3081
  }
@@ -2907,31 +3083,38 @@ function buildTools(ctx) {
2907
3083
  tools.watch = createTool({
2908
3084
  id: "watch",
2909
3085
  description: `\u770B\u539F\u7247\u7684\u4E00\u4E2A\u77ED\u7A97\u53E3\uFF08\u6709\u58F0\u97F3\uFF09\uFF0C\u6838\u5BF9\u8BF4\u8BDD\u4EBA / \u8EAB\u4EFD / \u9020\u578B / \u52A8\u4F5C / \u8D44\u4EA7\u4E8B\u5B9E\u3002 \u5355\u6B21\u7A97\u53E3\u4E0D\u8D85\u8FC7 15 \u79D2\uFF1B\u672C\u96C6\u9884\u7B97 ${ctx.mediaBudget} \u6B21\u3002\u53EA\u5728\u6587\u672C\u548C\u7ED3\u6784\u8BC1\u636E\u4E0D\u80FD\u6536\u53E3\u65F6\u7528\u3002`,
2910
- inputSchema: z3.object({
2911
- from: z3.string().describe("\u7A97\u53E3\u8D77\u70B9\uFF0C`M:SS` \u6216 `H:MM:SS`"),
2912
- to: z3.string().describe("\u7A97\u53E3\u7EC8\u70B9\uFF1B\u8986\u76D6\u5019\u9009\u58F0\u97F3\u6216\u52A8\u4F5C\uFF0C\u524D\u540E\u5404\u7559\u6700\u591A 3 \u79D2"),
2913
- question: z3.string().describe("\u8981\u6838\u5BF9\u4EC0\u4E48\u3002\u8981\u6C42\u5B83\u53EA\u62A5\u53EF\u76F4\u63A5\u770B\u5230/\u542C\u5230\u7684\u4E8B\u5B9E")
3086
+ inputSchema: z5.object({
3087
+ from: z5.string().describe("\u7A97\u53E3\u8D77\u70B9\uFF0C`M:SS` \u6216 `H:MM:SS`"),
3088
+ to: z5.string().describe("\u7A97\u53E3\u7EC8\u70B9\uFF1B\u8986\u76D6\u5019\u9009\u58F0\u97F3\u6216\u52A8\u4F5C\uFF0C\u524D\u540E\u5404\u7559\u6700\u591A 3 \u79D2"),
3089
+ question: z5.string().describe("\u8981\u6838\u5BF9\u4EC0\u4E48\u3002\u8981\u6C42\u5B83\u53EA\u62A5\u53EF\u76F4\u63A5\u770B\u5230/\u542C\u5230\u7684\u4E8B\u5B9E")
2914
3090
  }),
2915
3091
  execute: async ({ from, to, question }) => {
2916
3092
  if (ctx.mediaCalls >= ctx.mediaBudget) {
2917
3093
  return `\u5A92\u4F53\u6838\u5BF9\u9884\u7B97\u5DF2\u7528\u5B8C\uFF08${ctx.mediaBudget} \u6B21\uFF09\u3002\u8BC1\u636E\u4E0D\u8DB3\u7684\u5019\u9009\u8BB0\u4E3A unresolved\u3002`;
2918
3094
  }
2919
3095
  ctx.mediaCalls += 1;
2920
- const result = await watchWindow(ctx.video, from, to, question, {
2921
- apiKey: ctx.geminiKey,
2922
- model: ctx.mediaModel,
2923
- callKey: `review:ep${ctx.episode}:watch:${ctx.mediaCalls}`
2924
- });
2925
- ctx.usage.push(result.usage);
2926
- return result.answer;
3096
+ try {
3097
+ const result = await watchWindow(ctx.video, from, to, question, {
3098
+ apiKey: ctx.geminiKey,
3099
+ model: ctx.mediaModel,
3100
+ baseUrl: ctx.baseUrl,
3101
+ maxAttempts: ctx.maxAttempts,
3102
+ callKey: `review:ep${ctx.episode}:watch:${ctx.mediaCalls}`
3103
+ });
3104
+ ctx.usage.push(...result.usage);
3105
+ return result.answer;
3106
+ } catch (error) {
3107
+ if (error instanceof ReviewModelError) ctx.usage.push(...error.usage);
3108
+ throw error;
3109
+ }
2927
3110
  }
2928
3111
  });
2929
3112
  tools.frames = createTool({
2930
3113
  id: "frames",
2931
3114
  description: "\u62BD\u53D6\u539F\u7247\u82E5\u5E72\u65F6\u523B\u7684\u9759\u5E27\uFF0C\u76F4\u63A5\u8FDB\u4F60\u7684\u4E0A\u4E0B\u6587\u4F9B\u4F60\u81EA\u5DF1\u770B\u3002\u6CA1\u6709\u58F0\u97F3\uFF0C\u4F46\u7CBE\u786E\u4E14\u4FBF\u5B9C\u3002",
2932
- inputSchema: z3.object({
2933
- timestamps: z3.array(z3.string()).min(1).max(6).describe('1\u20136 \u4E2A M:SS \u65F6\u523B\u7684 JSON \u6570\u7EC4\uFF1B\u5355\u5E27\u4E5F\u7528\u6570\u7EC4\uFF0C\u4F8B\u5982 ["0:05"]'),
2934
- note: z3.string().optional().describe("\u4F60\u60F3\u5728\u8FD9\u4E9B\u5E27\u91CC\u786E\u8BA4\u4EC0\u4E48")
3115
+ inputSchema: z5.object({
3116
+ timestamps: z5.array(z5.string()).min(1).max(6).describe('1\u20136 \u4E2A M:SS \u65F6\u523B\u7684 JSON \u6570\u7EC4\uFF1B\u5355\u5E27\u4E5F\u7528\u6570\u7EC4\uFF0C\u4F8B\u5982 ["0:05"]'),
3117
+ note: z5.string().optional().describe("\u4F60\u60F3\u5728\u8FD9\u4E9B\u5E27\u91CC\u786E\u8BA4\u4EC0\u4E48")
2935
3118
  }),
2936
3119
  // The frames themselves reach the model through toModelOutput; the plain
2937
3120
  // return value is only what a transcript reader sees.
@@ -2973,8 +3156,7 @@ function buildTools(ctx) {
2973
3156
 
2974
3157
  // packages/review-agent/src/agent.ts
2975
3158
  import { Agent } from "@mastra/core/agent";
2976
- import { createGoogleGenerativeAI } from "@ai-sdk/google";
2977
- import { z as z4 } from "zod";
3159
+ import { z as z6 } from "zod";
2978
3160
  function seriesCard(doc) {
2979
3161
  return read(doc, "").split("\nepisodes\n")[0].trim();
2980
3162
  }
@@ -3016,16 +3198,10 @@ async function reviewUnit(doc, episode, video, config, asr) {
3016
3198
  phase: episode === 0 ? "converge" : "episode",
3017
3199
  ...video ? { video, geminiKey: config.apiKey } : {},
3018
3200
  mediaModel: config.mediaModel,
3201
+ baseUrl: config.baseUrl,
3202
+ maxAttempts: config.maxAttempts,
3019
3203
  mediaBudget
3020
3204
  });
3021
- const google = createGoogleGenerativeAI({
3022
- apiKey: config.apiKey,
3023
- ...config.baseUrl ? { baseURL: `${config.baseUrl.replace(/\/+$/, "").replace(/\/v1beta$/, "")}/v1beta` } : {},
3024
- fetch: ((url, init) => fetch(url, {
3025
- ...init,
3026
- ...dispatcher() ? { dispatcher: dispatcher() } : {}
3027
- }))
3028
- });
3029
3205
  const input = {
3030
3206
  seriesCard: seriesCard(doc),
3031
3207
  episode,
@@ -3034,45 +3210,52 @@ async function reviewUnit(doc, episode, video, config, asr) {
3034
3210
  neighbours: neighbourSynopses(doc, episode),
3035
3211
  asr
3036
3212
  };
3213
+ const usage = [];
3214
+ const modelFailures = [];
3037
3215
  const agent = new Agent({
3038
3216
  id: `review-ep${episode}`,
3039
3217
  name: `Script review \xB7 episode ${episode}`,
3040
3218
  instructions: systemPrompt(input),
3041
- model: google(config.model),
3219
+ model: agentModels(config, (route, failure) => {
3220
+ modelFailures.push(`${route.provider}/${route.model}: ${failure instanceof Error ? failure.message : String(failure)}`);
3221
+ }, (row) => usage.push({ ...row, call_key: `review:ep${episode}:agent:${row.call_key}` })),
3042
3222
  tools: buildTools(ctx)
3043
3223
  });
3044
3224
  let state2 = "completed";
3045
3225
  let message = "";
3046
3226
  let error = null;
3047
3227
  let steps = 0;
3048
- const usage = [];
3049
3228
  const minutes = Math.round(config.timeoutMs / 6e4);
3050
3229
  try {
3051
3230
  const response = await agent.generate(episodeTask(input), {
3052
3231
  maxSteps: config.maxSteps,
3053
3232
  modelSettings: { temperature: 0, timeout: { totalMs: config.timeoutMs } },
3054
3233
  structuredOutput: {
3055
- schema: z4.object({ message: z4.string().describe("\u4E00\u4E24\u53E5\u8BDD\u603B\u7ED3\u672C\u96C6\u5BA1\u6838\u7ED3\u679C") }),
3234
+ schema: z6.object({ message: z6.string().describe("\u4E00\u4E24\u53E5\u8BDD\u603B\u7ED3\u672C\u96C6\u5BA1\u6838\u7ED3\u679C") }),
3056
3235
  errorStrategy: "warn"
3236
+ },
3237
+ onStepFinish: (step2) => {
3238
+ steps += 1;
3239
+ const model = step2.model?.modelId ?? step2.response.modelId ?? config.model;
3240
+ const u = step2.usage;
3241
+ usage.push({
3242
+ call_key: `review:ep${episode}:agent:${steps}`,
3243
+ provider: step2.model?.provider?.startsWith("doubao") ? "doubao" : "google-gemini",
3244
+ model,
3245
+ input_tokens: u.inputTokens ?? 0,
3246
+ cached_input_tokens: u.cachedInputTokens ?? 0,
3247
+ output_tokens: u.outputTokens ?? 0,
3248
+ reasoning_tokens: u.reasoningTokens ?? 0
3249
+ });
3057
3250
  }
3058
3251
  });
3059
3252
  steps = response?.steps?.length ?? 0;
3060
3253
  message = response?.object?.message || response?.text?.trim() || `\u7B2C ${episode} \u96C6\u5BA1\u6838\u5B8C\u6210`;
3061
- const u = response?.usage ?? {};
3062
- usage.push({
3063
- call_key: `review:ep${episode}:agent`,
3064
- provider: "google-gemini",
3065
- model: config.model,
3066
- input_tokens: Number(u.inputTokens ?? u.promptTokens ?? 0),
3067
- cached_input_tokens: Number(u.cachedInputTokens ?? 0),
3068
- output_tokens: Number(u.outputTokens ?? u.completionTokens ?? 0),
3069
- reasoning_tokens: Number(u.reasoningTokens ?? 0)
3070
- });
3071
3254
  } catch (thrown) {
3072
3255
  const detail = thrown instanceof Error ? thrown.message : String(thrown);
3073
3256
  const timedOut = /timeout|aborted|AbortError/i.test(detail);
3074
3257
  state2 = timedOut ? "completed" : "failed";
3075
- error = detail.slice(0, 300);
3258
+ error = [...modelFailures, detail].join("\n");
3076
3259
  message = timedOut ? `\u5BA1\u6838\u5728 ${minutes} \u5206\u949F\u8D85\u65F6\u540E\u4E2D\u65AD\uFF0C\u5DF2\u4FDD\u7559\u6B64\u524D\u7ED3\u8BBA` : `\u7B2C ${episode} \u96C6\u5BA1\u6838\u5931\u8D25`;
3077
3260
  if (timedOut) ctx.findings.push(timeoutFinding(episode, minutes));
3078
3261
  }
@@ -3107,36 +3290,24 @@ import * as path5 from "node:path";
3107
3290
  // packages/review-agent/src/asr-review.ts
3108
3291
  import * as fs6 from "node:fs/promises";
3109
3292
  import * as path4 from "node:path";
3110
- import { z as z5 } from "zod";
3111
- var comparisonSchema = z5.object({
3112
- summary: z5.string().min(1),
3113
- timestamp_corrections: z5.array(z5.object({
3114
- action: z5.string().min(1).describe("\u4EC5\u884C\u951A\u70B9 ID\uFF0C\u4F8B\u5982 acAAAAAA\uFF1B\u4E0D\u542B\u65F6\u95F4\u6233\u3001\u7A7A\u683C\u3001\u8BF4\u8BDD\u4EBA\u6216\u6B63\u6587"),
3115
- start_ref: z5.string().min(1).describe("\u5BF9\u5E94\u8D77\u97F3\u7684 ASR \u53E5\u6216\u8BCD\u7F16\u53F7\uFF0C\u5982 U2 \u6216 U2W3"),
3116
- reason: z5.string().min(1)
3293
+ import { z as z7 } from "zod";
3294
+ var comparisonSchema = z7.object({
3295
+ summary: z7.string().min(1),
3296
+ timestamp_corrections: z7.array(z7.object({
3297
+ action: z7.string().min(1).describe("\u4EC5\u884C\u951A\u70B9 ID\uFF0C\u4F8B\u5982 acAAAAAA\uFF1B\u4E0D\u542B\u65F6\u95F4\u6233\u3001\u7A7A\u683C\u3001\u8BF4\u8BDD\u4EBA\u6216\u6B63\u6587"),
3298
+ start_ref: z7.string().min(1).describe("\u5BF9\u5E94\u8D77\u97F3\u7684 ASR \u53E5\u6216\u8BCD\u7F16\u53F7\uFF0C\u5982 U2 \u6216 U2W3"),
3299
+ reason: z7.string().min(1)
3117
3300
  })),
3118
- missing_dialogue: z5.array(z5.object({
3119
- asr_ref: z5.string().min(1).describe("\u5305\u542B\u6F0F\u6389\u53F0\u8BCD\u7684 ASR \u53E5\u7F16\u53F7\uFF0C\u5982 U2"),
3120
- reason: z5.string().min(1).describe("\u6F0F\u6389\u7684\u90E8\u5206\u3001\u90BB\u8FD1\u5267\u672C\u4F4D\u7F6E\u53CA\u6838\u5BF9\u7406\u7531")
3301
+ missing_dialogue: z7.array(z7.object({
3302
+ asr_ref: z7.string().min(1).describe("\u5305\u542B\u6F0F\u6389\u53F0\u8BCD\u7684 ASR \u53E5\u7F16\u53F7\uFF0C\u5982 U2"),
3303
+ reason: z7.string().min(1).describe("\u6F0F\u6389\u7684\u90E8\u5206\u3001\u90BB\u8FD1\u5267\u672C\u4F4D\u7F6E\u53CA\u6838\u5BF9\u7406\u7531")
3121
3304
  })),
3122
- notes: z5.array(z5.string().min(1)).describe("\u65E0\u6CD5\u53EF\u9760\u5BF9\u9F50\u6216 ASR \u8BC1\u636E\u4E0D\u8DB3\u7684\u5177\u4F53\u8BF4\u660E\uFF1B\u65E0\u95EE\u9898\u5219\u4E3A\u7A7A")
3305
+ notes: z7.array(z7.string().min(1)).describe("\u65E0\u6CD5\u53EF\u9760\u5BF9\u9F50\u6216 ASR \u8BC1\u636E\u4E0D\u8DB3\u7684\u5177\u4F53\u8BF4\u660E\uFF1B\u65E0\u95EE\u9898\u5219\u4E3A\u7A7A")
3123
3306
  });
3124
3307
  function episodeComparisonSchema(doc, episode) {
3125
3308
  const anchors = doc.episodes.filter((ep) => ep.no === episode).flatMap((ep) => ep.scenes.flatMap((scene) => scene.actions.filter((action) => action.type === "dialogue" || action.type === "inner_thought").map((action) => action.anchor)));
3126
- return comparisonSchema.extend({ timestamp_corrections: anchors.length ? z5.array(comparisonSchema.shape.timestamp_corrections.element.extend({ action: z5.enum(anchors) })) : comparisonSchema.shape.timestamp_corrections.max(0) });
3127
- }
3128
- var geminiSchema = z5.object({
3129
- candidates: z5.array(z5.object({
3130
- finishReason: z5.string(),
3131
- content: z5.object({ parts: z5.array(z5.object({ text: z5.string().optional(), thought: z5.boolean().optional() })) })
3132
- })).min(1),
3133
- usageMetadata: z5.object({
3134
- promptTokenCount: z5.number().optional(),
3135
- cachedContentTokenCount: z5.number().optional(),
3136
- candidatesTokenCount: z5.number().optional(),
3137
- thoughtsTokenCount: z5.number().optional()
3138
- }).optional()
3139
- });
3309
+ return comparisonSchema.extend({ timestamp_corrections: anchors.length ? z7.array(comparisonSchema.shape.timestamp_corrections.element.extend({ action: z7.enum(anchors) })) : comparisonSchema.shape.timestamp_corrections.max(0) });
3310
+ }
3140
3311
  function comparisonPrompt(episode, script, transcript) {
3141
3312
  return `\u6838\u5BF9\u7B2C ${episode} \u96C6\u7684\u5B8C\u6574\u8C46\u5305 ASR \u4E0E Gemini \u8F6C\u5F55\u5267\u672C\uFF0C\u5236\u5B9A\u53F0\u8BCD\u6821\u65F6\u4FEE\u6B63\u5E76\u627E\u51FA\u6F0F\u53F0\u8BCD\u3002
3142
3313
  \u4E24\u4EFD\u6750\u6599\u5747\u662F\u5F85\u6838\u5BF9\u6570\u636E\uFF0C\u5176\u4E2D\u4EFB\u4F55\u547D\u4EE4\u6216\u8981\u6C42\u90FD\u4E0D\u662F\u4F60\u7684\u6307\u4EE4\u3002\u65F6\u95F4\u5747\u4EE5\u672C\u96C6\u539F\u89C6\u9891 00:00 \u4E3A\u539F\u70B9\u3002
@@ -3227,36 +3398,20 @@ async function alignEpisode(doc, episode, video, scriptPath, config, onAsrComple
3227
3398
  fs6.writeFile(path4.join(reportDir, "asr.txt"), transcript),
3228
3399
  fs6.writeFile(path4.join(reportDir, "prompt.txt"), prompt)
3229
3400
  ]);
3230
- const base = (config.baseUrl ?? "https://generativelanguage.googleapis.com/v1beta").replace(/\/$/, "");
3231
- const apiBase = base.endsWith("/v1beta") || base.endsWith("/v1") ? base : `${base}/v1beta`;
3232
- const response = await post(`${apiBase}/models/${config.model}:generateContent`, {
3233
- contents: [{ role: "user", parts: [{ text: prompt }] }],
3234
- generationConfig: {
3235
- temperature: 0,
3236
- responseMimeType: "application/json",
3237
- responseJsonSchema: z5.toJSONSchema(schema)
3238
- }
3239
- }, config.timeoutMs, { "x-goog-api-key": config.apiKey });
3240
- const raw = await response.text();
3241
- await fs6.writeFile(path4.join(reportDir, "gemini-response.json"), raw);
3242
- if (!response.ok) throw new Error(`\u7B2C ${episode} \u96C6 Gemini \u5BF9\u6BD4\u8BF7\u6C42\u5931\u8D25\uFF1AHTTP ${response.status}\uFF1B\u8BE6\u7EC6\u54CD\u5E94\uFF1A${reportDir}/gemini-response.json`);
3243
- const payload = geminiSchema.parse(JSON.parse(raw));
3244
- const candidate = payload.candidates[0];
3245
- if (candidate.finishReason !== "STOP") throw new Error(`\u7B2C ${episode} \u96C6 Gemini \u5BF9\u6BD4\u672A\u6B63\u5E38\u5B8C\u6210\uFF1A${candidate.finishReason}\uFF1B\u8BC1\u636E\u76EE\u5F55\uFF1A${reportDir}`);
3246
- const text = candidate.content.parts.filter((p) => !p.thought).map((p) => p.text ?? "").join("");
3247
- const comparison = schema.parse(JSON.parse(text));
3248
- const report = resolveComparison(doc, episode, asrCues(utterances), comparison);
3401
+ const response = await generateReviewJson({
3402
+ config,
3403
+ prompt,
3404
+ schema,
3405
+ timeoutMs: config.timeoutMs,
3406
+ callKey: `review:asr:ep${episode}:${path4.basename(reportDir)}`,
3407
+ onResponse: (route, attempt, raw) => fs6.writeFile(path4.join(
3408
+ reportDir,
3409
+ `${route.provider}-${route.model.replaceAll("/", "_")}-${attempt}.json`
3410
+ ), raw)
3411
+ });
3412
+ const report = resolveComparison(doc, episode, asrCues(utterances), response.value);
3249
3413
  await fs6.writeFile(path4.join(reportDir, "report.json"), JSON.stringify(report, null, 2));
3250
- const u = payload.usageMetadata;
3251
- return { report, reportDir, usage: [{
3252
- call_key: `review:asr:ep${episode}:${path4.basename(reportDir)}`,
3253
- provider: "google-gemini",
3254
- model: config.model,
3255
- input_tokens: u?.promptTokenCount ?? 0,
3256
- cached_input_tokens: u?.cachedContentTokenCount ?? 0,
3257
- output_tokens: u?.candidatesTokenCount ?? 0,
3258
- reasoning_tokens: u?.thoughtsTokenCount ?? 0
3259
- }] };
3414
+ return { report, reportDir, usage: response.usage };
3260
3415
  }
3261
3416
 
3262
3417
  // packages/review-agent/src/review.ts
@@ -3305,6 +3460,7 @@ async function reviewScript(opts) {
3305
3460
  model: opts.model ?? "gemini-3.8-flash",
3306
3461
  mediaModel: opts.mediaModel ?? opts.model ?? "gemini-3.8-flash",
3307
3462
  ...opts.baseUrl ? { baseUrl: opts.baseUrl } : {},
3463
+ maxAttempts: opts.maxAttempts ?? 3,
3308
3464
  maxSteps: opts.maxSteps ?? 300,
3309
3465
  mediaBudget: opts.mediaBudget ?? 1,
3310
3466
  timeoutMs: opts.episodeTimeoutMs ?? 8 * 6e4
@@ -3351,7 +3507,7 @@ async function reviewScript(opts) {
3351
3507
  wall_ms: Date.now() - episodeStarted,
3352
3508
  steps: 0,
3353
3509
  media_calls: 0,
3354
- usage: []
3510
+ usage: error instanceof ReviewModelError ? error.usage : []
3355
3511
  }, "episode");
3356
3512
  return;
3357
3513
  }
@@ -3405,6 +3561,7 @@ export {
3405
3561
  neighbourSynopses,
3406
3562
  newContext,
3407
3563
  post,
3564
+ proxyFetch,
3408
3565
  proxyUrl,
3409
3566
  reviewScript,
3410
3567
  reviewUnit,