@lingjingai/scriptctl 0.35.0 → 0.36.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 +21 -5
- package/changes/0.36.0.md +22 -0
- package/dist/bin.js +0 -0
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +52 -128
- package/dist/cli.js.map +1 -1
- package/dist/common.d.ts +0 -16
- package/dist/common.js +2 -342
- package/dist/common.js.map +1 -1
- package/dist/domain/ingest/apply-plan.d.ts +23 -0
- package/dist/domain/ingest/apply-plan.js +210 -0
- package/dist/domain/ingest/apply-plan.js.map +1 -0
- package/dist/domain/ingest/assembler.d.ts +57 -0
- package/dist/domain/ingest/assembler.js +181 -0
- package/dist/domain/ingest/assembler.js.map +1 -0
- package/dist/domain/ingest/chunker.d.ts +9 -0
- package/dist/domain/ingest/chunker.js +38 -0
- package/dist/domain/ingest/chunker.js.map +1 -0
- package/dist/domain/ingest/evidence.d.ts +5 -0
- package/dist/domain/ingest/evidence.js +66 -0
- package/dist/domain/ingest/evidence.js.map +1 -0
- package/dist/domain/ingest/govern.d.ts +3 -0
- package/dist/domain/ingest/govern.js +127 -0
- package/dist/domain/ingest/govern.js.map +1 -0
- package/dist/domain/ingest/jsonl-parser.d.ts +6 -0
- package/dist/domain/ingest/jsonl-parser.js +187 -0
- package/dist/domain/ingest/jsonl-parser.js.map +1 -0
- package/dist/domain/ingest/markdown-parser.d.ts +6 -0
- package/dist/domain/ingest/markdown-parser.js +186 -0
- package/dist/domain/ingest/markdown-parser.js.map +1 -0
- package/dist/domain/ingest/records.d.ts +71 -0
- package/dist/domain/ingest/records.js +9 -0
- package/dist/domain/ingest/records.js.map +1 -0
- package/dist/domain/ingest/renumber.d.ts +14 -0
- package/dist/domain/ingest/renumber.js +130 -0
- package/dist/domain/ingest/renumber.js.map +1 -0
- package/dist/domain/ingest/script-v3.d.ts +94 -0
- package/dist/domain/ingest/script-v3.js +7 -0
- package/dist/domain/ingest/script-v3.js.map +1 -0
- package/dist/domain/ingest/state-reconcile.d.ts +11 -0
- package/dist/domain/ingest/state-reconcile.js +90 -0
- package/dist/domain/ingest/state-reconcile.js.map +1 -0
- package/dist/domain/ingest/validation.d.ts +11 -0
- package/dist/domain/ingest/validation.js +168 -0
- package/dist/domain/ingest/validation.js.map +1 -0
- package/dist/domain/ingest/video-aggregate.d.ts +23 -0
- package/dist/domain/ingest/video-aggregate.js +89 -0
- package/dist/domain/ingest/video-aggregate.js.map +1 -0
- package/dist/domain/ingest/video-apply.d.ts +37 -0
- package/dist/domain/ingest/video-apply.js +236 -0
- package/dist/domain/ingest/video-apply.js.map +1 -0
- package/dist/domain/ingest/video-md-parser.d.ts +2 -0
- package/dist/domain/ingest/video-md-parser.js +233 -0
- package/dist/domain/ingest/video-md-parser.js.map +1 -0
- package/dist/domain/ingest/video-records.d.ts +56 -0
- package/dist/domain/ingest/video-records.js +9 -0
- package/dist/domain/ingest/video-records.js.map +1 -0
- package/dist/domain/loom/index.d.ts +2 -1
- package/dist/domain/loom/index.js +12 -10
- package/dist/domain/loom/index.js.map +1 -1
- package/dist/domain/loom/types.d.ts +3 -2
- package/dist/domain/script/lookups.d.ts +3 -2
- package/dist/domain/script/lookups.js +25 -8
- package/dist/domain/script/lookups.js.map +1 -1
- package/dist/domain/script/patch/apply.js +2 -2
- package/dist/domain/script/patch/apply.js.map +1 -1
- package/dist/domain/script/patch/helpers.d.ts +3 -7
- package/dist/domain/script/patch/helpers.js +23 -96
- package/dist/domain/script/patch/helpers.js.map +1 -1
- package/dist/domain/script/patch/ops-action.d.ts +0 -2
- package/dist/domain/script/patch/ops-action.js +34 -100
- package/dist/domain/script/patch/ops-action.js.map +1 -1
- package/dist/domain/script/patch/ops-asset.d.ts +1 -0
- package/dist/domain/script/patch/ops-asset.js +102 -155
- package/dist/domain/script/patch/ops-asset.js.map +1 -1
- package/dist/domain/script/patch/ops-dialogue.d.ts +1 -4
- package/dist/domain/script/patch/ops-dialogue.js +43 -220
- package/dist/domain/script/patch/ops-dialogue.js.map +1 -1
- package/dist/domain/script/patch/ops-extend.js +8 -2
- package/dist/domain/script/patch/ops-extend.js.map +1 -1
- package/dist/domain/script/patch/ops-meta.js.map +1 -1
- package/dist/domain/script/patch/ops-scene.js +26 -39
- package/dist/domain/script/patch/ops-scene.js.map +1 -1
- package/dist/domain/script/patch/ops-state.d.ts +3 -3
- package/dist/domain/script/patch/ops-state.js +33 -49
- package/dist/domain/script/patch/ops-state.js.map +1 -1
- package/dist/domain/script/patch/registry.js +9 -13
- package/dist/domain/script/patch/registry.js.map +1 -1
- package/dist/domain/script/refs.js +23 -111
- package/dist/domain/script/refs.js.map +1 -1
- package/dist/domain/script/scene-refs.d.ts +3 -0
- package/dist/domain/script/scene-refs.js +35 -0
- package/dist/domain/script/scene-refs.js.map +1 -0
- package/dist/domain/script/schema.js +17 -21
- package/dist/domain/script/schema.js.map +1 -1
- package/dist/domain/script/text-util.d.ts +7 -0
- package/dist/domain/script/text-util.js +91 -0
- package/dist/domain/script/text-util.js.map +1 -0
- package/dist/help-text.js +117 -410
- package/dist/help-text.js.map +1 -1
- package/dist/infra/converters.js +1 -1
- package/dist/infra/converters.js.map +1 -1
- package/dist/infra/episode-title.d.ts +4 -0
- package/dist/infra/episode-title.js +129 -0
- package/dist/infra/episode-title.js.map +1 -0
- package/dist/infra/ingest/review-renderer.d.ts +1 -0
- package/dist/infra/ingest/review-renderer.js +431 -0
- package/dist/infra/ingest/review-renderer.js.map +1 -0
- package/dist/infra/ingest/source-resolver.d.ts +21 -0
- package/dist/infra/ingest/source-resolver.js +51 -0
- package/dist/infra/ingest/source-resolver.js.map +1 -0
- package/dist/infra/ingest/text-reader.d.ts +3 -0
- package/dist/infra/ingest/text-reader.js +27 -0
- package/dist/infra/ingest/text-reader.js.map +1 -0
- package/dist/infra/ingest/video-discovery.d.ts +8 -0
- package/dist/infra/ingest/video-discovery.js +60 -0
- package/dist/infra/ingest/video-discovery.js.map +1 -0
- package/dist/infra/ingest/workspace-store.d.ts +18 -0
- package/dist/infra/ingest/workspace-store.js +70 -0
- package/dist/infra/ingest/workspace-store.js.map +1 -0
- package/dist/infra/llm/anthropic-model.d.ts +11 -0
- package/dist/infra/llm/anthropic-model.js +109 -0
- package/dist/infra/llm/anthropic-model.js.map +1 -0
- package/dist/infra/llm/gemini-upload.d.ts +9 -0
- package/dist/infra/llm/gemini-upload.js +94 -0
- package/dist/infra/llm/gemini-upload.js.map +1 -0
- package/dist/infra/llm/gemini-video-model.d.ts +47 -0
- package/dist/infra/llm/gemini-video-model.js +167 -0
- package/dist/infra/llm/gemini-video-model.js.map +1 -0
- package/dist/infra/llm/openai-compatible-model.d.ts +13 -0
- package/dist/infra/llm/openai-compatible-model.js +143 -0
- package/dist/infra/llm/openai-compatible-model.js.map +1 -0
- package/dist/infra/llm/proxy.d.ts +1 -0
- package/dist/infra/llm/proxy.js +30 -0
- package/dist/infra/llm/proxy.js.map +1 -0
- package/dist/infra/llm/retry.d.ts +5 -0
- package/dist/infra/llm/retry.js +20 -0
- package/dist/infra/llm/retry.js.map +1 -0
- package/dist/llm/composition.d.ts +5 -0
- package/dist/llm/composition.js +5 -0
- package/dist/llm/composition.js.map +1 -0
- package/dist/llm/config.d.ts +26 -0
- package/dist/llm/config.js +201 -0
- package/dist/llm/config.js.map +1 -0
- package/dist/llm/registry.d.ts +44 -0
- package/dist/llm/registry.js +48 -0
- package/dist/llm/registry.js.map +1 -0
- package/dist/llm/router.d.ts +23 -0
- package/dist/llm/router.js +0 -0
- package/dist/llm/router.js.map +1 -0
- package/dist/llm/tasks/ingest/asset-curate.d.ts +13 -0
- package/dist/llm/tasks/ingest/asset-curate.js +45 -0
- package/dist/llm/tasks/ingest/asset-curate.js.map +1 -0
- package/dist/llm/tasks/ingest/asset-extract.d.ts +12 -0
- package/dist/llm/tasks/ingest/asset-extract.js +51 -0
- package/dist/llm/tasks/ingest/asset-extract.js.map +1 -0
- package/dist/llm/tasks/ingest/asset-govern.d.ts +22 -0
- package/dist/llm/tasks/ingest/asset-govern.js +143 -0
- package/dist/llm/tasks/ingest/asset-govern.js.map +1 -0
- package/dist/llm/tasks/ingest/mention-resolve.d.ts +9 -0
- package/dist/llm/tasks/ingest/mention-resolve.js +39 -0
- package/dist/llm/tasks/ingest/mention-resolve.js.map +1 -0
- package/dist/llm/tasks/ingest/plot-speaker.d.ts +11 -0
- package/dist/llm/tasks/ingest/plot-speaker.js +72 -0
- package/dist/llm/tasks/ingest/plot-speaker.js.map +1 -0
- package/dist/llm/tasks/ingest/roster-merge.d.ts +46 -0
- package/dist/llm/tasks/ingest/roster-merge.js +235 -0
- package/dist/llm/tasks/ingest/roster-merge.js.map +1 -0
- package/dist/llm/tasks/ingest/state-merge.d.ts +10 -0
- package/dist/llm/tasks/ingest/state-merge.js +84 -0
- package/dist/llm/tasks/ingest/state-merge.js.map +1 -0
- package/dist/llm/tasks/ingest/state-reconcile.d.ts +8 -0
- package/dist/llm/tasks/ingest/state-reconcile.js +44 -0
- package/dist/llm/tasks/ingest/state-reconcile.js.map +1 -0
- package/dist/llm/tasks/ingest/text-normalize.d.ts +10 -0
- package/dist/llm/tasks/ingest/text-normalize.js +89 -0
- package/dist/llm/tasks/ingest/text-normalize.js.map +1 -0
- package/dist/llm/tasks/ingest/video-correct.d.ts +30 -0
- package/dist/llm/tasks/ingest/video-correct.js +144 -0
- package/dist/llm/tasks/ingest/video-correct.js.map +1 -0
- package/dist/llm/tasks/ingest/video-transcribe.d.ts +4 -0
- package/dist/llm/tasks/ingest/video-transcribe.js +105 -0
- package/dist/llm/tasks/ingest/video-transcribe.js.map +1 -0
- package/dist/llm/tasks/schemas.d.ts +5 -0
- package/dist/llm/tasks/schemas.js +107 -0
- package/dist/llm/tasks/schemas.js.map +1 -0
- package/dist/llm/types.d.ts +97 -0
- package/dist/llm/types.js +13 -0
- package/dist/llm/types.js.map +1 -0
- package/dist/usecases/doctor.d.ts +1 -1
- package/dist/usecases/doctor.js +36 -9
- package/dist/usecases/doctor.js.map +1 -1
- package/dist/usecases/ingest/command.d.ts +5 -0
- package/dist/usecases/ingest/command.js +57 -0
- package/dist/usecases/ingest/command.js.map +1 -0
- package/dist/usecases/ingest/fanout.d.ts +10 -0
- package/dist/usecases/ingest/fanout.js +23 -0
- package/dist/usecases/ingest/fanout.js.map +1 -0
- package/dist/usecases/ingest/pass4-mention-resolve.d.ts +11 -0
- package/dist/usecases/ingest/pass4-mention-resolve.js +52 -0
- package/dist/usecases/ingest/pass4-mention-resolve.js.map +1 -0
- package/dist/usecases/ingest/pass5-extract.d.ts +7 -0
- package/dist/usecases/ingest/pass5-extract.js +50 -0
- package/dist/usecases/ingest/pass5-extract.js.map +1 -0
- package/dist/usecases/ingest/pass5-govern.d.ts +13 -0
- package/dist/usecases/ingest/pass5-govern.js +155 -0
- package/dist/usecases/ingest/pass5-govern.js.map +1 -0
- package/dist/usecases/ingest/pipeline.d.ts +28 -0
- package/dist/usecases/ingest/pipeline.js +144 -0
- package/dist/usecases/ingest/pipeline.js.map +1 -0
- package/dist/usecases/ingest/publish-command.d.ts +4 -0
- package/dist/usecases/ingest/publish-command.js +77 -0
- package/dist/usecases/ingest/publish-command.js.map +1 -0
- package/dist/usecases/ingest/status.d.ts +39 -0
- package/dist/usecases/ingest/status.js +155 -0
- package/dist/usecases/ingest/status.js.map +1 -0
- package/dist/usecases/ingest/video-pass3.d.ts +26 -0
- package/dist/usecases/ingest/video-pass3.js +0 -0
- package/dist/usecases/ingest/video-pass3.js.map +1 -0
- package/dist/usecases/ingest/video-pipeline.d.ts +32 -0
- package/dist/usecases/ingest/video-pipeline.js +202 -0
- package/dist/usecases/ingest/video-pipeline.js.map +1 -0
- package/dist/usecases/ingest/view-command.d.ts +4 -0
- package/dist/usecases/ingest/view-command.js +51 -0
- package/dist/usecases/ingest/view-command.js.map +1 -0
- package/dist/usecases/script/actions.js +2 -2
- package/dist/usecases/script/actions.js.map +1 -1
- package/dist/usecases/script/actor.js.map +1 -1
- package/dist/usecases/script/actors.js.map +1 -1
- package/dist/usecases/script/add-actor.js.map +1 -1
- package/dist/usecases/script/add-episode.js.map +1 -1
- package/dist/usecases/script/add-location.js.map +1 -1
- package/dist/usecases/script/add-prop.js.map +1 -1
- package/dist/usecases/script/alias.js.map +1 -1
- package/dist/usecases/script/assets.js.map +1 -1
- package/dist/usecases/script/context.js.map +1 -1
- package/dist/usecases/script/create.js +3 -6
- package/dist/usecases/script/create.js.map +1 -1
- package/dist/usecases/script/delete.js.map +1 -1
- package/dist/usecases/script/describe.js.map +1 -1
- package/dist/usecases/script/dialogue.js +6 -13
- package/dist/usecases/script/dialogue.js.map +1 -1
- package/dist/usecases/script/do.js +1 -1
- package/dist/usecases/script/do.js.map +1 -1
- package/dist/usecases/script/episodes.js.map +1 -1
- package/dist/usecases/script/{add-speaker.d.ts → importance.d.ts} +1 -1
- package/dist/usecases/script/importance.js +17 -0
- package/dist/usecases/script/importance.js.map +1 -0
- package/dist/usecases/script/insert.js.map +1 -1
- package/dist/usecases/script/issues.js.map +1 -1
- package/dist/usecases/script/lib.d.ts +8 -6
- package/dist/usecases/script/lib.js +81 -97
- package/dist/usecases/script/lib.js.map +1 -1
- package/dist/usecases/script/locations.js.map +1 -1
- package/dist/usecases/script/merge.js.map +1 -1
- package/dist/usecases/script/move.js.map +1 -1
- package/dist/usecases/script/props.js.map +1 -1
- package/dist/usecases/script/refs.js.map +1 -1
- package/dist/usecases/script/rename.js.map +1 -1
- package/dist/usecases/script/replace.js.map +1 -1
- package/dist/usecases/script/role.js.map +1 -1
- package/dist/usecases/script/scenes.js +8 -15
- package/dist/usecases/script/scenes.js.map +1 -1
- package/dist/usecases/script/script-patch.js +1 -1
- package/dist/usecases/script/script-patch.js.map +1 -1
- package/dist/usecases/script/script-validate.js +1 -1
- package/dist/usecases/script/script-validate.js.map +1 -1
- package/dist/usecases/script/session.d.ts +4 -4
- package/dist/usecases/script/session.js +57 -78
- package/dist/usecases/script/session.js.map +1 -1
- package/dist/usecases/script/split.js.map +1 -1
- package/dist/usecases/script/state-add.js.map +1 -1
- package/dist/usecases/script/state-delete.js.map +1 -1
- package/dist/usecases/script/state-rename.js.map +1 -1
- package/dist/usecases/script/summary.js.map +1 -1
- package/dist/usecases/script/synopsis-generate.d.ts +4 -5
- package/dist/usecases/script/synopsis-generate.js +30 -46
- package/dist/usecases/script/synopsis-generate.js.map +1 -1
- package/dist/usecases/script/transition.js.map +1 -1
- package/dist/usecases/script/type.js.map +1 -1
- package/dist/usecases/script/worldview.js.map +1 -1
- package/package.json +22 -17
- package/skills/scriptctl/SKILL.md +140 -120
- package/skills/scriptctl/references/atomic-write-workflow.md +12 -9
- package/skills/scriptctl/references/ingest-workflow.md +85 -0
- package/skills/scriptctl/references/state-reference-repair.md +26 -39
- package/dist/domain/direct/runner.d.ts +0 -25
- package/dist/domain/direct/runner.js +0 -88
- package/dist/domain/direct/runner.js.map +0 -1
- package/dist/domain/direct/stage.d.ts +0 -108
- package/dist/domain/direct/stage.js +0 -134
- package/dist/domain/direct/stage.js.map +0 -1
- package/dist/domain/direct/stages/asset-curation.d.ts +0 -2
- package/dist/domain/direct/stages/asset-curation.js +0 -10
- package/dist/domain/direct/stages/asset-curation.js.map +0 -1
- package/dist/domain/direct/stages/batch-extract.d.ts +0 -2
- package/dist/domain/direct/stages/batch-extract.js +0 -12
- package/dist/domain/direct/stages/batch-extract.js.map +0 -1
- package/dist/domain/direct/stages/batch-plan.d.ts +0 -2
- package/dist/domain/direct/stages/batch-plan.js +0 -9
- package/dist/domain/direct/stages/batch-plan.js.map +0 -1
- package/dist/domain/direct/stages/episode-merge.d.ts +0 -2
- package/dist/domain/direct/stages/episode-merge.js +0 -11
- package/dist/domain/direct/stages/episode-merge.js.map +0 -1
- package/dist/domain/direct/stages/episode-plan.d.ts +0 -2
- package/dist/domain/direct/stages/episode-plan.js +0 -9
- package/dist/domain/direct/stages/episode-plan.js.map +0 -1
- package/dist/domain/direct/stages/episode-synopsis.d.ts +0 -2
- package/dist/domain/direct/stages/episode-synopsis.js +0 -12
- package/dist/domain/direct/stages/episode-synopsis.js.map +0 -1
- package/dist/domain/direct/stages/episode-titles.d.ts +0 -2
- package/dist/domain/direct/stages/episode-titles.js +0 -12
- package/dist/domain/direct/stages/episode-titles.js.map +0 -1
- package/dist/domain/direct/stages/index.d.ts +0 -19
- package/dist/domain/direct/stages/index.js +0 -41
- package/dist/domain/direct/stages/index.js.map +0 -1
- package/dist/domain/direct/stages/metadata.d.ts +0 -2
- package/dist/domain/direct/stages/metadata.js +0 -11
- package/dist/domain/direct/stages/metadata.js.map +0 -1
- package/dist/domain/direct/stages/script-merge.d.ts +0 -2
- package/dist/domain/direct/stages/script-merge.js +0 -15
- package/dist/domain/direct/stages/script-merge.js.map +0 -1
- package/dist/domain/direct/stages/script-synopsis.d.ts +0 -2
- package/dist/domain/direct/stages/script-synopsis.js +0 -12
- package/dist/domain/direct/stages/script-synopsis.js.map +0 -1
- package/dist/domain/direct/stages/source-prepare.d.ts +0 -2
- package/dist/domain/direct/stages/source-prepare.js +0 -9
- package/dist/domain/direct/stages/source-prepare.js.map +0 -1
- package/dist/domain/direct/stages/state-binding.d.ts +0 -2
- package/dist/domain/direct/stages/state-binding.js +0 -9
- package/dist/domain/direct/stages/state-binding.js.map +0 -1
- package/dist/domain/direct/stages/state-curation.d.ts +0 -2
- package/dist/domain/direct/stages/state-curation.js +0 -10
- package/dist/domain/direct/stages/state-curation.js.map +0 -1
- package/dist/domain/direct/stages/validate.d.ts +0 -2
- package/dist/domain/direct/stages/validate.js +0 -10
- package/dist/domain/direct/stages/validate.js.map +0 -1
- package/dist/domain/direct-core.d.ts +0 -313
- package/dist/domain/direct-core.js +0 -8514
- package/dist/domain/direct-core.js.map +0 -1
- package/dist/domain/script/validate.d.ts +0 -22
- package/dist/domain/script/validate.js +0 -997
- package/dist/domain/script/validate.js.map +0 -1
- package/dist/infra/providers.d.ts +0 -97
- package/dist/infra/providers.js +0 -1396
- package/dist/infra/providers.js.map +0 -1
- package/dist/usecases/direct.d.ts +0 -27
- package/dist/usecases/direct.js +0 -4172
- package/dist/usecases/direct.js.map +0 -1
- package/dist/usecases/parse.d.ts +0 -15
- package/dist/usecases/parse.js +0 -429
- package/dist/usecases/parse.js.map +0 -1
- package/dist/usecases/script/add-speaker.js +0 -22
- package/dist/usecases/script/add-speaker.js.map +0 -1
- package/dist/usecases/script/export.d.ts +0 -3
- package/dist/usecases/script/export.js +0 -111
- package/dist/usecases/script/export.js.map +0 -1
- package/dist/usecases/script/overlap.d.ts +0 -3
- package/dist/usecases/script/overlap.js +0 -21
- package/dist/usecases/script/overlap.js.map +0 -1
- package/dist/usecases/script/speakers.d.ts +0 -3
- package/dist/usecases/script/speakers.js +0 -28
- package/dist/usecases/script/speakers.js.map +0 -1
- package/dist/usecases/script/state-change.d.ts +0 -3
- package/dist/usecases/script/state-change.js +0 -24
- package/dist/usecases/script/state-change.js.map +0 -1
- package/skills/scriptctl/references/direct-workflow.md +0 -72
package/dist/infra/providers.js
DELETED
|
@@ -1,1396 +0,0 @@
|
|
|
1
|
-
import Anthropic from "@anthropic-ai/sdk";
|
|
2
|
-
import { CliError, DEFAULT_BATCH_MAX_TOKENS, DEFAULT_MAX_TOKENS, DEFAULT_THINKING_BUDGET_TOKENS, EPISODE_SYNOPSIS_SCHEMA, EPISODE_TITLE_EXTRACTION_SCHEMA, EXIT_RUNTIME, EXIT_USAGE, DEFAULT_METADATA_ASSET_CHUNK, MARKDOWN_BATCH_PROMPT_SPEC, METADATA_DESC_MD_SPEC, METADATA_GLOBAL_MD_SPEC, SCRIPT_SYNOPSIS_SCHEMA, STATE_BINDING_SCHEMA, } from "../common.js";
|
|
3
|
-
import { ASSET_GROUP_MAX_ITEMS, _md_push_asset, _normalize_speaker_list, assembleMetadataFromParts, cleanSynopsisList, buildEpisodeTitleContext, buildMetadataContext, deterministicEpisodeShortTitle, deterministicExtractEpisode, deterministicExtractMetadata, episodesNeedingGeneratedTitles, formatBatchSource, formatAssetCurationDecisionExpressions, formatStateCurationDecisionExpressions, isRetryableProviderError, parseAssetDescriptionsMd, parseGlobalMetadataMd, parseMarkdownBatch, providerAttempts, } from "../domain/direct-core.js";
|
|
4
|
-
const DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS = 240_000;
|
|
5
|
-
function providerRequestTimeoutMs() {
|
|
6
|
-
const raw = (process.env.SCRIPTCTL_PROVIDER_REQUEST_TIMEOUT_MS ?? "").trim();
|
|
7
|
-
if (!raw)
|
|
8
|
-
return DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS;
|
|
9
|
-
const parsed = Number.parseInt(raw, 10);
|
|
10
|
-
if (!Number.isFinite(parsed))
|
|
11
|
-
return DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS;
|
|
12
|
-
return Math.max(30_000, parsed);
|
|
13
|
-
}
|
|
14
|
-
function providerRequestOptions() {
|
|
15
|
-
return { timeout: providerRequestTimeoutMs() };
|
|
16
|
-
}
|
|
17
|
-
const PROVIDER_CAPABILITIES = {
|
|
18
|
-
mock: {
|
|
19
|
-
completeText: true,
|
|
20
|
-
extractBatch: true,
|
|
21
|
-
extractEpisode: true,
|
|
22
|
-
extractMetadata: true,
|
|
23
|
-
groupAssets: true,
|
|
24
|
-
curateAssets: true,
|
|
25
|
-
curateStates: true,
|
|
26
|
-
bindStates: true,
|
|
27
|
-
extractEpisodeTitles: true,
|
|
28
|
-
extractEpisodeSynopsis: true,
|
|
29
|
-
extractScriptSynopsis: true,
|
|
30
|
-
},
|
|
31
|
-
anthropic: {
|
|
32
|
-
completeText: true,
|
|
33
|
-
extractBatch: true,
|
|
34
|
-
extractEpisode: true,
|
|
35
|
-
extractMetadata: true,
|
|
36
|
-
groupAssets: true,
|
|
37
|
-
curateAssets: true,
|
|
38
|
-
curateStates: true,
|
|
39
|
-
bindStates: true,
|
|
40
|
-
extractEpisodeTitles: true,
|
|
41
|
-
extractEpisodeSynopsis: true,
|
|
42
|
-
extractScriptSynopsis: true,
|
|
43
|
-
},
|
|
44
|
-
openai: {
|
|
45
|
-
completeText: true,
|
|
46
|
-
extractBatch: false,
|
|
47
|
-
extractEpisode: false,
|
|
48
|
-
extractMetadata: false,
|
|
49
|
-
groupAssets: false,
|
|
50
|
-
curateAssets: false,
|
|
51
|
-
curateStates: false,
|
|
52
|
-
bindStates: false,
|
|
53
|
-
extractEpisodeTitles: false,
|
|
54
|
-
extractEpisodeSynopsis: false,
|
|
55
|
-
extractScriptSynopsis: false,
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
export function providerCapabilities(name) {
|
|
59
|
-
return PROVIDER_CAPABILITIES[name] ?? null;
|
|
60
|
-
}
|
|
61
|
-
function strOf(v) {
|
|
62
|
-
if (v === null || v === undefined)
|
|
63
|
-
return "";
|
|
64
|
-
return String(v);
|
|
65
|
-
}
|
|
66
|
-
function isList(v) {
|
|
67
|
-
return Array.isArray(v);
|
|
68
|
-
}
|
|
69
|
-
function asList(v) {
|
|
70
|
-
return Array.isArray(v) ? v : [];
|
|
71
|
-
}
|
|
72
|
-
function isDict(v) {
|
|
73
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
74
|
-
}
|
|
75
|
-
function sleepMs(ms) {
|
|
76
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
77
|
-
}
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
// Mock provider
|
|
80
|
-
// ---------------------------------------------------------------------------
|
|
81
|
-
export class MockProvider {
|
|
82
|
-
name = "mock";
|
|
83
|
-
completeText(request) {
|
|
84
|
-
const system = request.system;
|
|
85
|
-
const user = request.user;
|
|
86
|
-
let text = "";
|
|
87
|
-
if (system.includes("## 片段梗概")) {
|
|
88
|
-
const body = firstNonEmptySourceLine(user);
|
|
89
|
-
text = [
|
|
90
|
-
"## 片段梗概",
|
|
91
|
-
`片段梗概:${body}`,
|
|
92
|
-
"",
|
|
93
|
-
"## 关键事件",
|
|
94
|
-
`- ${body}`,
|
|
95
|
-
"",
|
|
96
|
-
"## 人物变化",
|
|
97
|
-
"- 陈默 | 继续推进当前剧情",
|
|
98
|
-
"",
|
|
99
|
-
"## 结尾钩子",
|
|
100
|
-
body,
|
|
101
|
-
].join("\n");
|
|
102
|
-
}
|
|
103
|
-
else if (system.includes("## 分集梗概")) {
|
|
104
|
-
const body = firstNonEmptySourceLine(user);
|
|
105
|
-
text = [
|
|
106
|
-
"## 分集梗概",
|
|
107
|
-
`分集梗概:${body}`,
|
|
108
|
-
"",
|
|
109
|
-
"## 单句梗概",
|
|
110
|
-
body,
|
|
111
|
-
"",
|
|
112
|
-
"## 关键事件",
|
|
113
|
-
`- ${body}`,
|
|
114
|
-
].join("\n");
|
|
115
|
-
}
|
|
116
|
-
else if (system.includes("## 阶段梗概")) {
|
|
117
|
-
const body = firstNonEmptySourceLine(user);
|
|
118
|
-
text = [
|
|
119
|
-
"## 阶段梗概",
|
|
120
|
-
`阶段梗概:${body}`,
|
|
121
|
-
"",
|
|
122
|
-
"## 关键转折",
|
|
123
|
-
`- ${body}`,
|
|
124
|
-
"",
|
|
125
|
-
"## 未解决悬念",
|
|
126
|
-
"- ",
|
|
127
|
-
].join("\n");
|
|
128
|
-
}
|
|
129
|
-
else if (system.includes("## 全文梗概")) {
|
|
130
|
-
const body = firstNonEmptySourceLine(user);
|
|
131
|
-
text = [
|
|
132
|
-
"## 全文梗概",
|
|
133
|
-
`全文梗概:${body}`,
|
|
134
|
-
"",
|
|
135
|
-
"## 主题",
|
|
136
|
-
"mock-theme",
|
|
137
|
-
"",
|
|
138
|
-
"## 一句话",
|
|
139
|
-
"mock-logline",
|
|
140
|
-
"",
|
|
141
|
-
"## 风格",
|
|
142
|
-
"mock-style",
|
|
143
|
-
"",
|
|
144
|
-
"## 主要人物",
|
|
145
|
-
"- 陈默 | 主角",
|
|
146
|
-
].join("\n");
|
|
147
|
-
}
|
|
148
|
-
else {
|
|
149
|
-
text = firstNonEmptySourceLine(user);
|
|
150
|
-
}
|
|
151
|
-
return { text, raw: { provider: "mock", system, user } };
|
|
152
|
-
}
|
|
153
|
-
extractEpisode(sourceText, episodePlan) {
|
|
154
|
-
return this.extractBatch(sourceText, episodePlan);
|
|
155
|
-
}
|
|
156
|
-
extractBatch(sourceText, batchPlan) {
|
|
157
|
-
const result = deterministicExtractEpisode(sourceText, batchPlan);
|
|
158
|
-
const actors = [];
|
|
159
|
-
const locations = [];
|
|
160
|
-
const props = [];
|
|
161
|
-
const speakers = [];
|
|
162
|
-
for (const scene of asList(result["scenes"])) {
|
|
163
|
-
for (const name of asList(scene["actor_names"])) {
|
|
164
|
-
_md_push_asset(actors, strOf(name).trim(), null);
|
|
165
|
-
}
|
|
166
|
-
const loc = strOf(scene["location_name"]).trim();
|
|
167
|
-
if (loc && loc !== "未知场景")
|
|
168
|
-
_md_push_asset(locations, loc, null);
|
|
169
|
-
for (const prop of asList(scene["prop_names"])) {
|
|
170
|
-
_md_push_asset(props, strOf(prop).trim(), null);
|
|
171
|
-
}
|
|
172
|
-
for (const action of asList(scene["actions"])) {
|
|
173
|
-
const speaker = strOf(action["speaker"]).trim();
|
|
174
|
-
const speakerKind = strOf(action["speaker_kind"]).trim();
|
|
175
|
-
if (speaker && speakerKind && speakerKind !== "actor") {
|
|
176
|
-
speakers.push({ name: speaker, source_kind: speakerKind, description: null });
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
result["actors"] = actors;
|
|
181
|
-
result["locations"] = locations;
|
|
182
|
-
result["props"] = props;
|
|
183
|
-
result["speakers"] = _normalize_speaker_list(speakers);
|
|
184
|
-
// Deterministic fragment synopsis so the mock-flow e2e can verify the whole
|
|
185
|
-
// synopsis chain end-to-end: take the first non-empty action content.
|
|
186
|
-
const firstContent = asList(result["scenes"])
|
|
187
|
-
.flatMap((scene) => asList(scene["actions"]))
|
|
188
|
-
.map((action) => strOf(action["content"]).trim())
|
|
189
|
-
.find((c) => c.length > 0);
|
|
190
|
-
if (firstContent)
|
|
191
|
-
result["synopsis"] = firstContent.slice(0, 40);
|
|
192
|
-
return result;
|
|
193
|
-
}
|
|
194
|
-
extractMetadata(_sourceText, script) {
|
|
195
|
-
return deterministicExtractMetadata(script);
|
|
196
|
-
}
|
|
197
|
-
groupAssets(_script, contextText) {
|
|
198
|
-
const kind = strOf(/^kind:\s*(actor|location|prop)$/m.exec(contextText)?.[1]);
|
|
199
|
-
const rows = contextText
|
|
200
|
-
.split("\n")
|
|
201
|
-
.map((line) => line.trim())
|
|
202
|
-
.filter((line) => /^(?:A|L|P)\s+\|/.test(line))
|
|
203
|
-
.map((line) => {
|
|
204
|
-
const parts = line.split("|").map((part) => part.trim());
|
|
205
|
-
return { id: parts[1] ?? "", name: parts[2] ?? "" };
|
|
206
|
-
})
|
|
207
|
-
.filter((row) => row.id && row.name);
|
|
208
|
-
const seenNames = new Set();
|
|
209
|
-
const keptIds = [];
|
|
210
|
-
for (const row of rows) {
|
|
211
|
-
const key = row.name.replace(/[\s_\-–—·.,,、::;;'"“”‘’()[\]{}()【】《》<>]+/g, "").toLowerCase();
|
|
212
|
-
if (seenNames.has(key))
|
|
213
|
-
continue;
|
|
214
|
-
seenNames.add(key);
|
|
215
|
-
keptIds.push(row.id);
|
|
216
|
-
}
|
|
217
|
-
const expressions = [];
|
|
218
|
-
for (let index = 0; index < keptIds.length; index += ASSET_GROUP_MAX_ITEMS) {
|
|
219
|
-
expressions.push(`<group>${kind} ${keptIds.slice(index, index + ASSET_GROUP_MAX_ITEMS).join(" ")} # mock name-order group</group>`);
|
|
220
|
-
}
|
|
221
|
-
return expressions.length > 0 ? expressions.join("\n") : `<group>${kind} __empty__ # mock empty group</group>`;
|
|
222
|
-
}
|
|
223
|
-
curateAssets(script, contextText) {
|
|
224
|
-
if (contextText.includes("# Asset Curation Audit Context") || contextText.includes("# Location Canonicalization Audit Context")) {
|
|
225
|
-
return "<exp>NOOP # no asset curation changes</exp>";
|
|
226
|
-
}
|
|
227
|
-
const decisions = [];
|
|
228
|
-
const actors = asList(script["actors"]).filter(isDict);
|
|
229
|
-
const locations = asList(script["locations"]).filter(isDict);
|
|
230
|
-
const props = asList(script["props"]).filter(isDict);
|
|
231
|
-
const actorByName = new Map();
|
|
232
|
-
for (const actor of actors) {
|
|
233
|
-
const name = mockAssetName(actor, "actor");
|
|
234
|
-
if (name)
|
|
235
|
-
actorByName.set(name, actor);
|
|
236
|
-
}
|
|
237
|
-
for (const actor of actors) {
|
|
238
|
-
const id = mockAssetId(actor, "actor");
|
|
239
|
-
const name = mockAssetName(actor, "actor");
|
|
240
|
-
if (!id)
|
|
241
|
-
continue;
|
|
242
|
-
const variant = actorVisualVariant(name);
|
|
243
|
-
const base = variant ? actorByName.get(variant.baseName) : null;
|
|
244
|
-
if (variant && base && mockAssetId(base, "actor") !== id) {
|
|
245
|
-
decisions.push({
|
|
246
|
-
kind: "actor",
|
|
247
|
-
source_id: id,
|
|
248
|
-
decision: "move_to_state",
|
|
249
|
-
target_kind: "actor",
|
|
250
|
-
target_id: mockAssetId(base, "actor"),
|
|
251
|
-
state_name: variant.stateName,
|
|
252
|
-
reason: "mock: visual form variant belongs to the base actor state catalog.",
|
|
253
|
-
});
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
256
|
-
if (looksLikeGroupSpeaker(name)) {
|
|
257
|
-
decisions.push({
|
|
258
|
-
kind: "actor",
|
|
259
|
-
source_id: id,
|
|
260
|
-
decision: "move_to_speaker",
|
|
261
|
-
speaker_kind: "group",
|
|
262
|
-
new_name: name,
|
|
263
|
-
reason: "mock: crowd/extra label is a speaker source, not a reusable actor asset.",
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
else {
|
|
267
|
-
decisions.push({
|
|
268
|
-
kind: "actor",
|
|
269
|
-
source_id: id,
|
|
270
|
-
decision: "keep",
|
|
271
|
-
reason: "mock: named actor remains an actor asset.",
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
for (const loc of locations) {
|
|
276
|
-
const id = mockAssetId(loc, "location");
|
|
277
|
-
const name = mockAssetName(loc, "location");
|
|
278
|
-
if (!id)
|
|
279
|
-
continue;
|
|
280
|
-
const target = locations.find((candidate) => {
|
|
281
|
-
const candidateId = mockAssetId(candidate, "location");
|
|
282
|
-
const candidateName = mockAssetName(candidate, "location");
|
|
283
|
-
return Boolean(candidateId && candidateId !== id && candidateName && candidateName.length < name.length && name.includes(candidateName) && isMicroLocationName(name));
|
|
284
|
-
});
|
|
285
|
-
if (target) {
|
|
286
|
-
decisions.push({
|
|
287
|
-
kind: "location",
|
|
288
|
-
source_id: id,
|
|
289
|
-
decision: "merge",
|
|
290
|
-
target_kind: "location",
|
|
291
|
-
target_id: mockAssetId(target, "location"),
|
|
292
|
-
reason: "mock: micro-location folds into the stable parent location.",
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
else {
|
|
296
|
-
decisions.push({
|
|
297
|
-
kind: "location",
|
|
298
|
-
source_id: id,
|
|
299
|
-
decision: "keep",
|
|
300
|
-
reason: "mock: stable location remains a production set.",
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
for (const prop of props) {
|
|
305
|
-
const id = mockAssetId(prop, "prop");
|
|
306
|
-
const name = mockAssetName(prop, "prop");
|
|
307
|
-
if (!id)
|
|
308
|
-
continue;
|
|
309
|
-
if (isDisposableOrUiProp(name)) {
|
|
310
|
-
decisions.push({
|
|
311
|
-
kind: "prop",
|
|
312
|
-
source_id: id,
|
|
313
|
-
decision: "drop",
|
|
314
|
-
reason: "mock: disposable UI/screen detail stays in action text.",
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
else {
|
|
318
|
-
decisions.push({
|
|
319
|
-
kind: "prop",
|
|
320
|
-
source_id: id,
|
|
321
|
-
decision: "keep",
|
|
322
|
-
reason: "category=signature_instance; mock: hero prop remains explicit.",
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
return formatAssetCurationDecisionExpressions({ decisions });
|
|
327
|
-
}
|
|
328
|
-
curateStates(_script, contextText) {
|
|
329
|
-
return formatStateCurationDecisionExpressions(mockStateCurationPlan(contextText));
|
|
330
|
-
}
|
|
331
|
-
bindStates(_script, context) {
|
|
332
|
-
const scenes = [];
|
|
333
|
-
for (const scene of asList(context["scenes"])) {
|
|
334
|
-
if (!isDict(scene))
|
|
335
|
-
continue;
|
|
336
|
-
const actions = asList(scene["actions"]);
|
|
337
|
-
const actionText = actions.map((action) => strOf(action["content"])).join("\n");
|
|
338
|
-
const actors = mockBindingRefs(scene, "actors", "actor_id", actionText);
|
|
339
|
-
const locations = mockBindingRefs(scene, "locations", "location_id", actionText);
|
|
340
|
-
const props = mockBindingRefs(scene, "props", "prop_id", actionText);
|
|
341
|
-
scenes.push({
|
|
342
|
-
scene_id: scene["scene_id"],
|
|
343
|
-
actors,
|
|
344
|
-
locations,
|
|
345
|
-
props,
|
|
346
|
-
state_changes: mockStateChanges(scene, actions),
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
return { scenes };
|
|
350
|
-
}
|
|
351
|
-
extractEpisodeSynopsis(synopses, _ctx) {
|
|
352
|
-
// Prefix marks that this deterministic STITCH ran — lets e2e tests tell a
|
|
353
|
-
// real reduce-step call apart from the plain-join fallback (which has no
|
|
354
|
-
// prefix) and prove the multi-group reduce path actually executed.
|
|
355
|
-
return { synopsis: `stitch:${cleanSynopsisList(synopses).join(" ")}` };
|
|
356
|
-
}
|
|
357
|
-
extractScriptSynopsis(synopses, _ctx) {
|
|
358
|
-
const joined = cleanSynopsisList(synopses).join(" ");
|
|
359
|
-
return {
|
|
360
|
-
synopsis: joined,
|
|
361
|
-
theme: "mock-theme",
|
|
362
|
-
logline: "mock-logline",
|
|
363
|
-
style: "mock-style",
|
|
364
|
-
main_characters: [],
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
extractEpisodeTitles(sourceText, episodePlan) {
|
|
368
|
-
return {
|
|
369
|
-
episode_titles: episodesNeedingGeneratedTitles(episodePlan).map((ep) => ({
|
|
370
|
-
episode: Number(ep["episode"] ?? 0),
|
|
371
|
-
title: deterministicEpisodeShortTitle(sourceText, ep),
|
|
372
|
-
})),
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
// ---------------------------------------------------------------------------
|
|
377
|
-
// Anthropic provider
|
|
378
|
-
// ---------------------------------------------------------------------------
|
|
379
|
-
export class AnthropicProvider {
|
|
380
|
-
name = "anthropic";
|
|
381
|
-
client;
|
|
382
|
-
model;
|
|
383
|
-
constructor(model) {
|
|
384
|
-
const apiKey = (process.env.PROJECT_LITELLM_GATEWAY_API_KEY ?? "").trim();
|
|
385
|
-
if (!apiKey) {
|
|
386
|
-
throw new CliError("INIT FAILED: provider not configured", "Provider credentials are not configured for this environment.", {
|
|
387
|
-
exitCode: EXIT_RUNTIME,
|
|
388
|
-
required: ["PROJECT_LITELLM_GATEWAY_API_KEY"],
|
|
389
|
-
received: ["no API key environment variable"],
|
|
390
|
-
nextSteps: ["Run `scriptctl doctor` to identify missing configuration."],
|
|
391
|
-
errorCode: "PROVIDER_AUTH_MISSING",
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
const baseUrl = (process.env.PROJECT_LITELLM_GATEWAY_BASE_URL ?? "").trim();
|
|
395
|
-
const opts = { apiKey };
|
|
396
|
-
if (baseUrl)
|
|
397
|
-
opts.baseURL = baseUrl;
|
|
398
|
-
this.client = new Anthropic(opts);
|
|
399
|
-
this.model = model;
|
|
400
|
-
}
|
|
401
|
-
messageRequest(prompt, maxTokens) {
|
|
402
|
-
const rawMaxTokens = (process.env.SCRIPTCTL_MAX_TOKENS ?? "").trim();
|
|
403
|
-
let configuredMaxTokens = DEFAULT_MAX_TOKENS;
|
|
404
|
-
if (rawMaxTokens) {
|
|
405
|
-
const parsed = parseInt(rawMaxTokens, 10);
|
|
406
|
-
if (!Number.isNaN(parsed))
|
|
407
|
-
configuredMaxTokens = parsed;
|
|
408
|
-
}
|
|
409
|
-
let tokenBudget = maxTokens !== undefined ? maxTokens : configuredMaxTokens;
|
|
410
|
-
tokenBudget = Math.max(1024, Math.min(tokenBudget, configuredMaxTokens, DEFAULT_MAX_TOKENS));
|
|
411
|
-
const request = {
|
|
412
|
-
model: this.model,
|
|
413
|
-
max_tokens: tokenBudget,
|
|
414
|
-
messages: [{ role: "user", content: prompt }],
|
|
415
|
-
};
|
|
416
|
-
if (this.model.endsWith("-think")) {
|
|
417
|
-
const rawBudget = (process.env.SCRIPTCTL_THINKING_BUDGET_TOKENS ?? "").trim();
|
|
418
|
-
let budget = DEFAULT_THINKING_BUDGET_TOKENS;
|
|
419
|
-
if (rawBudget) {
|
|
420
|
-
const parsed = parseInt(rawBudget, 10);
|
|
421
|
-
if (!Number.isNaN(parsed))
|
|
422
|
-
budget = parsed;
|
|
423
|
-
}
|
|
424
|
-
request.thinking = { type: "enabled", budget_tokens: Math.max(1024, Math.min(budget, tokenBudget - 1000)) };
|
|
425
|
-
}
|
|
426
|
-
return request;
|
|
427
|
-
}
|
|
428
|
-
textMessageRequest(system, user, maxTokens) {
|
|
429
|
-
const request = this.messageRequest(user, maxTokens);
|
|
430
|
-
return system.trim() ? { ...request, system } : request;
|
|
431
|
-
}
|
|
432
|
-
async collectResponseText(request) {
|
|
433
|
-
// Non-streaming: the LiteLLM gateway drops idle streaming connections
|
|
434
|
-
// ("Premature close"), so every extraction uses the create path and is sized
|
|
435
|
-
// to fit a single response (see collectToolUseInput for the same policy).
|
|
436
|
-
const response = await this.client.messages.create(request, providerRequestOptions());
|
|
437
|
-
return [textFromResponse(response), strOf(response.stop_reason)];
|
|
438
|
-
}
|
|
439
|
-
/**
|
|
440
|
-
* Run a request that forces a specific tool call via `tool_choice`, then return that
|
|
441
|
-
* tool's `input` dict (already parsed by the SDK — no JSON.parse needed). Returns
|
|
442
|
-
* `null` when the provider didn't emit the expected tool_use block (e.g. refusal,
|
|
443
|
-
* truncated before tool call, gateway dropped tools field), letting the caller
|
|
444
|
-
* raise a method-specific CliError.
|
|
445
|
-
*/
|
|
446
|
-
async collectToolUseInput(request, toolName) {
|
|
447
|
-
// Always non-streaming. The LiteLLM gateway drops idle streaming connections
|
|
448
|
-
// ("Premature close"), and every extraction is sized to fit a single create
|
|
449
|
-
// response. (A request over NONSTREAMING_MAX_TOKENS used to stream; that path
|
|
450
|
-
// was the source of the gateway drops and is intentionally gone — keep all
|
|
451
|
-
// tool-use callers below that ceiling.)
|
|
452
|
-
const response = (await this.client.messages.create(request, providerRequestOptions()));
|
|
453
|
-
return [findToolUseInput(response.content ?? [], toolName), strOf(response.stop_reason)];
|
|
454
|
-
}
|
|
455
|
-
/**
|
|
456
|
-
* Shared forced-tool-use round-trip: build the request with a single forced
|
|
457
|
-
* tool, run it, and map the two failure modes (max_tokens truncation, missing
|
|
458
|
-
* tool_use block) to method-specific CliErrors. Every tool_use extraction
|
|
459
|
-
* (titles / curation / metadata / episode-synopsis / script-synopsis) routes
|
|
460
|
-
* through here so the protocol and error contract live in one place.
|
|
461
|
-
*/
|
|
462
|
-
async runToolUse(spec) {
|
|
463
|
-
const request = { ...this.messageRequest(spec.prompt, spec.maxTokens) };
|
|
464
|
-
request["tools"] = [{ name: spec.toolName, description: spec.description, input_schema: spec.schema }];
|
|
465
|
-
request["tool_choice"] = { type: "tool", name: spec.toolName };
|
|
466
|
-
const [input, stopReason] = await this.collectToolUseInput(request, spec.toolName);
|
|
467
|
-
if (stopReason === "max_tokens") {
|
|
468
|
-
throw new CliError(spec.truncatedTitle, `${spec.truncatedTitle}.`, {
|
|
469
|
-
exitCode: EXIT_RUNTIME,
|
|
470
|
-
required: ["complete JSON within provider max_tokens"],
|
|
471
|
-
received: [`stop_reason: ${stopReason}`, `max_tokens: ${request["max_tokens"]}`],
|
|
472
|
-
nextSteps: spec.nextSteps,
|
|
473
|
-
});
|
|
474
|
-
}
|
|
475
|
-
if (!input) {
|
|
476
|
-
throw new CliError(spec.invalidTitle, `${spec.invalidTitle}.`, {
|
|
477
|
-
exitCode: EXIT_RUNTIME,
|
|
478
|
-
required: [`tool_use block with name "${spec.toolName}"`],
|
|
479
|
-
received: [`stop_reason: ${stopReason || "unknown"}`, `missing tool_use block: ${spec.toolName}`],
|
|
480
|
-
nextSteps: spec.nextSteps,
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
return input;
|
|
484
|
-
}
|
|
485
|
-
extractEpisode(sourceText, episodePlan) {
|
|
486
|
-
return this.extractBatch(sourceText, episodePlan);
|
|
487
|
-
}
|
|
488
|
-
async completeText(request) {
|
|
489
|
-
const msg = this.textMessageRequest(request.system, request.user, DEFAULT_BATCH_MAX_TOKENS);
|
|
490
|
-
const [raw, stopReason] = await this.collectResponseTextWithRetry(msg);
|
|
491
|
-
if (stopReason === "max_tokens") {
|
|
492
|
-
throw new CliError("LLM FAILED: Provider output truncated", "Provider output truncated.", {
|
|
493
|
-
exitCode: EXIT_RUNTIME,
|
|
494
|
-
required: ["complete text within provider max_tokens"],
|
|
495
|
-
received: [`stop_reason: ${stopReason}`, `max_tokens: ${msg.max_tokens}`, `raw chars: ${raw.length}`, `tail: ${raw.slice(-160) || "<empty response>"}`],
|
|
496
|
-
nextSteps: ["Rerun the command; completed checkpoints will be reused. If this repeats, lower the input chunk size."],
|
|
497
|
-
errorCode: "PROVIDER_OUTPUT_TRUNCATED",
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
if (!raw.trim()) {
|
|
501
|
-
throw new CliError("LLM FAILED: Provider returned empty text", "Provider returned an empty response.", {
|
|
502
|
-
exitCode: EXIT_RUNTIME,
|
|
503
|
-
required: ["non-empty text response"],
|
|
504
|
-
received: [`stop_reason: ${stopReason || "unknown"}`, "raw chars: 0"],
|
|
505
|
-
nextSteps: ["Rerun with a smaller chunk or adjust the prompt/spec."],
|
|
506
|
-
errorCode: "PROVIDER_EMPTY_RESPONSE",
|
|
507
|
-
});
|
|
508
|
-
}
|
|
509
|
-
return { text: raw, raw: { stop_reason: stopReason } };
|
|
510
|
-
}
|
|
511
|
-
async extractBatch(sourceText, batchPlan) {
|
|
512
|
-
const context = isList(batchPlan["context"]) || typeof batchPlan["context"] === "object" && batchPlan["context"] !== null
|
|
513
|
-
? batchPlan["context"]
|
|
514
|
-
: {};
|
|
515
|
-
const numberedSource = formatBatchSource(sourceText, batchPlan);
|
|
516
|
-
const prompt = "You convert one batch from an existing script into the documented markdown template.\n" +
|
|
517
|
-
"\n" +
|
|
518
|
-
`${MARKDOWN_BATCH_PROMPT_SPEC}\n` +
|
|
519
|
-
"\n" +
|
|
520
|
-
"Batch plan:\n" +
|
|
521
|
-
`${JSON.stringify(batchPlan)}\n` +
|
|
522
|
-
"\n" +
|
|
523
|
-
"Read-only context:\n" +
|
|
524
|
-
`${JSON.stringify(context)}\n` +
|
|
525
|
-
"\n" +
|
|
526
|
-
"Batch Source:\n" +
|
|
527
|
-
`${numberedSource}\n`;
|
|
528
|
-
const request = this.messageRequest(prompt, DEFAULT_BATCH_MAX_TOKENS);
|
|
529
|
-
const [raw, stopReason] = await this.collectResponseText(request);
|
|
530
|
-
if (stopReason === "max_tokens") {
|
|
531
|
-
throw new CliError("INIT FAILED: Provider output truncated", "Provider output truncated.", {
|
|
532
|
-
exitCode: EXIT_RUNTIME,
|
|
533
|
-
required: ["complete markdown within provider max_tokens"],
|
|
534
|
-
received: [
|
|
535
|
-
`stop_reason: ${stopReason}`,
|
|
536
|
-
`max_tokens: ${request.max_tokens}`,
|
|
537
|
-
`raw chars: ${raw.length}`,
|
|
538
|
-
`tail: ${raw.slice(-160) || "<empty response>"}`,
|
|
539
|
-
],
|
|
540
|
-
nextSteps: ["Rerun init; completed batch checkpoints will be reused. If this repeats, split the source episode."],
|
|
541
|
-
errorCode: "PROVIDER_OUTPUT_TRUNCATED",
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
if (!raw.trim()) {
|
|
545
|
-
// Empty body with a non-max_tokens stop reason is how an Anthropic content
|
|
546
|
-
// filter / refusal surfaces: the model emits no markdown at all. Mark it
|
|
547
|
-
// distinctly so init can classify it as terminal (retrying the same source
|
|
548
|
-
// will deterministically refuse again) instead of an open-ended failure.
|
|
549
|
-
throw new CliError("INIT FAILED: Provider refused content", "Provider returned an empty response (content filter / refusal).", {
|
|
550
|
-
exitCode: EXIT_RUNTIME,
|
|
551
|
-
required: ["non-empty markdown for the batch"],
|
|
552
|
-
received: [`stop_reason: ${stopReason || "unknown"}`, "raw chars: 0"],
|
|
553
|
-
nextSteps: [
|
|
554
|
-
"This batch is blocked by the provider's content filter and will not succeed on a plain rerun.",
|
|
555
|
-
"Soften the offending source span, then re-run just this unit: `scriptctl direct init --episodes <N>` (or `--batches <key>`).",
|
|
556
|
-
],
|
|
557
|
-
errorCode: "PROVIDER_CONTENT_FILTERED",
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
const parsed = parseMarkdownBatch(raw, batchPlan);
|
|
561
|
-
parsed["_raw_markdown"] = raw;
|
|
562
|
-
return parsed;
|
|
563
|
-
}
|
|
564
|
-
async extractEpisodeTitles(sourceText, episodePlan) {
|
|
565
|
-
const context = buildEpisodeTitleContext(sourceText, episodePlan);
|
|
566
|
-
if (context.length === 0)
|
|
567
|
-
return { episode_titles: [] };
|
|
568
|
-
const prompt = "You generate missing episode titles for a direct script conversion.\n" +
|
|
569
|
-
"Use only each episode excerpt below. Do not invent plot events outside it.\n" +
|
|
570
|
-
"Return JSON matching the configured schema.\n" +
|
|
571
|
-
"\n" +
|
|
572
|
-
"Requirements:\n" +
|
|
573
|
-
"- Return one item for every input episode.\n" +
|
|
574
|
-
"- The title field must be a short Chinese episode title, 2-8 Chinese characters when possible.\n" +
|
|
575
|
-
"- Do not include episode numbers, `ep_###`, `Episode ###`, punctuation wrappers, or quotes.\n" +
|
|
576
|
-
"- Prefer concrete plot hooks, conflicts, reversals, or memorable images from the episode.\n" +
|
|
577
|
-
"\n" +
|
|
578
|
-
"Episodes needing titles:\n" +
|
|
579
|
-
`${JSON.stringify(context)}\n`;
|
|
580
|
-
return this.runToolUse({
|
|
581
|
-
prompt,
|
|
582
|
-
maxTokens: 4096,
|
|
583
|
-
toolName: "submit_episode_titles",
|
|
584
|
-
description: "Return generated short Chinese titles for every input episode.",
|
|
585
|
-
schema: EPISODE_TITLE_EXTRACTION_SCHEMA,
|
|
586
|
-
truncatedTitle: "INIT FAILED: Episode title output truncated",
|
|
587
|
-
invalidTitle: "INIT FAILED: Provider returned invalid episode title JSON",
|
|
588
|
-
nextSteps: ["Rerun init; title generation will retry before batch extraction."],
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
async groupAssets(_script, contextText) {
|
|
592
|
-
const prompt = "You plan small asset review groups for `scriptctl direct init`.\n" +
|
|
593
|
-
"You receive only asset ids and names. Do not make final keep/drop/merge/state decisions.\n" +
|
|
594
|
-
"\n" +
|
|
595
|
-
"Rules:\n" +
|
|
596
|
-
`- Output only <group>...</group> lines, with at most ${ASSET_GROUP_MAX_ITEMS} ids per group.\n` +
|
|
597
|
-
"- Group ids whose names suggest they should be reviewed together later: same identity/form, alias-like names, same place family, or same prop family.\n" +
|
|
598
|
-
"- Output only groups that should be reviewed together later; unchanged standalone assets may be omitted to save tokens.\n" +
|
|
599
|
-
"- Omit a duplicate id only when it is an obvious duplicate of a retained id with the same normalized name.\n" +
|
|
600
|
-
"- Any omitted non-duplicate id will be reviewed as its own singleton group; do not omit merely because an asset seems unimportant.\n" +
|
|
601
|
-
"- Mentioned ids must appear at most once. Do not invent ids.\n" +
|
|
602
|
-
"- No JSON, markdown, headings, bullets, or prose outside group tags.\n" +
|
|
603
|
-
"\n" +
|
|
604
|
-
"Expression grammar:\n" +
|
|
605
|
-
"- `<group><kind> <source_id> [source_id...] # reason</group>`\n" +
|
|
606
|
-
"- `<kind>` is the context kind: actor, location, or prop.\n" +
|
|
607
|
-
"\n" +
|
|
608
|
-
"Context:\n" +
|
|
609
|
-
`${contextText}\n`;
|
|
610
|
-
const request = this.messageRequest(prompt, 8000);
|
|
611
|
-
const [raw, stopReason] = await this.collectResponseTextWithRetry(request);
|
|
612
|
-
if (stopReason === "max_tokens") {
|
|
613
|
-
throw new CliError("INIT FAILED: Asset grouping output truncated", "Asset grouping output truncated.", {
|
|
614
|
-
exitCode: EXIT_RUNTIME,
|
|
615
|
-
required: ["complete asset grouping expressions within provider max_tokens"],
|
|
616
|
-
received: [`stop_reason: ${stopReason}`, `max_tokens: ${request.max_tokens}`, `raw chars: ${raw.length}`, `tail: ${raw.slice(-160) || "<empty response>"}`],
|
|
617
|
-
nextSteps: ["Rerun init; completed extraction checkpoints will be reused and asset grouping will retry."],
|
|
618
|
-
errorCode: "PROVIDER_OUTPUT_TRUNCATED",
|
|
619
|
-
});
|
|
620
|
-
}
|
|
621
|
-
if (!raw.trim()) {
|
|
622
|
-
throw new CliError("INIT FAILED: Provider returned empty asset grouping", "Provider returned an empty asset grouping response.", {
|
|
623
|
-
exitCode: EXIT_RUNTIME,
|
|
624
|
-
required: ["one or more <group> asset grouping expressions"],
|
|
625
|
-
received: [`stop_reason: ${stopReason || "unknown"}`, "raw chars: 0"],
|
|
626
|
-
nextSteps: ["Rerun init; completed extraction checkpoints will be reused and asset grouping will retry."],
|
|
627
|
-
errorCode: "PROVIDER_EMPTY_RESPONSE",
|
|
628
|
-
});
|
|
629
|
-
}
|
|
630
|
-
return raw;
|
|
631
|
-
}
|
|
632
|
-
async curateAssets(_script, contextText) {
|
|
633
|
-
const prompt = "You curate assets for `scriptctl direct init` after batch merge.\n" +
|
|
634
|
-
"Return only asset curation operation expressions. The caller will parse and apply them deterministically.\n" +
|
|
635
|
-
"\n" +
|
|
636
|
-
"Policy:\n" +
|
|
637
|
-
"- Target profile: align with a human-refined asset boundary for AI short-drama production.\n" +
|
|
638
|
-
"- Actors: keep named, speaking, visually identifiable, or plot-functional roles even when they appear only once.\n" +
|
|
639
|
-
"- Move only anonymous crowds, system/broadcast sources, or pure voice labels into speakers.\n" +
|
|
640
|
-
"- Move Q版 / 幻影 / 兽形 / 本体 / 真身 / 伪装 / alternate visual form actors into states on the base actor.\n" +
|
|
641
|
-
"- Use `identity=` hints to merge same-identity aliases/forms/titles when evidence supports it; do not keep duplicate base/form actors apart.\n" +
|
|
642
|
-
"- Merge duplicate names, aliases, typo variants, and same-identity titles into stable canonical assets.\n" +
|
|
643
|
-
"- Locations: keep visually distinct production sets; do not blindly merge interiors, doors, rooftops, underground levels, warehouses, fields, pens, sky/air variants, or other shootable sub-spaces.\n" +
|
|
644
|
-
"- Use `parent=` and `gran=` hints to MERGE non-distinct doorway/transition/nearby/overhead/angle-only micro locations into a parent; KEEP distinct shootable subspaces.\n" +
|
|
645
|
-
"- Location canonical names should preserve world/domain scope when the context distinguishes worlds or realms.\n" +
|
|
646
|
-
"- If a location co-occurs with a scope marker such as a world/realm/domain location, prefer `renameHint=` scoped canonical names via RENAME when the unscoped name would be ambiguous.\n" +
|
|
647
|
-
"- Props: use a strict refined prop boundary. Most visible or plot-mentioned objects should stay in action text or actor/location state, not standalone props.\n" +
|
|
648
|
-
"- Default prop decision is DROP unless standalone continuity is clear.\n" +
|
|
649
|
-
"- KEEP a prop when it is a named/signature physical instance whose visual identity must be reused, the same object is exchanged/carried/inspected across scenes, it carries readable evidence/rules/secrets, drives a transformation/conflict, functions as recurring character/faction equipment, or is a functional set device with continuity.\n" +
|
|
650
|
-
"- Every KEEP prop reason must start with one category: category=readable_evidence; category=container_device; category=transformation_key; category=exchanged_object; category=signature_instance; category=character_bound_core; category=signature_weapon; category=visual_symbol; or category=set_device; then cite concise evidence.\n" +
|
|
651
|
-
"- Do not KEEP merely because an object appears multiple times, is plot-functional, held/used by a character, or is UI/display content, an ordinary document/bill, food, or generic set dressing.\n" +
|
|
652
|
-
"- Move actor body marks and worn appearance into actor states; detachable named devices/weapons/tools/accessories with standalone continuity may remain props.\n" +
|
|
653
|
-
"- When a prop line has `ownerA=actor_id:name`, distinguish body/worn styling (STATE) from detachable recurring core objects (KEEP with category=character_bound_core or category=signature_weapon).\n" +
|
|
654
|
-
"- In long scripts, keep a compact prop catalog; prefer the strongest standalone hero props and DROP/STATE borderline objects.\n" +
|
|
655
|
-
"- For each duplicate group, choose one final canonical target (prefer higher scene_count, then shorter stable name); merge every other source directly into that final target.\n" +
|
|
656
|
-
"- MERGE is for same-kind duplicates. For actor/location entries that are really props, use PROP instead of cross-kind MERGE.\n" +
|
|
657
|
-
"- PROP targets must be `NEW` or an existing prop id only. Never use actor/location ids as PROP targets; if a prop is really a location/set element, DROP it or handle the location asset separately.\n" +
|
|
658
|
-
"- Never create reciprocal merges or merge cycles. Do not merge A->B and B->A, and do not create A->B->C chains when A can point to C.\n" +
|
|
659
|
-
"- Every asset line with `req=Y:*` must receive an expression, including KEEP if it should stay.\n" +
|
|
660
|
-
"- Every prop line must receive an expression. Do not omit props.\n" +
|
|
661
|
-
"- Use `required_decision_ids` as the coverage checklist before final output.\n" +
|
|
662
|
-
"- Assets with `req=-` should be omitted when unchanged; do not spend output on KEEP for req=- assets.\n" +
|
|
663
|
-
"- If output is long, prioritize every req=Y decision over any optional req=- expression.\n" +
|
|
664
|
-
"- If the context header is `# Asset Curation Audit Context`, output only corrective expressions for listed candidates, or NOOP if no correction is needed.\n" +
|
|
665
|
-
"- If the context header is `# Location Canonicalization Audit Context`, output only RENAME/MERGE location expressions for listed candidates, or NOOP if no location canonicalization is needed.\n" +
|
|
666
|
-
"- If the context header is `# Asset Curation Repair Context`, output replacement expressions only for `repair_required_source_keys`; do not repeat unrelated accepted decisions.\n" +
|
|
667
|
-
"- In audit mode, the first character of your response must be `<`. Do not write candidate reviews, headings, bullets, or prose outside expressions.\n" +
|
|
668
|
-
"- In audit mode, primary KEEP actor candidates may be corrected to SPEAKER, MERGE, or RENAME when they are anonymous crowds, pure voice labels, duplicate identities, or generic numbered roles without dialogue.\n" +
|
|
669
|
-
"- In audit mode, do not downgrade a numbered/title-like actor to SPEAKER when it has dialogue or speaking-scene evidence; keep it as an actor unless it is truly a crowd label such as 众人/宾客们/围观者.\n" +
|
|
670
|
-
"- In audit mode, primary KEEP location candidates may be corrected to RENAME or MERGE when scoped canonical naming or duplicate/parent-child granularity is wrong.\n" +
|
|
671
|
-
"- In audit mode, primary KEEP prop candidates may output KEEP only to add/replace a valid category reason; otherwise output DROP/MERGE/STATE/RENAME when KEEP is too broad.\n" +
|
|
672
|
-
"- In audit mode, primary KEEP props with `ownerA=` should become STATE only for body marks or worn appearance; keep detachable recurring core objects when evidence supports it.\n" +
|
|
673
|
-
"- In audit mode, primary DROP/MERGE/STATE prop candidates may be corrected to KEEP/RENAME/MERGE when the evidence shows standalone hero-prop continuity.\n" +
|
|
674
|
-
"- In location canonicalization mode, do not output KEEP, DROP, STATE, PROP, SPEAKER, actor, or prop operations.\n" +
|
|
675
|
-
"- In location canonicalization mode, omit unchanged candidates; KEEP is invalid and will fail validation.\n" +
|
|
676
|
-
"- In location canonicalization mode, use `scopeHint=`/`renameHint=` to RENAME ambiguous unscoped locations into scoped canonical names; use `parent=`/`gran=` to MERGE only non-distinct micro/transition/angle locations into a parent; leave distinct shootable subspaces unchanged.\n" +
|
|
677
|
-
"- For STATE/PROP/MERGE targets, use only existing target ids visible in the context line, such as `identity=`, `parent=`, `same=`, `coA=`, `coL=`, or related cluster ids. If no valid target id is visible, choose DROP instead of guessing.\n" +
|
|
678
|
-
"- Reference existing ids only. Do not invent source_id or target_id values.\n" +
|
|
679
|
-
"- For every actionable merge/drop/move/rename, give a short concrete reason from the context.\n" +
|
|
680
|
-
"- For every KEEP expression, give a short concrete reason from the context; KEEP prop reasons must begin like `category=signature_instance; ...` using one allowed category.\n" +
|
|
681
|
-
"- Keep reasons short. Prefer one precise phrase over long explanations.\n" +
|
|
682
|
-
"\n" +
|
|
683
|
-
"Output format:\n" +
|
|
684
|
-
"- Output plain text only. Do not output JSON, markdown tables, fenced code blocks, or explanations outside expressions.\n" +
|
|
685
|
-
"- Each expression must be one line wrapped exactly as `<exp>...</exp>`.\n" +
|
|
686
|
-
"- Every expression must include `# reason` at the end; the reason is required for audit.\n" +
|
|
687
|
-
"- If there are no changes, output exactly `<exp>NOOP # no asset curation changes</exp>`.\n" +
|
|
688
|
-
"\n" +
|
|
689
|
-
"Expression grammar:\n" +
|
|
690
|
-
"- `<exp>KEEP <kind> <source_id> # reason</exp>`\n" +
|
|
691
|
-
"- `<exp>MERGE <kind> <source_id> -> <target_id> # reason</exp>`\n" +
|
|
692
|
-
"- `<exp>DROP <kind> <source_id> # reason</exp>`\n" +
|
|
693
|
-
"- `<exp>RENAME <kind> <source_id> \"<new_name>\" # reason</exp>`\n" +
|
|
694
|
-
"- `<exp>STATE <kind> <source_id> -> <target_kind> <target_id> \"<state_name>\" # reason</exp>`\n" +
|
|
695
|
-
"- `<exp>SPEAKER <kind> <source_id> -> <speaker_kind> \"<display_name>\" # reason</exp>`\n" +
|
|
696
|
-
"- `<exp>PROP <kind> <source_id> -> <target_prop_id|NEW> \"<prop_name>\" # reason</exp>`\n" +
|
|
697
|
-
"- `<kind>` is one of actor, location, prop. `<target_kind>` is one of actor, location, prop.\n" +
|
|
698
|
-
"- `<speaker_kind>` is one of system, broadcast, group, other.\n" +
|
|
699
|
-
"- Use double quotes for names. Escape only `\"` and `\\\\` inside quoted strings.\n" +
|
|
700
|
-
"- Do not write natural-language operations. If an operation cannot fit this grammar, omit it.\n" +
|
|
701
|
-
"\n" +
|
|
702
|
-
"Context:\n" +
|
|
703
|
-
`${contextText}\n`;
|
|
704
|
-
const request = this.messageRequest(prompt, 20000);
|
|
705
|
-
const [raw, stopReason] = await this.collectResponseTextWithRetry(request);
|
|
706
|
-
if (stopReason === "max_tokens") {
|
|
707
|
-
throw new CliError("INIT FAILED: Asset curation output truncated", "Asset curation output truncated.", {
|
|
708
|
-
exitCode: EXIT_RUNTIME,
|
|
709
|
-
required: ["complete asset curation expressions within provider max_tokens"],
|
|
710
|
-
received: [`stop_reason: ${stopReason}`, `max_tokens: ${request.max_tokens}`, `raw chars: ${raw.length}`, `tail: ${raw.slice(-160) || "<empty response>"}`],
|
|
711
|
-
nextSteps: ["Rerun init; completed extraction checkpoints will be reused and asset curation will retry."],
|
|
712
|
-
errorCode: "PROVIDER_OUTPUT_TRUNCATED",
|
|
713
|
-
});
|
|
714
|
-
}
|
|
715
|
-
if (!raw.trim()) {
|
|
716
|
-
throw new CliError("INIT FAILED: Provider returned empty asset curation", "Provider returned an empty asset curation response.", {
|
|
717
|
-
exitCode: EXIT_RUNTIME,
|
|
718
|
-
required: ["one or more <exp> asset curation expressions"],
|
|
719
|
-
received: [`stop_reason: ${stopReason || "unknown"}`, "raw chars: 0"],
|
|
720
|
-
nextSteps: ["Rerun init; completed extraction checkpoints will be reused and asset curation will retry."],
|
|
721
|
-
errorCode: "PROVIDER_EMPTY_RESPONSE",
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
return raw;
|
|
725
|
-
}
|
|
726
|
-
async curateStates(_script, contextText) {
|
|
727
|
-
const prompt = "You curate visual state catalogs for `scriptctl direct init` after asset curation.\n" +
|
|
728
|
-
"Return only state curation operation expressions. The caller will parse, validate, and apply them deterministically.\n" +
|
|
729
|
-
"\n" +
|
|
730
|
-
"Core definition:\n" +
|
|
731
|
-
"- A state is a reusable production visual asset state. It is NOT a story condition, action, pose, emotion, camera effect, or momentary VFX intensity.\n" +
|
|
732
|
-
"- Keep a state only when the asset needs a clearly different reusable macro visual form for image/video generation.\n" +
|
|
733
|
-
"\n" +
|
|
734
|
-
"Policy:\n" +
|
|
735
|
-
"- Actors: KEEP major macro form changes such as Q version, non-human/beast/god/giant form, durable alternate body/form, major recurring outfit/appearance redesign, or major recurring damage/repair condition.\n" +
|
|
736
|
-
"- Actors: DROP or ACTION_STATE emotions, poses, unconscious/asleep/lying/sitting, fatigue as behavior, holding/carrying/using an item, minor marks, temporary glow, brightness/thickness/spread changes, fading/transparent/solidifying progress frames, one-off ordinary clothes, and scene-only actions.\n" +
|
|
737
|
-
"- Actors: VFX/power intensity alone is not a state. Glowing, flashing, aura, light wall, brightness, totem spreading, transparent/solidifying/recovery frames, and one-action power bursts should MERGE into the nearest broad macro form or become ACTION_STATE.\n" +
|
|
738
|
-
"- Actors: ordinary human/humanoid form is not a separate state when the default actor already covers it. Keep human-form states only when they carry a distinct age period, disguise, outfit, or major body redesign.\n" +
|
|
739
|
-
"- Actors: keep at most one broad representative for a continuous transformation/recovery process. Do not keep separate process frames for half-transparent, head-only, partial solidifying, restored, brightness increasing, mark spreading, or power burst moments.\n" +
|
|
740
|
-
"- Locations: KEEP recurring physical set conditions and scene-header time/lighting states such as 黄昏/夜晚/清晨 that require a different reusable set look; DROP camera/action-only labels.\n" +
|
|
741
|
-
"- Props: KEEP physical condition variants such as broken/open/activated/repaired only when the prop itself remains a tracked asset; DROP use/action-only states.\n" +
|
|
742
|
-
"- MERGE states that differ only by degree/progress of the same major appearance. Prefer one broad macro canonical state over many micro-steps or process frames.\n" +
|
|
743
|
-
"- Treat extraction reasons as evidence, not as decisions. Ignore self-justifying phrases such as `needs a separate image`, `reusable`, or `stable` unless the name/description prove a distinct macro form.\n" +
|
|
744
|
-
"- For one continuous transformation/recovery process, keep at most one broad representative state for the asset form. MERGE/DROP intermediate frames such as half-transparent body, lower body vanished, only head remains, partial solidifying, brightness increasing, restored/recovered, or mark spreading.\n" +
|
|
745
|
-
"- If the best canonical state name is broader than any existing name, RENAME_STATE one representative state and MERGE_STATE the variants into it.\n" +
|
|
746
|
-
"- State descriptions must contain only durable visible appearance. Remove camera/staging/story wording such as back-to-camera, face hidden, close-up, initial pose, entering, taking hands, kissing, or standing in a scene.\n" +
|
|
747
|
-
"- When keeping a useful state whose description contains camera/staging/story wording, keep the state and rewrite the description with DESC.\n" +
|
|
748
|
-
"- Every S line in the context must receive exactly one expression. Do not omit any listed state.\n" +
|
|
749
|
-
"- Every asset block with one or more S lines must also receive exactly one DEFAULT_STATE expression.\n" +
|
|
750
|
-
"- DEFAULT_STATE selects the most common normal appearance for that asset. Keep its readable state_name; deterministic apply will rewrite only its state_id to `default`.\n" +
|
|
751
|
-
"- Use only existing state_id values listed under the same asset block. Never merge across assets. Never invent ids.\n" +
|
|
752
|
-
"- Use each state's extraction reason when present. If the reason says the state is a momentary process frame, action, VFX intensity, or ordinary clothing, prefer MERGE/DROP/ACTION_STATE unless there is strong reusable macro-form evidence.\n" +
|
|
753
|
-
"- For every expression, cite a concise concrete reason from the state name/description/extraction reason.\n" +
|
|
754
|
-
"- Keep reasons short. Prefer one precise phrase.\n" +
|
|
755
|
-
"\n" +
|
|
756
|
-
"Output format:\n" +
|
|
757
|
-
"- Output plain text only. Do not output JSON, markdown tables, fenced code blocks, or explanations outside expressions.\n" +
|
|
758
|
-
"- Each expression must be one line wrapped exactly as `<exp>...</exp>`.\n" +
|
|
759
|
-
"- Every expression must include `# reason` at the end.\n" +
|
|
760
|
-
"- If there are no states to curate, output exactly `<exp>NOOP # no state curation changes</exp>`.\n" +
|
|
761
|
-
"\n" +
|
|
762
|
-
"Expression grammar:\n" +
|
|
763
|
-
"- `<exp>KEEP_STATE <kind> <asset_id> <state_id> [DESC=\"<clean visual description>\"] # reason</exp>`\n" +
|
|
764
|
-
"- `<exp>MERGE_STATE <kind> <asset_id> <state_id> -> <target_state_id> # reason</exp>`\n" +
|
|
765
|
-
"- `<exp>DROP_STATE <kind> <asset_id> <state_id> # reason</exp>`\n" +
|
|
766
|
-
"- `<exp>ACTION_STATE <kind> <asset_id> <state_id> # reason</exp>`\n" +
|
|
767
|
-
"- `<exp>RENAME_STATE <kind> <asset_id> <state_id> \"<new_name>\" [DESC=\"<clean visual description>\"] # reason</exp>`\n" +
|
|
768
|
-
"- `<exp>DEFAULT_STATE <kind> <asset_id> <state_id> # reason</exp>`\n" +
|
|
769
|
-
"- `<kind>` is one of actor, location, prop.\n" +
|
|
770
|
-
"- Use double quotes for names. Escape only `\"` and `\\\\` inside quoted strings.\n" +
|
|
771
|
-
"\n" +
|
|
772
|
-
"Context:\n" +
|
|
773
|
-
`${contextText}\n`;
|
|
774
|
-
const request = this.messageRequest(prompt, 20000);
|
|
775
|
-
const [raw, stopReason] = await this.collectResponseTextWithRetry(request);
|
|
776
|
-
if (stopReason === "max_tokens") {
|
|
777
|
-
throw new CliError("INIT FAILED: State curation output truncated", "State curation output truncated.", {
|
|
778
|
-
exitCode: EXIT_RUNTIME,
|
|
779
|
-
required: ["complete state curation expressions within provider max_tokens"],
|
|
780
|
-
received: [`stop_reason: ${stopReason}`, `max_tokens: ${request.max_tokens}`, `raw chars: ${raw.length}`, `tail: ${raw.slice(-160) || "<empty response>"}`],
|
|
781
|
-
nextSteps: ["Rerun init; completed extraction checkpoints will be reused and state curation will retry."],
|
|
782
|
-
errorCode: "PROVIDER_OUTPUT_TRUNCATED",
|
|
783
|
-
});
|
|
784
|
-
}
|
|
785
|
-
if (!raw.trim()) {
|
|
786
|
-
throw new CliError("INIT FAILED: Provider returned empty state curation", "Provider returned an empty state curation response.", {
|
|
787
|
-
exitCode: EXIT_RUNTIME,
|
|
788
|
-
required: ["one or more <exp> state curation expressions"],
|
|
789
|
-
received: [`stop_reason: ${stopReason || "unknown"}`, "raw chars: 0"],
|
|
790
|
-
nextSteps: ["Rerun init; completed extraction checkpoints will be reused and state curation will retry."],
|
|
791
|
-
errorCode: "PROVIDER_EMPTY_RESPONSE",
|
|
792
|
-
});
|
|
793
|
-
}
|
|
794
|
-
return raw;
|
|
795
|
-
}
|
|
796
|
-
async bindStates(_script, context) {
|
|
797
|
-
const prompt = "You bind scene asset refs to existing visual states for `scriptctl direct init`.\n" +
|
|
798
|
-
"Return JSON through the forced tool call. The caller will reject unknown scene, asset, or state ids.\n" +
|
|
799
|
-
"\n" +
|
|
800
|
-
"Policy:\n" +
|
|
801
|
-
"- Use only state_id values listed under that exact scene ref's `states` array. Never borrow a state_id from another asset.\n" +
|
|
802
|
-
"- Each asset should already have an asset-scoped `default` state_id. Each ref usually has `current_state_id` from deterministic default binding.\n" +
|
|
803
|
-
"- Omit refs that should keep `current_state_id`; output a ref only when the scene/actions prove it should change to another existing state.\n" +
|
|
804
|
-
"- Do not output null for refs; omit the ref instead.\n" +
|
|
805
|
-
"- Emit state_changes only for explicit durable transitions: transformation/form change, outfit change, breakage, repair, damage state, or location condition change.\n" +
|
|
806
|
-
"- Do not emit state_changes for emotions, poses, camera effects, dialogue tone, or momentary actions.\n" +
|
|
807
|
-
"- Do not include reason or explanation fields; return only the schema fields needed for deterministic apply.\n" +
|
|
808
|
-
"- Include each scene from the input chunk, even if its arrays are empty.\n" +
|
|
809
|
-
"- `action_index` is the zero-based index from the scene actions array.\n" +
|
|
810
|
-
"\n" +
|
|
811
|
-
"Context:\n" +
|
|
812
|
-
`${JSON.stringify(context)}\n`;
|
|
813
|
-
return this.runToolUse({
|
|
814
|
-
prompt,
|
|
815
|
-
maxTokens: 16000,
|
|
816
|
-
toolName: "submit_state_binding",
|
|
817
|
-
description: "Return scene state refs and explicit state changes for deterministic application.",
|
|
818
|
-
schema: STATE_BINDING_SCHEMA,
|
|
819
|
-
truncatedTitle: "INIT FAILED: State binding output truncated",
|
|
820
|
-
invalidTitle: "INIT FAILED: Provider returned invalid state binding JSON",
|
|
821
|
-
nextSteps: ["Rerun init; completed extraction and curation checkpoints will be reused and state binding will retry."],
|
|
822
|
-
});
|
|
823
|
-
}
|
|
824
|
-
// Metadata is extracted as MANY small markdown requests instead of one big one:
|
|
825
|
-
// a single GLOBAL call (worldview + protagonists, tiny input/output) plus
|
|
826
|
-
// per-asset-chunk DESCRIPTION calls (≤ DEFAULT_METADATA_ASSET_CHUNK assets each).
|
|
827
|
-
// Every request is small on the non-streaming `messages.create` path the gateway
|
|
828
|
-
// doesn't drop, chunking bounds the output, and each call retries on transient
|
|
829
|
-
// gateway errors. Pieces merge through assembleMetadataFromParts (enum/role
|
|
830
|
-
// coercion + deterministic backfill).
|
|
831
|
-
async extractMetadata(_sourceText, script) {
|
|
832
|
-
const context = buildMetadataContext(script);
|
|
833
|
-
const actors = asList(context["actors"]);
|
|
834
|
-
const locations = asList(context["locations"]);
|
|
835
|
-
const props = asList(context["props"]);
|
|
836
|
-
// 1. GLOBAL: worldview + protagonists. Tiny input — names/titles, no examples.
|
|
837
|
-
const globalContext = {
|
|
838
|
-
title: context["title"],
|
|
839
|
-
worldview_options: context["worldview_options"],
|
|
840
|
-
episodes: context["episodes"],
|
|
841
|
-
actors: actors.map((a) => ({ actor_id: a["actor_id"], actor_name: a["actor_name"] })),
|
|
842
|
-
};
|
|
843
|
-
const globalRaw = await this.runMetadataMarkdown("You decide two global fields for a script: its worldview and which actors are protagonists.\n" +
|
|
844
|
-
"Use only the structured context below. Do not invent anything outside it.", METADATA_GLOBAL_MD_SPEC, globalContext);
|
|
845
|
-
const { worldviewRaw, protagonistIds } = parseGlobalMetadataMd(globalRaw);
|
|
846
|
-
// 2. DESCRIPTIONS: pool every asset (with a uniform `id`) and chunk so each
|
|
847
|
-
// request stays small in both input and output.
|
|
848
|
-
const pooled = [
|
|
849
|
-
...actors.map((a) => ({ id: a["actor_id"], name: a["actor_name"], states: a["states"], examples: a["examples"] })),
|
|
850
|
-
...locations.map((l) => ({ id: l["location_id"], name: l["location_name"], states: l["states"], examples: l["examples"] })),
|
|
851
|
-
...props.map((p) => ({ id: p["prop_id"], name: p["prop_name"], examples: p["examples"] })),
|
|
852
|
-
];
|
|
853
|
-
// Description chunks are independent (each merges into descriptionsById by
|
|
854
|
-
// id), so fan them out concurrently instead of one-await-at-a-time. Chunk
|
|
855
|
-
// count is small (≤ ceil(assets / DEFAULT_METADATA_ASSET_CHUNK)) and every
|
|
856
|
-
// request is already tiny + self-retrying, so a plain Promise.all is the
|
|
857
|
-
// right bound — this is what turns metadata from a serial back-to-back
|
|
858
|
-
// crawl into the same parallel profile as the other LLM stages.
|
|
859
|
-
const chunks = [];
|
|
860
|
-
for (let i = 0; i < pooled.length; i += DEFAULT_METADATA_ASSET_CHUNK) {
|
|
861
|
-
chunks.push(pooled.slice(i, i + DEFAULT_METADATA_ASSET_CHUNK));
|
|
862
|
-
}
|
|
863
|
-
const descriptionsById = new Map();
|
|
864
|
-
const rawChunks = await Promise.all(chunks.map((chunk) => this.runMetadataMarkdown("You write one concise visual description per asset, for downstream art generation.\n" +
|
|
865
|
-
"Use only the structured context below. Do not invent plot events outside it.\n" +
|
|
866
|
-
"Describe durable appearance/identity only; do not copy camera, staging, pose, entrance, hand-taking, kissing, or scene-position wording from state names or examples.", METADATA_DESC_MD_SPEC, { title: context["title"], assets: chunk })));
|
|
867
|
-
for (const raw of rawChunks) {
|
|
868
|
-
for (const [id, desc] of parseAssetDescriptionsMd(raw))
|
|
869
|
-
descriptionsById.set(id, desc);
|
|
870
|
-
}
|
|
871
|
-
if (descriptionsById.size === 0 && !worldviewRaw.trim() && protagonistIds.length === 0) {
|
|
872
|
-
// Every request came back unusable (prose refusal / wrong format) — reject
|
|
873
|
-
// instead of silently shipping a name-only deterministic baseline.
|
|
874
|
-
throw new CliError("INIT FAILED: Provider returned invalid metadata", "Provider returned no recognizable metadata markdown (content filter / wrong format).", {
|
|
875
|
-
exitCode: EXIT_RUNTIME,
|
|
876
|
-
required: ["metadata markdown (# 世界观 / # 主角 / # 描述 sections)"],
|
|
877
|
-
nextSteps: ["Rerun init; extraction checkpoints will be reused and metadata will retry."],
|
|
878
|
-
errorCode: "PROVIDER_CONTENT_FILTERED",
|
|
879
|
-
});
|
|
880
|
-
}
|
|
881
|
-
return assembleMetadataFromParts(script, worldviewRaw, protagonistIds, descriptionsById);
|
|
882
|
-
}
|
|
883
|
-
// Shared metadata round-trip: build the markdown prompt, run it on the
|
|
884
|
-
// non-streaming path with transient-error retry, and surface truncation as a
|
|
885
|
-
// CliError. Returns the raw markdown body.
|
|
886
|
-
async runMetadataMarkdown(intro, spec, context) {
|
|
887
|
-
const prompt = `${intro}\n\n${spec}\n\nContext:\n${JSON.stringify(context)}\n`;
|
|
888
|
-
const request = this.messageRequest(prompt, DEFAULT_BATCH_MAX_TOKENS);
|
|
889
|
-
const [raw, stopReason] = await this.collectResponseTextWithRetry(request);
|
|
890
|
-
if (stopReason === "max_tokens") {
|
|
891
|
-
throw new CliError("INIT FAILED: Metadata output truncated", "Metadata output truncated.", {
|
|
892
|
-
exitCode: EXIT_RUNTIME,
|
|
893
|
-
required: ["complete metadata markdown within provider max_tokens"],
|
|
894
|
-
received: [`stop_reason: ${stopReason}`, `max_tokens: ${request.max_tokens}`, `raw chars: ${raw.length}`, `tail: ${raw.slice(-160) || "<empty response>"}`],
|
|
895
|
-
nextSteps: ["Rerun init; extraction checkpoints will be reused and metadata will retry."],
|
|
896
|
-
errorCode: "PROVIDER_OUTPUT_TRUNCATED",
|
|
897
|
-
});
|
|
898
|
-
}
|
|
899
|
-
return raw;
|
|
900
|
-
}
|
|
901
|
-
// collectResponseText + bounded backoff retry on transient gateway errors
|
|
902
|
-
// (peer closed / connection reset / 5xx — see isRetryableProviderError). Unlike
|
|
903
|
-
// extractBatch's bare call, metadata's small requests self-heal a flaky gateway
|
|
904
|
-
// instead of hard-failing the whole init on a single drop.
|
|
905
|
-
async collectResponseTextWithRetry(request) {
|
|
906
|
-
const attempts = providerAttempts();
|
|
907
|
-
let lastExc = null;
|
|
908
|
-
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
909
|
-
try {
|
|
910
|
-
return await this.collectResponseText(request);
|
|
911
|
-
}
|
|
912
|
-
catch (exc) {
|
|
913
|
-
if (isRetryableProviderError(exc) && attempt < attempts) {
|
|
914
|
-
lastExc = exc;
|
|
915
|
-
await sleepMs(Math.min(8000, 800 * attempt));
|
|
916
|
-
continue;
|
|
917
|
-
}
|
|
918
|
-
throw exc;
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
throw lastExc ?? new Error("metadata request failed after retries");
|
|
922
|
-
}
|
|
923
|
-
async extractEpisodeSynopsis(synopses, ctx) {
|
|
924
|
-
const beats = cleanSynopsisList(synopses);
|
|
925
|
-
const label = strOf(ctx["title"]).trim() || strOf(ctx["episode"]).trim() || "this group";
|
|
926
|
-
const isEpisode = strOf(ctx["episode"]).trim().length > 0;
|
|
927
|
-
const lengthRequirement = isEpisode
|
|
928
|
-
? "- synopsis: 200-300 Chinese characters, written as one connected paragraph.\n"
|
|
929
|
-
: "- synopsis: 400-600 Chinese characters, written as one connected stage/arc synopsis paragraph.\n";
|
|
930
|
-
const prompt = "You stitch ordered plot beats into ONE coherent synopsis.\n" +
|
|
931
|
-
"The beats below are in chronological order and all belong to the same unit\n" +
|
|
932
|
-
`(${label}). Produce a single connected synopsis, not a list.\n` +
|
|
933
|
-
"\n" +
|
|
934
|
-
"Requirements:\n" +
|
|
935
|
-
lengthRequirement +
|
|
936
|
-
"- Chinese plain text only.\n" +
|
|
937
|
-
"- Summarize ONLY the given beats. Do not invent events or add anything not present.\n" +
|
|
938
|
-
"- Do not spoil beyond the beats; do not list assets.\n" +
|
|
939
|
-
"- Keep the chronological order of the beats.\n" +
|
|
940
|
-
"\n" +
|
|
941
|
-
"Plot beats (ordered):\n" +
|
|
942
|
-
`${JSON.stringify(beats)}\n`;
|
|
943
|
-
return this.runToolUse({
|
|
944
|
-
prompt,
|
|
945
|
-
maxTokens: 2048,
|
|
946
|
-
toolName: "submit_episode_synopsis",
|
|
947
|
-
description: "Return one coherent synopsis stitched from the ordered plot beats.",
|
|
948
|
-
schema: EPISODE_SYNOPSIS_SCHEMA,
|
|
949
|
-
truncatedTitle: "SUMMARY FAILED: Synopsis output truncated",
|
|
950
|
-
invalidTitle: "SUMMARY FAILED: Provider returned invalid synopsis JSON",
|
|
951
|
-
nextSteps: ["Rerun init; the summary stage degrades to joined beats if this repeats."],
|
|
952
|
-
});
|
|
953
|
-
}
|
|
954
|
-
async extractScriptSynopsis(synopses, ctx) {
|
|
955
|
-
const episodeSynopses = cleanSynopsisList(synopses);
|
|
956
|
-
const title = strOf(ctx["title"]).trim();
|
|
957
|
-
const prompt = "You produce a whole-script overview from per-episode synopses.\n" +
|
|
958
|
-
`Script title: ${title || "(untitled)"}.\n` +
|
|
959
|
-
"Use ONLY the episode synopses below; do not invent plot, characters, or facts.\n" +
|
|
960
|
-
"Return JSON matching the configured schema.\n" +
|
|
961
|
-
"\n" +
|
|
962
|
-
"Requirements:\n" +
|
|
963
|
-
"- synopsis: 800-1200 Chinese characters; a connected whole-plot overview covering the main plot, key relationships, major conflicts, and ending direction.\n" +
|
|
964
|
-
"- theme: the central theme / 主旨, one short Chinese phrase or sentence.\n" +
|
|
965
|
-
"- logline: a single Chinese sentence capturing the whole story.\n" +
|
|
966
|
-
"- style: genre / tone as free Chinese text (e.g. 都市甜宠 / 悬疑复仇).\n" +
|
|
967
|
-
"- main_characters: the main people only, each with name and a short role (主角/配角/身份).\n" +
|
|
968
|
-
"- Ground everything in the episode synopses; if information is missing, keep it brief rather than inventing.\n" +
|
|
969
|
-
"\n" +
|
|
970
|
-
"Episode synopses (ordered):\n" +
|
|
971
|
-
`${JSON.stringify(episodeSynopses)}\n`;
|
|
972
|
-
return this.runToolUse({
|
|
973
|
-
prompt,
|
|
974
|
-
maxTokens: 2048,
|
|
975
|
-
toolName: "submit_script_synopsis",
|
|
976
|
-
description: "Return the whole-script overview (synopsis, theme, logline, style, main_characters).",
|
|
977
|
-
schema: SCRIPT_SYNOPSIS_SCHEMA,
|
|
978
|
-
truncatedTitle: "SUMMARY FAILED: Script synopsis output truncated",
|
|
979
|
-
invalidTitle: "SUMMARY FAILED: Provider returned invalid script synopsis JSON",
|
|
980
|
-
nextSteps: ["Rerun init; the summary stage leaves overview fields empty if this repeats."],
|
|
981
|
-
});
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
// ---------------------------------------------------------------------------
|
|
985
|
-
// OpenAI-compatible text provider
|
|
986
|
-
// ---------------------------------------------------------------------------
|
|
987
|
-
const OPENAI_TEXT_MAX_TOKENS = 4096;
|
|
988
|
-
export class OpenAICompatibleProvider {
|
|
989
|
-
name = "openai";
|
|
990
|
-
apiKey;
|
|
991
|
-
endpoint;
|
|
992
|
-
model;
|
|
993
|
-
constructor(model) {
|
|
994
|
-
const apiKey = (process.env.PROJECT_LITELLM_GATEWAY_API_KEY ?? "").trim();
|
|
995
|
-
if (!apiKey) {
|
|
996
|
-
throw new CliError("INIT FAILED: provider not configured", "Provider credentials are not configured for this environment.", {
|
|
997
|
-
exitCode: EXIT_RUNTIME,
|
|
998
|
-
required: ["PROJECT_LITELLM_GATEWAY_API_KEY"],
|
|
999
|
-
received: ["no API key environment variable"],
|
|
1000
|
-
nextSteps: ["Run `scriptctl doctor` to identify missing configuration."],
|
|
1001
|
-
errorCode: "PROVIDER_AUTH_MISSING",
|
|
1002
|
-
});
|
|
1003
|
-
}
|
|
1004
|
-
const baseUrl = (process.env.PROJECT_LITELLM_GATEWAY_BASE_URL ?? "").trim();
|
|
1005
|
-
if (!baseUrl) {
|
|
1006
|
-
throw new CliError("INIT FAILED: provider not configured", "Provider endpoint is not configured for this environment.", {
|
|
1007
|
-
exitCode: EXIT_RUNTIME,
|
|
1008
|
-
required: ["PROJECT_LITELLM_GATEWAY_BASE_URL"],
|
|
1009
|
-
received: ["no provider base URL"],
|
|
1010
|
-
nextSteps: ["Run `scriptctl doctor` to identify missing configuration."],
|
|
1011
|
-
errorCode: "PROVIDER_ENDPOINT_MISSING",
|
|
1012
|
-
});
|
|
1013
|
-
}
|
|
1014
|
-
this.apiKey = apiKey;
|
|
1015
|
-
this.endpoint = openAiChatCompletionsEndpoint(baseUrl);
|
|
1016
|
-
this.model = model;
|
|
1017
|
-
}
|
|
1018
|
-
async completeText(request) {
|
|
1019
|
-
const messages = [];
|
|
1020
|
-
const system = request.system.trim();
|
|
1021
|
-
if (system)
|
|
1022
|
-
messages.push({ role: "system", content: system });
|
|
1023
|
-
messages.push({ role: "user", content: request.user });
|
|
1024
|
-
const body = {
|
|
1025
|
-
model: this.model,
|
|
1026
|
-
messages,
|
|
1027
|
-
max_tokens: OPENAI_TEXT_MAX_TOKENS,
|
|
1028
|
-
};
|
|
1029
|
-
const response = await fetch(this.endpoint, {
|
|
1030
|
-
method: "POST",
|
|
1031
|
-
headers: {
|
|
1032
|
-
"authorization": `Bearer ${this.apiKey}`,
|
|
1033
|
-
"content-type": "application/json",
|
|
1034
|
-
},
|
|
1035
|
-
body: JSON.stringify(body),
|
|
1036
|
-
signal: request.signal,
|
|
1037
|
-
});
|
|
1038
|
-
const rawBody = await response.text();
|
|
1039
|
-
if (!response.ok) {
|
|
1040
|
-
throw new CliError("LLM FAILED: Provider request failed", "Provider request failed.", {
|
|
1041
|
-
exitCode: EXIT_RUNTIME,
|
|
1042
|
-
required: ["successful OpenAI-compatible chat completion"],
|
|
1043
|
-
received: [`status: ${response.status}`, `body: ${rawBody.slice(0, 500) || "<empty response>"}`],
|
|
1044
|
-
nextSteps: ["Check the provider name/model and rerun; completed checkpoints will be reused."],
|
|
1045
|
-
errorCode: "PROVIDER_REQUEST_FAILED",
|
|
1046
|
-
});
|
|
1047
|
-
}
|
|
1048
|
-
const payload = parseJsonDict(rawBody, "LLM FAILED: Provider returned invalid JSON");
|
|
1049
|
-
const choice = firstOpenAiChoice(payload);
|
|
1050
|
-
const finishReason = choice ? strOf(choice["finish_reason"]).trim() : "";
|
|
1051
|
-
const text = openAiChoiceContent(choice).trim();
|
|
1052
|
-
if (finishReason === "length") {
|
|
1053
|
-
throw new CliError("LLM FAILED: Provider output truncated", "Provider output truncated.", {
|
|
1054
|
-
exitCode: EXIT_RUNTIME,
|
|
1055
|
-
required: ["complete text within provider max_tokens"],
|
|
1056
|
-
received: [`finish_reason: ${finishReason}`, `max_tokens: ${OPENAI_TEXT_MAX_TOKENS}`, `raw chars: ${text.length}`, `tail: ${text.slice(-160) || "<empty response>"}`],
|
|
1057
|
-
nextSteps: ["Rerun the command; completed checkpoints will be reused. If this repeats, lower the input chunk size."],
|
|
1058
|
-
errorCode: "PROVIDER_OUTPUT_TRUNCATED",
|
|
1059
|
-
});
|
|
1060
|
-
}
|
|
1061
|
-
if (!text) {
|
|
1062
|
-
throw new CliError("LLM FAILED: Provider returned empty text", "Provider returned an empty response.", {
|
|
1063
|
-
exitCode: EXIT_RUNTIME,
|
|
1064
|
-
required: ["non-empty text response"],
|
|
1065
|
-
received: [`finish_reason: ${finishReason || "unknown"}`, `raw chars: ${rawBody.length}`],
|
|
1066
|
-
nextSteps: ["Rerun with a smaller chunk or adjust the prompt/spec."],
|
|
1067
|
-
errorCode: "PROVIDER_EMPTY_RESPONSE",
|
|
1068
|
-
});
|
|
1069
|
-
}
|
|
1070
|
-
return { text, raw: { finish_reason: finishReason } };
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
function textFromResponse(response) {
|
|
1074
|
-
const parts = [];
|
|
1075
|
-
for (const block of response.content ?? []) {
|
|
1076
|
-
if (block.text)
|
|
1077
|
-
parts.push(block.text);
|
|
1078
|
-
}
|
|
1079
|
-
return parts.join("\n").trim();
|
|
1080
|
-
}
|
|
1081
|
-
function firstNonEmptySourceLine(text) {
|
|
1082
|
-
const lines = text.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
1083
|
-
return lines.find((line) => !line.endsWith(":") && !line.endsWith(":")) ?? lines[0] ?? "";
|
|
1084
|
-
}
|
|
1085
|
-
function mockAssetId(asset, kind) {
|
|
1086
|
-
if (kind === "actor")
|
|
1087
|
-
return strOf(asset["actor_id"]).trim();
|
|
1088
|
-
if (kind === "location")
|
|
1089
|
-
return strOf(asset["location_id"]).trim();
|
|
1090
|
-
return strOf(asset["prop_id"]).trim();
|
|
1091
|
-
}
|
|
1092
|
-
function mockAssetName(asset, kind) {
|
|
1093
|
-
if (kind === "actor")
|
|
1094
|
-
return strOf(asset["actor_name"]).trim();
|
|
1095
|
-
if (kind === "location")
|
|
1096
|
-
return strOf(asset["location_name"]).trim();
|
|
1097
|
-
return strOf(asset["prop_name"]).trim();
|
|
1098
|
-
}
|
|
1099
|
-
function actorVisualVariant(name) {
|
|
1100
|
-
const prefix = /^(Q版|幻影|兽形|本体|灵体|黑化|幼年|少年|老年)[\s·::、-]*(.+)$/.exec(name);
|
|
1101
|
-
if (prefix)
|
|
1102
|
-
return { baseName: prefix[2].trim(), stateName: prefix[1].trim() };
|
|
1103
|
-
const suffix = /^(.+?)[\s·::、-]*(Q版|幻影|兽形|本体|灵体|黑化|幼年|少年|老年)$/.exec(name);
|
|
1104
|
-
if (suffix)
|
|
1105
|
-
return { baseName: suffix[1].trim(), stateName: suffix[2].trim() };
|
|
1106
|
-
return null;
|
|
1107
|
-
}
|
|
1108
|
-
function looksLikeGroupSpeaker(name) {
|
|
1109
|
-
return /众人|群众|围观者|围观群众|路人们|大家|人群|村民们|族人们|员工们|同事们/.test(name) ||
|
|
1110
|
-
/^路人[甲乙丙丁ABCD]$/.test(name);
|
|
1111
|
-
}
|
|
1112
|
-
function isMicroLocationName(name) {
|
|
1113
|
-
return /门口|门前|门边|角落|墙边|窗边|桌边|床边|车旁|车前|车后|楼道|走廊/.test(name);
|
|
1114
|
-
}
|
|
1115
|
-
function isDisposableOrUiProp(name) {
|
|
1116
|
-
return /一次性|手机画面|屏幕|界面|弹窗|提示框|聊天框|字幕条/.test(name);
|
|
1117
|
-
}
|
|
1118
|
-
function mockStateCategory(kind, name) {
|
|
1119
|
-
if (kind === "location" && /^(白天|日间|上午|下午|午后|中午|清晨|早晨|黎明|夜晚|夜间|夜里|深夜|凌晨|黄昏|傍晚)$/.test(name)) {
|
|
1120
|
-
return { action: "keep", group: `location-time:${name}` };
|
|
1121
|
-
}
|
|
1122
|
-
if (/默认/.test(name))
|
|
1123
|
-
return { action: "keep", group: "default" };
|
|
1124
|
-
if (/Q版|卡通/.test(name))
|
|
1125
|
-
return { action: "rename", canonicalName: "Q版", group: "q" };
|
|
1126
|
-
if (/透明|消融|半透明|仅余|残躯|凝实|复原中/.test(name))
|
|
1127
|
-
return { action: "action_desc" };
|
|
1128
|
-
if (/巨人|神像|神明|金身|百米/.test(name))
|
|
1129
|
-
return { action: "rename", canonicalName: "百米金色巨人", group: "giant" };
|
|
1130
|
-
if (/巨熊|熊形|兽形|神兽|真身|龙形|兽化/.test(name))
|
|
1131
|
-
return { action: "rename", canonicalName: "标准巨熊", group: "beast" };
|
|
1132
|
-
if (/战甲|铠甲|黑化|幼年|少年|老年|破损|碎裂|烧毁|修复|打开|关闭/.test(name))
|
|
1133
|
-
return { action: "keep", group: name };
|
|
1134
|
-
return { action: "action_desc" };
|
|
1135
|
-
}
|
|
1136
|
-
function mockStateCurationPlan(contextText) {
|
|
1137
|
-
const decisions = [];
|
|
1138
|
-
const defaultSelections = [];
|
|
1139
|
-
let currentKind = null;
|
|
1140
|
-
let currentAssetId = "";
|
|
1141
|
-
let currentDefaultStateId = "";
|
|
1142
|
-
let currentFirstStateId = "";
|
|
1143
|
-
const groupTargetByAsset = new Map();
|
|
1144
|
-
const flushDefault = () => {
|
|
1145
|
-
if (!currentKind || !currentAssetId)
|
|
1146
|
-
return;
|
|
1147
|
-
const stateId = currentDefaultStateId || currentFirstStateId;
|
|
1148
|
-
if (!stateId)
|
|
1149
|
-
return;
|
|
1150
|
-
defaultSelections.push({
|
|
1151
|
-
kind: currentKind,
|
|
1152
|
-
asset_id: currentAssetId,
|
|
1153
|
-
state_id: stateId,
|
|
1154
|
-
decision: "default",
|
|
1155
|
-
reason: "mock: selected common baseline visual state.",
|
|
1156
|
-
});
|
|
1157
|
-
};
|
|
1158
|
-
for (const rawLine of contextText.split(/\r?\n/)) {
|
|
1159
|
-
const assetMatch = /^(?:A|L|P)\s+\|\s+(actor|location|prop)\s+\|\s+(\S+)/.exec(rawLine.trim());
|
|
1160
|
-
if (assetMatch) {
|
|
1161
|
-
flushDefault();
|
|
1162
|
-
currentKind = assetMatch[1];
|
|
1163
|
-
currentAssetId = assetMatch[2] ?? "";
|
|
1164
|
-
currentDefaultStateId = "";
|
|
1165
|
-
currentFirstStateId = "";
|
|
1166
|
-
continue;
|
|
1167
|
-
}
|
|
1168
|
-
const stateMatch = /^\s*S\s+\|\s+(\S+)\s+\|\s+([^|]+?)\s+\|/.exec(rawLine);
|
|
1169
|
-
if (!stateMatch || !currentKind || !currentAssetId)
|
|
1170
|
-
continue;
|
|
1171
|
-
const stateId = stateMatch[1] ?? "";
|
|
1172
|
-
const stateName = strOf(stateMatch[2]).trim();
|
|
1173
|
-
if (!currentFirstStateId)
|
|
1174
|
-
currentFirstStateId = stateId;
|
|
1175
|
-
const category = mockStateCategory(currentKind, stateName);
|
|
1176
|
-
const groupKey = `${currentKind}:${currentAssetId}:${category.group ?? "action"}`;
|
|
1177
|
-
if (category.action === "action_desc") {
|
|
1178
|
-
decisions.push({
|
|
1179
|
-
kind: currentKind,
|
|
1180
|
-
asset_id: currentAssetId,
|
|
1181
|
-
state_id: stateId,
|
|
1182
|
-
decision: "action_desc",
|
|
1183
|
-
reason: "mock: transient visual/action detail belongs in action text.",
|
|
1184
|
-
});
|
|
1185
|
-
continue;
|
|
1186
|
-
}
|
|
1187
|
-
const target = category.group ? groupTargetByAsset.get(groupKey) : null;
|
|
1188
|
-
if (target && target !== stateId) {
|
|
1189
|
-
decisions.push({
|
|
1190
|
-
kind: currentKind,
|
|
1191
|
-
asset_id: currentAssetId,
|
|
1192
|
-
state_id: stateId,
|
|
1193
|
-
decision: "merge",
|
|
1194
|
-
target_state_id: target,
|
|
1195
|
-
reason: "mock: degree variants collapse to one reusable visual state.",
|
|
1196
|
-
});
|
|
1197
|
-
continue;
|
|
1198
|
-
}
|
|
1199
|
-
if (category.group)
|
|
1200
|
-
groupTargetByAsset.set(groupKey, stateId);
|
|
1201
|
-
if (!currentDefaultStateId)
|
|
1202
|
-
currentDefaultStateId = stateId;
|
|
1203
|
-
if (category.action === "rename" && category.canonicalName && category.canonicalName !== stateName) {
|
|
1204
|
-
decisions.push({
|
|
1205
|
-
kind: currentKind,
|
|
1206
|
-
asset_id: currentAssetId,
|
|
1207
|
-
state_id: stateId,
|
|
1208
|
-
decision: "rename",
|
|
1209
|
-
new_name: category.canonicalName,
|
|
1210
|
-
reason: "mock: broad canonical visual form.",
|
|
1211
|
-
});
|
|
1212
|
-
}
|
|
1213
|
-
else {
|
|
1214
|
-
decisions.push({
|
|
1215
|
-
kind: currentKind,
|
|
1216
|
-
asset_id: currentAssetId,
|
|
1217
|
-
state_id: stateId,
|
|
1218
|
-
decision: "keep",
|
|
1219
|
-
reason: "mock: reusable visual state.",
|
|
1220
|
-
});
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
flushDefault();
|
|
1224
|
-
return { decisions, default_selections: defaultSelections };
|
|
1225
|
-
}
|
|
1226
|
-
function mockBindingRefs(scene, refKey, idKey, actionText) {
|
|
1227
|
-
const refs = isDict(scene["refs"]) ? scene["refs"] : {};
|
|
1228
|
-
const out = [];
|
|
1229
|
-
for (const ref of asList(refs[refKey])) {
|
|
1230
|
-
if (!isDict(ref))
|
|
1231
|
-
continue;
|
|
1232
|
-
const id = strOf(ref["id"]).trim();
|
|
1233
|
-
const states = asList(ref["states"]);
|
|
1234
|
-
if (!id || states.length === 0)
|
|
1235
|
-
continue;
|
|
1236
|
-
const stateId = pickMockStateId(states, actionText);
|
|
1237
|
-
out.push({ [idKey]: id, state_id: stateId });
|
|
1238
|
-
}
|
|
1239
|
-
return out;
|
|
1240
|
-
}
|
|
1241
|
-
function pickMockStateId(states, actionText) {
|
|
1242
|
-
for (const state of states) {
|
|
1243
|
-
const name = strOf(state["state_name"]).trim();
|
|
1244
|
-
const stateId = strOf(state["state_id"]).trim();
|
|
1245
|
-
if (name && name !== "默认" && stateId && actionText.includes(name))
|
|
1246
|
-
return stateId;
|
|
1247
|
-
}
|
|
1248
|
-
for (const state of states) {
|
|
1249
|
-
if (strOf(state["state_id"]).trim() === "default")
|
|
1250
|
-
return strOf(state["state_id"]).trim() || null;
|
|
1251
|
-
}
|
|
1252
|
-
return null;
|
|
1253
|
-
}
|
|
1254
|
-
function mockStateChanges(scene, actions) {
|
|
1255
|
-
const refs = isDict(scene["refs"]) ? scene["refs"] : {};
|
|
1256
|
-
const changeCues = /变成|变身|化作|恢复|变回|换上|穿上|脱下|破损|碎裂|修复|烧毁|亮起|熄灭/;
|
|
1257
|
-
const out = [];
|
|
1258
|
-
const seen = new Set();
|
|
1259
|
-
for (let actionIndex = 0; actionIndex < actions.length; actionIndex++) {
|
|
1260
|
-
const content = strOf(actions[actionIndex]["content"]);
|
|
1261
|
-
if (!changeCues.test(content))
|
|
1262
|
-
continue;
|
|
1263
|
-
for (const [refKey, targetKind] of [
|
|
1264
|
-
["actors", "actor"],
|
|
1265
|
-
["locations", "location"],
|
|
1266
|
-
["props", "prop"],
|
|
1267
|
-
]) {
|
|
1268
|
-
for (const ref of asList(refs[refKey])) {
|
|
1269
|
-
if (!isDict(ref))
|
|
1270
|
-
continue;
|
|
1271
|
-
const targetId = strOf(ref["id"]).trim();
|
|
1272
|
-
const states = asList(ref["states"]);
|
|
1273
|
-
const toStateId = pickExplicitMockStateId(states, content);
|
|
1274
|
-
if (!targetId || !toStateId)
|
|
1275
|
-
continue;
|
|
1276
|
-
const key = `${actionIndex}:${targetKind}:${targetId}`;
|
|
1277
|
-
if (seen.has(key))
|
|
1278
|
-
continue;
|
|
1279
|
-
seen.add(key);
|
|
1280
|
-
const defaultStateId = mockDefaultStateId(states);
|
|
1281
|
-
out.push({
|
|
1282
|
-
action_index: actionIndex,
|
|
1283
|
-
target_kind: targetKind,
|
|
1284
|
-
target_id: targetId,
|
|
1285
|
-
from_state_id: defaultStateId || null,
|
|
1286
|
-
to_state_id: toStateId,
|
|
1287
|
-
effective_from: "after_action",
|
|
1288
|
-
reason: "mock: action contains an explicit durable visual state change.",
|
|
1289
|
-
});
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
return out;
|
|
1294
|
-
}
|
|
1295
|
-
function pickExplicitMockStateId(states, content) {
|
|
1296
|
-
for (const state of states) {
|
|
1297
|
-
const name = strOf(state["state_name"]).trim();
|
|
1298
|
-
const stateId = strOf(state["state_id"]).trim();
|
|
1299
|
-
if (name && name !== "默认" && stateId && content.includes(name))
|
|
1300
|
-
return stateId;
|
|
1301
|
-
}
|
|
1302
|
-
return "";
|
|
1303
|
-
}
|
|
1304
|
-
function mockDefaultStateId(states) {
|
|
1305
|
-
for (const state of states) {
|
|
1306
|
-
if (strOf(state["state_id"]).trim() === "default")
|
|
1307
|
-
return strOf(state["state_id"]).trim();
|
|
1308
|
-
}
|
|
1309
|
-
return "";
|
|
1310
|
-
}
|
|
1311
|
-
/**
|
|
1312
|
-
* Find a forced tool_use block by name in a Message's content array.
|
|
1313
|
-
* Returns its `input` (already parsed by the SDK) or `null` when missing/non-object.
|
|
1314
|
-
*/
|
|
1315
|
-
function findToolUseInput(content, toolName) {
|
|
1316
|
-
for (const block of content) {
|
|
1317
|
-
if (block?.type === "tool_use" && block.name === toolName) {
|
|
1318
|
-
const inp = block.input;
|
|
1319
|
-
if (inp && typeof inp === "object" && !Array.isArray(inp))
|
|
1320
|
-
return inp;
|
|
1321
|
-
return null;
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
return null;
|
|
1325
|
-
}
|
|
1326
|
-
function openAiChatCompletionsEndpoint(baseUrl) {
|
|
1327
|
-
const base = baseUrl.trim().replace(/\/+$/, "");
|
|
1328
|
-
if (base.endsWith("/chat/completions"))
|
|
1329
|
-
return base;
|
|
1330
|
-
if (base.endsWith("/v1"))
|
|
1331
|
-
return `${base}/chat/completions`;
|
|
1332
|
-
return `${base}/v1/chat/completions`;
|
|
1333
|
-
}
|
|
1334
|
-
function parseJsonDict(raw, title) {
|
|
1335
|
-
try {
|
|
1336
|
-
const payload = JSON.parse(raw);
|
|
1337
|
-
if (isDict(payload))
|
|
1338
|
-
return payload;
|
|
1339
|
-
}
|
|
1340
|
-
catch {
|
|
1341
|
-
// Fall through to the structured provider error below.
|
|
1342
|
-
}
|
|
1343
|
-
throw new CliError(title, "Provider returned invalid JSON.", {
|
|
1344
|
-
exitCode: EXIT_RUNTIME,
|
|
1345
|
-
required: ["OpenAI-compatible JSON response"],
|
|
1346
|
-
received: [`body: ${raw.slice(0, 500) || "<empty response>"}`],
|
|
1347
|
-
nextSteps: ["Rerun the command; completed checkpoints will be reused."],
|
|
1348
|
-
errorCode: "PROVIDER_INVALID_JSON",
|
|
1349
|
-
});
|
|
1350
|
-
}
|
|
1351
|
-
function firstOpenAiChoice(payload) {
|
|
1352
|
-
const choices = payload["choices"];
|
|
1353
|
-
if (!Array.isArray(choices))
|
|
1354
|
-
return null;
|
|
1355
|
-
for (const choice of choices) {
|
|
1356
|
-
if (isDict(choice))
|
|
1357
|
-
return choice;
|
|
1358
|
-
}
|
|
1359
|
-
return null;
|
|
1360
|
-
}
|
|
1361
|
-
function openAiChoiceContent(choice) {
|
|
1362
|
-
if (!choice)
|
|
1363
|
-
return "";
|
|
1364
|
-
const message = isDict(choice["message"]) ? choice["message"] : null;
|
|
1365
|
-
if (!message)
|
|
1366
|
-
return "";
|
|
1367
|
-
const content = message["content"];
|
|
1368
|
-
if (typeof content === "string")
|
|
1369
|
-
return content;
|
|
1370
|
-
if (!Array.isArray(content))
|
|
1371
|
-
return "";
|
|
1372
|
-
const parts = [];
|
|
1373
|
-
for (const item of content) {
|
|
1374
|
-
if (isDict(item) && typeof item["text"] === "string")
|
|
1375
|
-
parts.push(item["text"]);
|
|
1376
|
-
}
|
|
1377
|
-
return parts.join("\n");
|
|
1378
|
-
}
|
|
1379
|
-
// ---------------------------------------------------------------------------
|
|
1380
|
-
// Factory
|
|
1381
|
-
// ---------------------------------------------------------------------------
|
|
1382
|
-
export function makeProvider(name, model) {
|
|
1383
|
-
if (name === "mock")
|
|
1384
|
-
return new MockProvider();
|
|
1385
|
-
if (name === "anthropic")
|
|
1386
|
-
return new AnthropicProvider(model);
|
|
1387
|
-
if (name === "openai")
|
|
1388
|
-
return new OpenAICompatibleProvider(model);
|
|
1389
|
-
throw new CliError("INIT FAILED: Unsupported provider", "Unsupported provider.", {
|
|
1390
|
-
exitCode: EXIT_USAGE,
|
|
1391
|
-
required: ["--provider: anthropic | openai | mock"],
|
|
1392
|
-
received: [`--provider: ${name}`],
|
|
1393
|
-
nextSteps: ["Use --provider anthropic, --provider openai, or --provider mock."],
|
|
1394
|
-
});
|
|
1395
|
-
}
|
|
1396
|
-
//# sourceMappingURL=providers.js.map
|