@lingjingai/scriptctl 0.73.9 → 0.74.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/README.md +10 -24
- package/changes/0.74.0.md +13 -0
- package/dist/bin.js +1 -1
- package/dist/{chunk-7QE6NHW5.js → chunk-QQ7KIXWT.js} +1 -1
- package/dist/cli.js +1 -1
- package/dist/review-agent.js +341 -406
- package/package.json +1 -1
- package/skills/scriptctl/SKILL.md +6 -8
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ Script v4 的新节点使用类型前缀加 6 位 Bitcoin Base58 的 NanoID。
|
|
|
54
54
|
| 仅 `-p script.json` | 内部结构、引用、人物与状态的一致性 | `review-agent` |
|
|
55
55
|
| `-s` 文本 ingest 工作区或对应 txt/md/docx | 原文行窗口、遗漏、说话人、心声类型和语义忠实度 | `review-text` |
|
|
56
56
|
| `-s` 漫画 ingest 工作区或对应 PDF | 原页、画格、气泡与正文对照 | `review-comic` |
|
|
57
|
-
| `-s` 视频/分集目录或完整 `--video` 列表 |
|
|
57
|
+
| `-s` 视频/分集目录或完整 `--video` 列表 | 原声、画面与时间轴 | `review-agent` |
|
|
58
58
|
|
|
59
59
|
纯剧本使用独立 prompt 和工具,不要求时间戳,不生成缺失剧情;遇到无法判定哪一版正确的设定矛盾时保留原文并报告未解决项。需要事实依据的警告不靠猜测补齐。
|
|
60
60
|
|
|
@@ -68,29 +68,23 @@ Script v4 的新节点使用类型前缀加 6 位 Bitcoin Base58 的 NanoID。
|
|
|
68
68
|
|
|
69
69
|
既有 v4 产物通过 `v4_identity_map.json` 和 `pass1/body_start.json` 恢复来源,按稳定 ID 对应,不按当前行序或正文相似度猜测。保留这些文件以及 `manifest.json`、`pass1/source.txt`。缺失来源或摘要不符时停止,不转为纯剧本审核。
|
|
70
70
|
|
|
71
|
-
`.review/text/episode-N.json` 与 `global.json` 保存 findings、用量和已读/未读行窗口;读取记录不代表内容正确。未读完本集原文会留下 unresolved。文本审核使用 `--episodes`、`--converge-only`、`--max-steps` 和 `--episode-timeout-ms
|
|
71
|
+
`.review/text/episode-N.json` 与 `global.json` 保存 findings、用量和已读/未读行窗口;读取记录不代表内容正确。未读完本集原文会留下 unresolved。文本审核使用 `--episodes`、`--converge-only`、`--max-steps` 和 `--episode-timeout-ms`。
|
|
72
72
|
|
|
73
|
-
##
|
|
73
|
+
## 原视频审核
|
|
74
74
|
|
|
75
|
-
`scriptctl review -p <script.json> -s <视频目录>`(或按集排序的完整 `--video`
|
|
75
|
+
`scriptctl review -p <script.json> -s <视频目录>`(或按集排序的完整 `--video` 列表)逐集执行「Mastra 审核 → 落盘」。`--concurrency` 控制同时审核的集数,默认 **5 集并发**。需要 `ffmpeg`。
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
- **时间不齐或缺时间**:Gemini 为每条可明确对应的台词选择 ASR 句/词起点。程序取该编号的真实毫秒时间,经 screenplay SDK 批量更新本集时间戳,保留原始文字、说话人、类型及其他字段。本集校时落盘后立即启动本集 Agent,不等待其他集;纯画面动作不按 ASR 猜时间,只有后半句证据时保留整条起点。
|
|
80
|
-
- **漏台词**:记录 ASR 原话、时间范围、邻近位置和理由,不直接插入 ASR 文本。随后对应集 Agent 收到清单和切好的看片窗口,逐项 `watch` 核对原片后修复并记录 finding。每处线索按窗口数增加看片预算,长句拆成不超过 15 秒的窗口。
|
|
81
|
-
- **无法可靠对应**:保留原样,把具体说明交给 Agent。服务请求失败、无效响应或非法修正目标记为本集失败,继续后续集;失败集不应用对比方案、不启动本集 Agent。已完成集的修改保留,失败清单在 `episode` 事件及最终统计中体现。
|
|
82
|
-
|
|
83
|
-
剧本旁 `.review/asr/episode-N/` 保存按音频 SHA-256 复用的成功 ASR 响应,以及每次对比的 `script.txt`、`asr.txt`、`prompt.txt`、逐模型逐次响应和 `report.json`(含前后时间、台词、缺漏线索)。当前剧本每次重新对比,不缓存对比结论。
|
|
77
|
+
审核不做语音识别,也不自动校时:时间戳保留 ingest 的精度,Agent 在看片预算内用 `watch`、`frames` 按疑点核对原片,只修时间轴校验报出的问题和有原片证据的错位。默认每集看片预算为 1 次,时间轴问题按看片窗口补充,`--media-budget` 可调。单集失败不影响其他集,已完成集的修改保留,失败清单在 `episode` 事件及最终统计中体现。
|
|
84
78
|
|
|
85
79
|
视频和纯剧本审核使用 `review-agent`,文本和漫画使用 `review-text`、`review-comic`;主 Agent 每次模型请求按 MSUICODE Vertex → GCP → T3 → 兔子(`TUZI_API_KEY`,`https://api.tu-zi.com`)→ 豆包 Seed 2.1 Lite(`ARK_API_KEY`)→ Turbo(`DOUBAO_TURBO_API_KEY`)的顺序使用,Gemini 渠道均为 `gemini-3.8-flash`。NNYYAI 不参与 Agent 工具调用:它会把并行函数调用拆成两段,并丢失工具循环的函数结果历史。Grok 也不在默认链中:长审核历史下它只回复计划而不调用工具。MSUICODE 地址为 `http://15.204.107.123:3000`,三个来源分别读取 `MSUICODE_VERTEX_API_KEY`、`MSUICODE_GCP_API_KEY`、`MSUICODE_T3_API_KEY`。主渠道密钥必需,额外渠道仅在配置对应密钥时参与。普通服务错误按顺序切换;Gemini 返回明确的内容拦截时,当前这一步跳过剩余 Gemini 渠道交给豆包,下一步重新从 Vertex 开始。各模型沿用 provider 的 `maxAttempts`,内置最多三次。模型切换发生在当前调用内,保留成功的工具操作;用量记录实际渠道和模型,Gemini 思考签名只在生成它的渠道内复用。逐集与全剧审核都要求成功调用 validate 后才结束;模型只回复计划而没有收口时会被要求继续,全部渠道都失败时直接结束。`--no-fallback` 或 `SCRIPTCTL_REVIEW_SINGLE_MODEL=1` 仅使用选定模型。
|
|
86
80
|
|
|
87
|
-
|
|
81
|
+
原片视频核验使用 `review-media`:NNYYAI Gemini 3.8 → 3.7 → 3.6 Flash → 豆包 Seed 2.1 Lite → Turbo,凭据分别为 `NNYYAI_API_KEY`、`ARK_API_KEY`、`DOUBAO_TURBO_API_KEY`;`DOUBAO_LITE_MODEL`、`DOUBAO_TURBO_MODEL` 可覆盖对应豆包端点。显式 `--model` 或自定义配置仍决定媒体主渠道。
|
|
88
82
|
|
|
89
83
|
全剧收敛审核必须成功执行 `validate` 才能正常结束;未完成时在同一会话内继续,保留已有工具操作,并共用原步数和总时间预算。整集默认 15 分钟总时限,耗尽后结束本轮审核。
|
|
90
84
|
|
|
91
|
-
`
|
|
85
|
+
`episode` NDJSON 报告本集完成或失败,已有修改已保存;`usage_total` 汇总审核用量。
|
|
92
86
|
|
|
93
|
-
`--episodes`
|
|
87
|
+
`--episodes` 只审核所选集,适合失败集重试;`--converge-only` 只做全剧收敛。`--check` 返回所选 `review_kind`,只检查配置与 bundle,不发模型请求。
|
|
94
88
|
|
|
95
89
|
## 原漫画审核
|
|
96
90
|
|
|
@@ -100,7 +94,7 @@ Gemini 接收左侧带时间戳、行锚点的亚洲格式剧本正文,以及
|
|
|
100
94
|
|
|
101
95
|
漫画 Agent 逐页看原稿并反向核对 action,使用 `source` 定位、`pages` 看完整原页、`region` 看清晰局部,核对遗漏、重复、文字、人物、心声类型及阅读顺序。需要 Poppler(`pdftoppm`);局部图按显示页的 CropBox/旋转归一化坐标渲染。正文修改保留来源;插入遗漏内容必须同时提供 `extend.scriptctl.source` 页码及区域。跨页内容使用同一条 action 的多个来源区域,不添加时间戳。
|
|
102
96
|
|
|
103
|
-
`.review/comic/episode-N.json` 保存逐话 findings、用量和原页加载记录,`global.json` 保存最后的全剧收敛结果。`viewed_pages` 仅表示整页图片已加载,不代表内容正确或小字已看清;未加载完全部页时报告未解决项。图片证据保存在剧本旁的 `review-pages
|
|
97
|
+
`.review/comic/episode-N.json` 保存逐话 findings、用量和原页加载记录,`global.json` 保存最后的全剧收敛结果。`viewed_pages` 仅表示整页图片已加载,不代表内容正确或小字已看清;未加载完全部页时报告未解决项。图片证据保存在剧本旁的 `review-pages/`。漫画没有校时和视频媒体预算;`--max-steps` 和 `--episode-timeout-ms` 限制审核运行。逐话结果在剧本与报告落盘后才发出,未提供 `-s` 仍是纯剧本审核。
|
|
104
98
|
|
|
105
99
|
## 审核失败单元的类型
|
|
106
100
|
|
|
@@ -108,12 +102,8 @@ Gemini 接收左侧带时间戳、行锚点的亚洲格式剧本正文,以及
|
|
|
108
102
|
|
|
109
103
|
| `failure_kind` | `retry` | 含义 |
|
|
110
104
|
|---|---|---|
|
|
111
|
-
| `dialogue_cross_language` | `ours` | 剧本台词与原声语言不同,且按原声自动改写失败(对应不到一半台词) |
|
|
112
|
-
| `dialogue_mismatch` | `caller` | 台词与原声差异超过 50%,多为视频与集对应错误 |
|
|
113
|
-
| `asr_config` | `caller` | 缺少 `SD_AUDIO_APIKEY` |
|
|
114
105
|
| `video_missing` | `caller` | 该集没有传视频 |
|
|
115
106
|
| `credentials` | `caller` | 模型渠道拒绝凭据(401/403) |
|
|
116
|
-
| `asr_failed` | `retry` | ASR 或比对模型暂时不可用 |
|
|
117
107
|
| `timeout` | `retry` | 单元超时 |
|
|
118
108
|
| `model_unavailable` | `retry` | 所有模型渠道都失败 |
|
|
119
109
|
| `timeline_unresolved` | `retry_once` | 审核后时间轴仍有冲突 |
|
|
@@ -141,7 +131,7 @@ Gemini 接收左侧带时间戳、行锚点的亚洲格式剧本正文,以及
|
|
|
141
131
|
| `PROJECT_LITELLM_GATEWAY_BASE_URL` | 内置配置的 gateway endpoint;OpenAI-compatible synopsis 必须配置 |
|
|
142
132
|
| `SCRIPTCTL_LLM_CONFIG` | LLM 路由 YAML;优先级低于 `--llm-config` |
|
|
143
133
|
| `NNYYAI_CI_API_KEY` | 内置视频 ingest 首选 NNYYAI 次卡 `gemini-3.8-flash-anci` 的密钥 |
|
|
144
|
-
| `NNYYAI_API_KEY` | 视频 ingest 普通 NNYYAI
|
|
134
|
+
| `NNYYAI_API_KEY` | 视频 ingest 普通 NNYYAI 后备渠道,以及漫画拉片与独立媒体核验的密钥 |
|
|
145
135
|
| `TUZI_API_KEY` | 审核 Agent 的兔子 Gemini 渠道密钥 |
|
|
146
136
|
| `MSUICODE_VERTEX_API_KEY` | 默认审核 Agent 的 MSUICODE Vertex 主渠道密钥 |
|
|
147
137
|
| `MSUICODE_GCP_API_KEY` / `MSUICODE_T3_API_KEY` | 审核 Agent 的 MSUICODE GCP / T3 后续渠道密钥 |
|
|
@@ -265,7 +255,3 @@ scriptctl ingest --source-path ./video.mp4 --video-provider gemini
|
|
|
265
255
|
### Official Gemini via Vertex AI
|
|
266
256
|
|
|
267
257
|
The built-in official Gemini provider uses `google-vertex` with Google Application Default Credentials (ADC). Set `GOOGLE_CLOUD_PROJECT` and configure ADC on each execution machine; tokens refresh automatically. Video requests send inline media; text requests use the Vertex OpenAI-compatible API. A custom provider can declare `protocol: google-vertex`, `timeoutMs` and `maxAttempts` without an `apiKey` field. To use a Vertex API key, add `apiKey: { env: GOOGLE_API_KEY }` to that provider and set the named environment variable. Select the provider through the task profile in your LLM configuration. The project reads `VERTEX_PROJECT_ID`, falling back to `GOOGLE_CLOUD_PROJECT`; the location reads `VERTEX_LOCATION`, then `GOOGLE_CLOUD_LOCATION`, defaulting to `global`. Custom Gemini gateways retain the `google-gemini` protocol and their own credentials.
|
|
268
|
-
|
|
269
|
-
### 台词与原声门禁
|
|
270
|
-
|
|
271
|
-
有原视频的审核先比对整集剧本台词与 ASR 文字。剧本与 ASR 文字体系不同(例如英文原声配中文字幕被当成台词)时,以原声为准自动改写:模型只把每条台词对应到原声的 ASR 起止编号,台词原文全部取自 ASR(只补标点与大小写),说话人和类型不变,时间戳校准到原声起音,每条改写记一条 `fixed` 结论,NDJSON 发出 `asr-rewritten` 事件,之后照常审核。改写不到一半台词或改写后仍与原声明显不符时,本集失败(`dialogue_cross_language`)。同一种语言但字符差异超过 50%(视频绑错或台词凭空编造)时本集失败(`dialogue_mismatch`);10%–50% 的差异照常审核,并作为待核对线索交给 Agent。被拦的集不影响其余集的全剧收敛。仅在已核实并授权的任务中设置 `SCRIPTCTL_ALLOW_DIALOGUE_MISMATCH=1`,放行被拦的集且不做自动改写,仍写入原始差异报告。
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# 0.74.0
|
|
2
|
+
|
|
3
|
+
## Added
|
|
4
|
+
- None.
|
|
5
|
+
|
|
6
|
+
## Changed
|
|
7
|
+
- 视频审核不再做语音识别和自动校时:去掉豆包 ASR、Gemini 对比、台词差异门禁和按原声自动改写,不再需要 `SD_AUDIO_APIKEY`。台词由 Agent 在看片预算内按疑点核对,时间戳保留 ingest 的精度。
|
|
8
|
+
|
|
9
|
+
## Fixed
|
|
10
|
+
- None.
|
|
11
|
+
|
|
12
|
+
## Breaking
|
|
13
|
+
- 移除 NDJSON 事件 `asr-started`、`asr-transcribed`、`asr-skipped`、`asr-compared`、`asr-applied`、`asr-rewritten`,失败类型 `dialogue_cross_language`、`dialogue_mismatch`、`asr_config`、`asr_failed`,工具 `asr_window` 和环境变量 `SCRIPTCTL_ALLOW_DIALOGUE_MISMATCH`。`done.statistics.fixed` 不再包含校时条数。
|
package/dist/bin.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{b as s}from"./chunk-
|
|
2
|
+
import{b as s}from"./chunk-QQ7KIXWT.js";import{Console as r}from"node:console";var o=new r({stdout:process.stderr,stderr:process.stderr});console.debug=o.debug.bind(o);console.info=o.info.bind(o);console.log=o.log.bind(o);s(process.argv.slice(2)).then(e=>{process.exitCode=e}).catch(e=>{console.error(e),process.exitCode=70});
|
|
@@ -219,7 +219,7 @@ ${s}`);continue}let c=i.length>1?i[1]??"":"";c||(c=n),n=c;let a=[];s&&a.push(`${
|
|
|
219
219
|
`),{line:p}=qe.object({line:qe.number().int().nonnegative()}).parse(r("pass1/body_start.json"));p>=d.length&&Kt("Text body start is outside the source snapshot.");let l=_S.parse(r("v4_identity_map.json")),f=Mo(JSON.parse(Xn.readFileSync(t,"utf8")));f.version!==4&&Kt("Text source review requires the ingested Script v4.");let u=wa(s.source.sha256),h=Object.entries(l.episode).map(([y,_])=>({id:_,...Lp(y,p)})).sort((y,_)=>y.start_line-_.start_line);h.some(y=>y.end_line>d.length)&&Kt("An episode's ingest window is outside the original source.");let m=f.episodes.filter(y=>!n?.length||n.includes(y.no)).map(y=>{let _=h.filter(A=>A.id===y.id);_.length!==1&&Kt(`Episode ${y.no} has no unambiguous text source identity.`);let x=_[0],E=h[h.indexOf(x)+1];return{episode:y.no,start_line:h.indexOf(x)===0?p+1:x.start_line,end_line:E?E.end_line:d.length}});m.length||Kt("No selected episodes exist in this script.");let g=new Map,w=new Map(Object.entries(l.action).map(([y,_])=>[_,Np(y,u,p)]));for(let y of bn(f)){if(!m.some(E=>E.episode===y.scene.ep.no))continue;let _=qe.object({scriptctl:qe.object({source:Op.optional()}).optional()}).safeParse(y.raw.extend??{});_.success||Kt(`Invalid text source metadata on ${y.anchor}.`);let x=_.data.scriptctl?.source??w.get(y.anchor);x&&(x.document_id!==u&&Kt(`Action ${y.anchor} belongs to a different text document.`),x.ranges.some(E=>E.start_line>E.end_line||E.end_line>d.length)&&Kt(`Invalid source lines on ${y.anchor}.`),g.set(y.anchor,x))}return{document_id:u,title:jt.basename(s.source.path),path:c,lines:d,episodes:m,references:g}}var sg="review-agent",SS=["minimal","low","medium","high"];function ka(e,t){let n=t||sg,o=e.models[n];if(!o)throw new S(`LLM profile "${n}" is not defined.`,{exitCode:b,errorCode:"REVIEW_PROFILE_UNKNOWN",hint:`Define it under models, or pick one of: ${Object.keys(e.models).join(", ")}.`,data:{expected:["a defined models entry"],actual:[n]}});let i=e.providers[o.provider];if(!i)throw new S(`Provider "${o.provider}" is not defined.`,{exitCode:b,errorCode:"REVIEW_PROVIDER_UNKNOWN",hint:`Available providers: ${Object.keys(e.providers).join(", ")}.`,data:{expected:["a defined providers entry"],actual:[o.provider]}});let r=(process.env[i.apiKeyEnv]??"").trim();if(!r&&i.protocol!=="google-vertex")throw new S(`${i.apiKeyEnv} is not set.`,{exitCode:b,errorCode:"REVIEW_API_KEY_MISSING",hint:`Export ${i.apiKeyEnv} for provider ${o.provider}.`,data:{expected:[i.apiKeyEnv],actual:["<empty>"]}});let s=i.protocol.startsWith("google")&&SS.includes(o.reasoning??"")?o.reasoning:void 0;return{provider:o.provider,model:o.model,baseUrl:i.baseUrl,apiKey:r,maxAttempts:i.maxAttempts,protocol:i.protocol,responseSchemaField:i.videoResponseSchemaField,...s?{thinking:s}:{}}}function rg(e){let{baseUrl:t,...n}=e;return{...n,...t?{baseUrl:t}:{}}}function Up(e,t,n=!1){let o=rg(ka(e,t));if(n||process.env.SCRIPTCTL_REVIEW_SINGLE_MODEL==="1")return{...o,singleModel:!0};let i=(e.models[t]?.fallbacks??[]).flatMap(r=>{let s=e.models[r],c=s&&e.providers[s.provider];return c&&c.protocol!=="google-vertex"&&!process.env[c.apiKeyEnv]?.trim()?[]:[rg(ka(e,r))]});return{...o,fallbacks:i}}async function nn(){let e=ba.dirname(ES(import.meta.url)),t=ba.join(e,"review-agent.js"),n=ui.existsSync(t)?xS(t).href:"@lingjingai/review-agent";try{return await import(n)}catch(o){throw new S("The review agent is not installed.",{exitCode:X,errorCode:"REVIEW_AGENT_MISSING",hint:"Reinstall scriptctl without --omit=optional, or install @mastra/core and @ai-sdk/google.",data:{expected:["@lingjingai/review-agent"],actual:[String(o.message).slice(0,200)]}})}}function es(e){let t=e.trim();if(!t)return;let n=t.split(",").map(o=>Number(o.trim()));if(n.some(o=>!Number.isInteger(o)||o<1))throw new S("--episodes takes a comma-separated list of 1-based episode numbers.",{exitCode:b,errorCode:"REVIEW_EPISODES_INVALID",hint:"For example: --episodes 1,2,5",data:{expected:["1-based episode numbers"],actual:[t]}});return n}function at(e,t,n){let o=e[t];if(o===void 0)return n;let i=Number(o);if(!Number.isFinite(i)||i<=0)throw new S(`--${t.replace(/_/g,"-")} must be a positive number.`,{exitCode:b,errorCode:"REVIEW_OPTION_INVALID",hint:`Pass a positive number for --${t.replace(/_/g,"-")}.`,data:{expected:["a positive number"],actual:[String(o)]}});return i}async function ag(e){let t=bo(O(e.llm_config).trim()||void 0),n=O(e.model).trim(),o=O(e.source_path).trim(),i=(Array.isArray(e.video)?e.video:[]).map(m=>m.trim()).filter(m=>m.length>0),r=Fm(o,i.length>0),c=Up(t,n||(r==="comic"?"review-comic":r==="text"?"review-text":sg),e.fallback===!1),a=O(e.media_model).trim(),d=r==="video"?a?Up(t,a,e.fallback===!1):t.source==="builtin"&&!n?Up(t,"review-media",e.fallback===!1):c:void 0,p=O(e.assets).split(",").map(m=>m.trim()).filter(Boolean);if(p.length&&(r!=="video"||e.converge_only!==!0||e.converge===!1))throw new S("--assets \u7528\u4E8E\u89C6\u9891\u8D44\u4EA7\u5B9A\u70B9\u590D\u6838\uFF0C\u8BF7\u540C\u65F6\u63D0\u4F9B --converge-only \u4E0E\u89C6\u9891\u6765\u6E90\u3002",{exitCode:b,errorCode:"REVIEW_OPTION_INVALID",hint:"\u7528 --assets actXXXXXX --converge-only\uFF1B--episodes \u53EF\u9650\u5B9A\u4EBA\u7269\u9636\u6BB5\u590D\u6838\u96C6\u6570\u3002",data:{expected:["video review with --converge-only"],actual:[r]}});if(e.check){let m=await nn(),w=typeof{script:m.reviewPlainScript,text:m.reviewTextScript,comic:m.reviewComicScript,video:m.reviewVideoScript}[r]=="function";return[{text:w?`ready: ${c.provider}/${c.model}${d?`
|
|
220
220
|
media: ${d.provider}/${d.model}`:""}`:"the review agent bundle is present but exports no reviewScript",data:{ready:w,review_kind:r,provider:c.provider,model:c.model,...d?{media_provider:d.provider,media_model:d.model}:{},llm_config:t.sourcePath??"builtin"}},w?G:X]}let l=me(e);if(!ui.existsSync(l))throw new S("Local script file not found.",{exitCode:b,errorCode:"SCRIPT_NOT_FOUND",hint:"Run `scriptctl checkout`, `scriptctl create`, or pass --script-path to an existing script.",data:{expected:["existing local script JSON"],actual:[l]}});let f=i.filter(m=>!ui.existsSync(m));if(f.length>0)throw new S("A --video path does not exist.",{exitCode:b,errorCode:"REVIEW_SOURCE_NOT_FOUND",hint:"Pass --video once per episode, in episode order.",data:{expected:["existing video files"],actual:f.slice(0,5)}});if(o&&!ui.existsSync(o))throw new S("Review source not found.",{exitCode:b,errorCode:"REVIEW_SOURCE_NOT_FOUND",hint:"Pass an existing source or ingest workspace, or omit -s for script-only review.",data:{expected:["existing source or ingest workspace"],actual:[o]}});let u=await nn(),h=m=>process.stdout.write(JSON.stringify(m)+`
|
|
221
221
|
`);try{let m={scriptPath:l,...es(O(e.episodes))?{episodes:es(O(e.episodes))}:{},concurrency:at(e,"concurrency",5),converge:e.converge!==!1,convergeOnly:e.converge_only===!0,resumeUnits:(Array.isArray(e.resume_log)?e.resume_log:[]).flatMap(g=>ui.readFileSync(g,"utf8").split(`
|
|
222
|
-
`).flatMap(w=>{let y;try{y=JSON.parse(w)}catch{return[]}return y&&(y.type==="episode"||y.type==="global")?[y]:[]})),...c,maxSteps:at(e,"max_steps",300),episodeTimeoutMs:at(e,"episode_timeout_ms",9e5),onEvent:h};if(r==="comic"){let g=await zm(o,l,m.converge?void 0:m.episodes);await u.reviewComicScript({...m,comic:g})}else if(r==="text"){let g=await ig(o,l,m.converge?void 0:m.episodes);await u.reviewTextScript({...m,text:g})}else r==="video"?await u.reviewVideoScript({...m,source:o,videos:i,mediaConfig:d,...p.length?{convergenceAssets:p}:{},strategy:e.review_strategy==="batch"?"batch":"legacy",mediaModel:d.model,...e.media_budget===void 0?{}:{mediaBudget:at(e,"media_budget",1)}}):await u.reviewPlainScript(m)}catch(m){throw m instanceof S?m:new S("Review failed.",{exitCode:X,errorCode:"REVIEW_FAILED",hint:r==="video"?"Read the error details
|
|
222
|
+
`).flatMap(w=>{let y;try{y=JSON.parse(w)}catch{return[]}return y&&(y.type==="episode"||y.type==="global")?[y]:[]})),...c,maxSteps:at(e,"max_steps",300),episodeTimeoutMs:at(e,"episode_timeout_ms",9e5),onEvent:h};if(r==="comic"){let g=await zm(o,l,m.converge?void 0:m.episodes);await u.reviewComicScript({...m,comic:g})}else if(r==="text"){let g=await ig(o,l,m.converge?void 0:m.episodes);await u.reviewTextScript({...m,text:g})}else r==="video"?await u.reviewVideoScript({...m,source:o,videos:i,mediaConfig:d,...p.length?{convergenceAssets:p}:{},strategy:e.review_strategy==="batch"?"batch":"legacy",mediaModel:d.model,...e.media_budget===void 0?{}:{mediaBudget:at(e,"media_budget",1)}}):await u.reviewPlainScript(m)}catch(m){throw m instanceof S?m:new S("Review failed.",{exitCode:X,errorCode:"REVIEW_FAILED",hint:r==="video"?"Read the error details and NDJSON episode reports before retrying.":`Read the error details and episode reports${r==="script"?"":` in .review/${r}/`} before retrying.`,data:{expected:["a completed review run"],actual:[m instanceof Error?m.message:String(m)]}})}return[{text:"",data:{}},G]}import*as Rn from"node:fs";import*as ge from"node:fs";import*as Ee from"node:path";import{createHash as IS}from"node:crypto";var ts=["analyze","direction","plan","write","review"],AS={analyze:"reskin",direction:"reskin-claude",plan:"reskin-claude",write:"reskin",review:"reskin-claude"},kS={"claude-opus-5-5":{input:4,cached_input:.2,output:20}};function Pp(e,t){let n=Ee.join(e,t);return{root:n,source:Ee.join(n,"0_input","script.json"),analysis:Ee.join(n,"1_src","\u62C6\u89E3.json"),direction:Ee.join(n,"2_new","\u6362\u58F3\u65B9\u5411.json"),brief:Ee.join(n,"2_new","\u6362\u58F3\u65B9\u5411.md"),plan:Ee.join(n,"2_new","\u6362\u58F3\u65B9\u6848.json"),script:Ee.join(n,"4_output","script.json"),log:o=>Ee.join(n,"logs",`${o}.ndjson`),status:Ee.join(n,"status.json")}}function va(e,t){ge.mkdirSync(Ee.dirname(e),{recursive:!0}),ge.writeFileSync(`${e}.tmp`,JSON.stringify(t,null,2)+`
|
|
223
223
|
`),ge.renameSync(`${e}.tmp`,e)}function Cp(e){return ge.existsSync(e)?JSON.parse(ge.readFileSync(e,"utf8")):null}function bS(e,t){let n=Object.keys(e).filter(o=>t.startsWith(o)).sort((o,i)=>i.length-o.length)[0];return n?e[n]:null}function cg(e,t){let n=0,o=[];for(let[i,r]of Object.entries(e)){let s=bS(t,i);if(!s){o.push(i);continue}n+=((r.input-r.cached_input)*s.input+r.cached_input*s.cached_input+r.output*s.output)/1e6}return{usd:n,unpriced:o}}function vS(e,t){for(let n of t){let o=e[n.model]??={calls:0,input:0,cached_input:0,output:0};o.calls+=1,o.input+=n.input_tokens,o.cached_input+=n.cached_input_tokens,o.output+=n.output_tokens}}var OS=new Set(["unit-done","episode","global","direction-done"]),RS=["\u987A","\u8981\u6539\u505A\u6CD5","\u7275\u5F3A","\u6491\u4E0D\u4F4F"];function MS(e){if(!e)return"\u672A\u5BA1\u8BFB";let t=[...e.stages,...e.points];return RS.map(n=>[n,t.filter(o=>o.grade===n).length]).filter(([,n])=>n).map(([n,o])=>`${n} ${o}`).join(" \xB7 ")}function dg(e){return e?[...e.stages.filter(t=>t.grade!=="\u987A").map(t=>`${t.stage}\uFF08${t.grade}\uFF09\uFF1A${t.note}`),...e.points.filter(t=>t.grade!=="\u987A").map(t=>`${t.point}\uFF08${t.grade}\uFF09\uFF1A${t.note}`)]:[]}function LS(e,t){return IS("sha256").update(JSON.stringify([ge.readFileSync(e,"utf8"),t.notes??"",t.custom??"",t.market??"",t.rating??"",t.avoid??[]])).digest("hex")}function NS(e,t,n){let o=n.market?.trim();return[`# ${e.title}`,`\u76EE\u6807\u5E02\u573A\uFF1A${o||"\u540C\u539F\u5267"}`,o&&n.language?.trim()?`\u53F0\u8BCD\u8BED\u8A00\uFF1A${n.language.trim()}`:"",`\u5C3A\u5EA6\uFF1A${n.rating?.trim()||"\u540C\u539F\u5267"}`,`\u6362\u58F3\uFF1A${e.shell}`,`\u6362\u6CD5\uFF1A${e.approach}`,e.keeps?`\u6CBF\u7528\u539F\u5267\uFF1A${e.keeps}`:"",e.changes?`\u5FC5\u987B\u6539\u52A8\uFF1A${e.changes}`:"",e.risks?`\u98CE\u9669\uFF1A${e.risks}`:"",dg(e.review).length?`\u72EC\u7ACB\u5BA1\u8BFB\u6307\u51FA\u7684\u627F\u63A5\u95EE\u9898\uFF0C\u65B9\u6848\u91CC\u8981\u89E3\u51B3\uFF1A
|
|
224
224
|
${dg(e.review).map(i=>`- ${i}`).join(`
|
|
225
225
|
`)}`:"",e.research?`\u65B0\u58F3\u5728\u5F53\u5730\u7684\u73B0\u5B9E\uFF08\u8054\u7F51\u8C03\u7814\uFF0C\u5177\u4F53\u505A\u6CD5\u4F9D\u636E\u5B83\u8BBE\u8BA1\uFF09\uFF1A
|
package/dist/cli.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{a,b}from"./chunk-
|
|
1
|
+
import{a,b}from"./chunk-QQ7KIXWT.js";export{a as buildProgram,b as main};
|